手动分段:SECTION
和SEGMENT
同义
asm
section data
data:
dw 0xffff
section code
mov bx, data
段对齐:section xxx align=16
表示16字节对齐
段起始处定为汇编地址起始处:section xxx vstart=0
指定段内数据的汇编地址从0开始,即在该段中,段起始处就是汇编地址起始处
主引导扇区大小有限,将其作为程序加载器,用于加载用户程序,加载器通过用户程序头部段获取用户程序信息:
asm
SECTION header vstart=0 ;定义用户程序头部段
program_length dd program_end ;程序总长度
;用户程序入口点
code_entry dw start ;偏移地址
dd section.code.start ;段地址(4字节) = section.段名.start
realloc_tbl_len dw (segtbl_end-segtbl_begin)/4
;段重定位表项个数,每个表项4字节
;段重定位表
segtbl_begin:
code_segment dd section.code.start
data_segment dd section.data.start
stack_segment dd section.stack.start
segtbl_end:
;===============================================================================
SECTION code align=16 vstart=0
start:
...
;===============================================================================
SECTION data align=16 vstart=0
...
;===============================================================================
SECTION stack align=16 vstart=0
...
stack_end:
;===============================================================================
SECTION trail align=16
program_end:
加载器的工作流程:
- 读取用户程序的起始扇区
- 把整个用户程序都读入内存
- 计算段的物理地址和逻辑段地址(段的重定位)
- 转移到用户程序执行(将处理器的控制权交给用户程序)
确定用户程序加载位置:
asm
app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号)
;常数的声明不会占用汇编地址
SECTION mbr align=16 vstart=0x7c00
...
phy_base dd 0x10000 ;用户程序被加载的物理起始地址
times 510-($-$$) db 0
db 0x55,0xaa
主引导扇区起始地址0000:0x7c00,用户程序物理起始地址0000:0x7c00+phy_base,当段使用vstart=0x7c00
时,用户程序物理起始地址简化为0000:phy_base
在mbr段内,先找到用户程序所在段,这就需要将物理起始地址转化为16位段地址,即0x10000除0x10(右移1位)
asm
mov ax, [cs:phy_base] ;计算用于加载用户程序的逻辑段地址
mov dx, [cs:phy_base+0x02]
mov bx, 0x10
div bx
mov ds, ax ;令DS和ES指向该段以进行操作
mov es, ax
端口号多少与端口本身大小(8位还是16位等)无关
输入/输出端口访问:
asm
; 输入 设备->处理器
; in al/ax, dx/imm8
mov dx, 0xc30
in al, dx ;访问端口号为0xc30的输入端口
;访问8位端口时使用al,16位时用ax
in al, 0x60 ;端口号小于256可以用立即数
; 输出 处理器->设备
; out dx/imm8, al/ax
mov dx, 0x3c0
out dx, ax
out 0x60, al
访问端口必须以扇区为单位,使用LBA
asm
mov dx, 0x1f2 ; 端口号
mov al, 1 ; 1个扇区(1-255),若为0表示输出256个扇区
out dx, al
构建端口到磁盘扇区(设备)的映射
asm
; 逻辑扇区号(LBA地址)=0000 00000000 00000000 00000010=0x02即表示
mov dx, 0x1f3 ; 指定某磁盘操作端口号
mov al, 0x02 ; 末位扇区号/表示读写两个扇区
out dx, al ; LBA地址的0~7位
inc dx ; 端口号0x1f4
mov al, 0x00 ; 256个扇区
out dx, al ; LBA地址8~15位
inc dx ; 端口号0x1f5
out dx, al ; LBA地址16~23位
inc dx ; 端口号0x1f6
mov al, 0xe0 ; 1110 0000
out dx, al ; LBA地址20~27位
端口0x1f7
是命令端口,即可以接收命令,也是状态端口,可以返回状态
asm
mov dx, 0x1f7
mov al, 0x20 ; 读命令
out dx, al ; 将读命令发送给命令端口
等待磁盘状态
asm
mov dx, 0x1f7
.waits:
in al, dx ; 把磁盘状态返回给寄存器
and al, 0x88 ; 与1000_1000 = 取出3、7位
cmp al, 0x08 ; 0000_1000 判断是否准备好
jnz .waits
端口1f0
是硬盘的数据取出端口
连续取出数据
asm
; ds指向将存放扇区数据的段,bx为偏移地址
mov cx, 256 ; 总共要读取的字数
mov dx, 0x1f0
.readw:
in ax, dx ; 读一个字
mov [bx], ax ; 传入内存
add bx, 2 ; 内存偏移地址+2
loop .readw
整合进mbr段内(16位相对近调用-同段内调用)
asm
app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号)
;常数的声明不会占用汇编地址
SECTION mbr align=16 vstart=0x7c00
;设置堆栈段和栈指针
mov ax,0
mov ss,ax
mov sp,ax
; 0x10000 -> (0x0001):0x0000
mov ax,[cs:phy_base] ;计算用于加载磁盘内的用户程序的内存逻辑段地址
mov dx,[cs:phy_base+0x02]
mov bx,16
div bx ; 物理地址转逻辑段地址(除16)
mov ds,ax ;令DS和ES指向该段以进行操作
mov es,ax
;以下读取程序的起始部分
xor di,di ; 100存入di:si中时di必为0
mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号
xor bx,bx ;加载到DS:0x0000处
call read_hard_disk_0 ; 过程调用
...
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;输入:DI:SI=起始逻辑扇区号(磁盘)
; DS:BX=目标缓冲区地址(内存)
push ax ; 保护现场
push bx
push cx
push dx
; 构建端口到磁盘扇区(设备)的映射
mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数
inc dx ;0x1f3
mov ax,si ; 为将si分割成两个8位而传入ax寄存器中,0110_0100
out dx,al ;LBA地址7~0,0100
inc dx ;0x1f4
mov al,ah
out dx,al ;LBA地址15~8,0110
inc dx ;0x1f5
mov ax,di ; 0000_0000
out dx,al ;LBA地址23~16
inc dx ;0x1f6
mov al,0xe0 ;LBA28模式,主盘
or al,ah ;LBA地址27~24
out dx,al
inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al
; 等待硬盘状态
.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输
mov cx,256 ;总共要读取的字数
mov dx,0x1f0
; 连续取出数据
.readw:
in ax,dx
mov [bx],ax
add bx,2
loop .readw
pop dx ; 恢复现场
pop cx
pop bx
pop ax
ret ; 返回
...
phy_base dd 0x10000 ;用户程序被加载的物理起始地址
times 510-($-$$) db 0
db 0x55,0xaa
过程调用和返回的原理:
- 将程序指针寄存器IP内容(指向CALL指令的下一条指令)压栈,CS不用压栈
- CALL后标号赋给IP
- RET指令将原IP出栈
加载整个用户程序到内存
asm
call read_hard_disk_0 ; 过程调用,读取了一个扇区的数据
; ds此时指向存放扇区数据(用户程序)的段
mov dx, [2]
mov ax, [0] ; 用户程序头:program_length dd program_end 两个字
mov bx, 512 ; 每扇区512字节
div bx ; 商为用户程序所占扇区数,余数为不满一个扇区的数据字节数
cmp dx, 0
jnz @1 ; 未除尽,表示实际占用的扇区数多一个
dec ax ; 所以跳过本次ax自减步骤
@1:
cmp ax, 0
jz direct ; 若读取扇区数为0则跳转
push ds ; 保存ds值(用户程序头部段地址)
mov cx, ax ; 控制循环次数为剩余扇区数
@2:
mov ax, ds
add ax, 0x20 ; 这里段地址+0x20,实际物理地址+0x200,得到下一个以512字节为边界的段地址(即下一个扇区段)
mov ds, ax
xor bx, bx ; 偏移地址置零
inc si ; 逻辑扇区号(磁盘)加一,即选择下一扇区
call read_hard_disk_0 ; 再读硬盘
loop @2
pop ds
重定位用户程序:根据用户程序头部信息确定其所在段逻辑地址
asm
;计算入口点代码段基址
direct:
mov dx,[0x08] ; 用户程序:code_entry dd section.code.start [8,9]
mov ax,[0x06] ; [6,7]
call calc_segment_base
mov [0x06],ax ;回填修正后的入口点代码段基址
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位汇编地址
;返回:AX=16位段基地址,逻辑段地址,(0x0001):0x0000
push dx
; 通过段的物理地址和头部信息记录的程序入口的汇编地址相加,得出程序入口的物理地址
; [cs:phy_base+2]_[cs:phy_base] = 0x0001_0000
; dx_ax = 0x0000_0000
add ax,[cs:phy_base] ; 相加,若进位则标志寄存器CF=1
adc dx,[cs:phy_base+0x02] ; 带进位相加,dx = dx + [cs:phy_base+0x02] + CF
shr ax,4 ; 左移 ****_****_****_0000 -> 0000_****_****_****
ror dx,4 ; 循环左移 0000_0000_0000_**** -> ****_0000_0000_0000
and dx,0xf000 ; 防止地址出错,发生20位地址相加溢位
or ax,dx ; 合并ax, dx
pop dx
ret
重定位段重定位表项:
asm
call calc_segment_base
mov [0x06],ax ;回填修正后的入口点代码段基址
;开始处理段重定位表
mov cx,[0x0a] ;需要重定位的项目数量, realloc_tbl_len dw (segtbl_end-segtbl_begin)/4
mov bx,0x0c ;重定位表首地址
realloc:
mov dx,[bx+0x02] ;32位地址的高16位,code_segment dd section.code.start
mov ax,[bx]
call calc_segment_base ; 重定位该表项段地址
mov [bx],ax ;回填段的基址
add bx,4 ;下一个重定位项(每项占4个字节)
loop realloc
jmp far [0x04] ;16位间接绝对远转移,jmp far m,[0x04]处放着code_entry dw start,此时ds指向用户程序段,执行后转移到用户程序start标号处执行程序
用户程序执行
asm
start:
;初始执行时,DS和ES指向用户程序头部段
mov ax,[stack_segment] ;设置到用户程序自己的堆栈
mov ss,ax
mov sp,stack_end
mov ax,[data_1_segment] ;设置到用户程序自己的数据段
mov ds,ax
mov ax, 0xb800
mov es, ax
mov si,msg0
mov di, 0
next:
mov al, [si]
cmp al, 0
je exit
mov byte [es:di], al
mov byte [es:di+1], 0x02
inc si
add di, 2
jmp next
exit:
jmp $
...
SECTION stack align=16 vstart=0
resb 256 ; 告知编译器跳过256字节不编译,resw,resd
stack_end:
用db
,保留空间,并且指定空间的内容。 用 resb
,只保留空间,内容由编译器给你指定
回车的光标处理:
回车字符0x0d
将光标置于本行行首,换行字符0x0a
将光标置于下一行行首
无符号乘法指令:
- 操作数8位(乘数)-积16位:AL * 操作数 =AX
- 操作数16位(乘数)-积32位:DX_AX * 操作数 = DX_AX
- 操作数32位(乘数)-积64位:EDX_EAX * 操作数 = ·EDX_EAX(8086不支持,80386开始支持)
- 操作数64位(乘数)-积128位:RDX_RAX * 操作数 = RDX_RAX(8086和32位处理器不支持,6位处理器支持)
asm
mov bx,msg0
call put_string ;显示第一段信息
put_string: ;显示串(0结尾)。
;输入:DS:BX=串地址
mov cl,[bx] ; 取出一个字节
or cl,cl ;cl=0 ?
jz .exit ;是则返回主程序
call put_char
inc bx ;下一个字符
jmp put_string
.exit:
ret
;-------------------------------------------------------------------------------
put_char: ;显示一个字符
;输入:cl=字符ascii
push ax
push bx
push cx
push dx
push ds
push es
;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al ;访问3d4索引端口,指定0e寄存器
mov dx,0x3d5
in al,dx ;访问3d5数据端口,获取光标高8位
mov ah,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx ;低8位
mov bx,ax ;BX=代表光标位置的16位数
; 16位数表示一个屏的位置的标号,80*25,编号0~1999
cmp cl,0x0d ;是否是回车符?
jnz .put_0a ;不是。看看是不是换行等字符
mov ax,bx ; 准备被除数(16位光标位置数)
mov bl,80
div bl ; 除80,商为光标行号
mul bl ; 光标行号乘80得到本行行首光标编号
mov bx,ax ; 保存结果到bx
jmp .set_cursor
换行及其他字符的光标处理:
asm
.put_0a:
cmp cl,0x0a ;换行符?
jnz .put_other ;不是,那就正常显示字符
add bx,80 ; 下一行
jmp .roll_screen
.put_other: ;正常显示字符
mov ax,0xb800
mov es,ax
shl bx,1 ; 因为显示一个字符需要两个字节,所以位置编号乘2就是对应位置的显存逻辑偏移地址
mov [es:bx],cl
;以下将光标位置推进一个字符
shr bx,1 ; 恢复到位置编号
add bx,1
滚屏和清屏:
asm
.roll_screen:
cmp bx,2000
jl .set_cursor ;光标未超出屏幕则跳转
push bx
mov ax,0xb800
mov ds,ax
mov es,ax
cld ; 清除标志寄存器,低位到高位传输
mov si,0xa0 ; 源区域偏移地址
mov di,0x00 ; 目标区域偏移地址
mov cx,1920 ; 传1920次
rep movsw
;清除屏幕最底一行
mov bx,3840 ; 最后一行行首在显存的逻辑偏移地址为3840(24行*80列*2字节)
mov cx,80 ; 填一行80个
.cls:
mov word [es:bx],0x0720 ; 黑底白字07,空格编码20
add bx,2 ; 下一个字
loop .cls
pop bx
sub bx, 80 ; 在进入滚屏过程中普通字符加1到了2000,换行加了80,超过了屏幕,减80恢复到最后一行
设置光标位置:
asm
; bx为目标光标位置编号
.set_cursor:
mov dx,0x3d4 ;索引端口
mov al,0x0e
out dx,al ; 指定0e寄存器
mov dx,0x3d5
mov al,bh ; 输入光标位置的高8位
out dx,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl ; 输入光标位置的低8位
out dx,al
pop es
pop ds
pop dx
pop cx
pop bx
pop ax
ret
近过程调用将IP压栈,用RET返回,远过程调用将CS和IP压栈,用RETF返回call far
asm
push word [es:code_2_segment] ; 压入段地址-CS
mov ax,begin
push ax ;压入偏移地址-IP,可以直接push begin,但是80386才开始支持
retf ;转移到代码段2执行
虽然call
和call far
依赖ret
和retf
,但是ret
和retf
并不依赖call
和call far
,只要再栈中压入正确的地址,就可以直接返回到对应区域而不用call
和call far