记录一段主引导扇区程序,它会跳转到逻辑扇区号100的扇区执行用户程序:
;文件名:fifteen.asm
;文件说明:硬盘主引导扇区代码(加载程序)
;创建日期:2022-11-29 20:54
app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号必须是100开始)
;常数的声明不会占用汇编地址
SECTION mbr align=16 vstart=0x7c00 ; 段内所有元素的汇编地址将从0x7c00开始
;设置堆栈段和栈指针
mov ax,0
mov ss,ax ;栈段和栈指针都指向的0,也就是0000:0000
mov sp,ax
mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址,phy_base为四个字节,0x10000;地址为0x7c00+phy_base
mov dx,[cs:phy_base+0x02] ;DX AX: 0001 0000;小端字节序存放4字节
mov bx,16 ;除数为16位,所以被除数在DX和AX当中,也就是10000,右移4位的目的是得到段地址1000
div bx
mov ds,ax ;计算完后DX为余数,AX为商,令DS和ES指向该段以进行操作,段地址为1000
mov es,ax
;以下读取程序的起始部分 ,用于读取用户程序的第一个扇区,该扇区包括了用户程序的头部,而头部包括了用户程序的大小
xor di,di
mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号
xor bx,bx ;加载到DS:0x0000处
call read_hard_disk_0
;以下判断整个程序有多大
mov dx,[2] ;把高16位放到dx,此处是在使用刚加载进来的用户程序的头部的前四个字节,也就是用户程序的总长度
mov ax,[0] ;把低16位放到ax
mov bx,512 ;512字节每扇区
div bx
cmp dx,0 ;计算完后DX为余数,AX为商;如果余数不为零,则扇区数为ax中的数+1,但此时已经读掉了一个头部扇区,因此还剩ax中的数的个数
jnz @1 ;如果dx不为零,则ax中的个数正好为剩余的扇区数,直接转移,不执行下一条指令
dec ax ;如果dx为零,说明没余数,执行到这,已经读了一个扇区,扇区总数需减1
@1:
cmp ax,0 ;考虑用户程序实际长度小于等于512个字节的情况
jz direct
;读取剩余的扇区
push ds ;以下要用到并改变DS寄存器 ,将ds压栈
mov cx,ax ;循环次数(剩余扇区数)
@2:
mov ax,ds
add ax,0x20 ;得到下一个以512字节为边界的段地址,以一个扇区作为一个段
mov ds,ax
xor bx,bx ;每次读时,偏移地址始终为0x0000
inc si ;下一个逻辑扇区
call read_hard_disk_0
loop @2 ;循环读,直到读完整个功能程序
pop ds ;恢复数据段基址到用户程序头部段
;计算入口点代码段基址
direct:
mov dx,[0x08] ;用户程序入口点所在代码段的汇编地址
mov ax,[0x06]
call calc_segment_base
mov [0x06],ax ;回填修正后的入口点代码段基址
;开始处理段重定位表,计算用户程序中所有段的逻辑段地址
mov cx,[0x0a] ;需要重定位的项目数量,循环次数
mov bx,0x0c ;重定位表首地址
realloc:
mov dx,[bx+0x02] ;32位地址的高16位
mov ax,[bx]
call calc_segment_base
mov [bx],ax ;回填段的基址
add bx,4 ;下一个重定位项(每项占4个字节)
loop realloc
jmp far [0x04] ;转移到用户程序;取出16位的偏移地址和16位的逻辑段地址(已经从32位汇编地址变成了16位的逻辑地址),修改cs:ip
;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区,用DI:SI来传递28位的逻辑扇区号
;输入:DI:SI=起始逻辑扇区号 0000:0100
; DS:BX=目标缓冲区地址 1000:0000,硬盘读出的数据将来要存到内存的地址
push ax
push bx
push cx
push dx
mov dx,0x1f2 ;将端口号写入到dx寄存器,0x1f2:8位端口,设置读取的扇区数量
mov al,1 ;扇区数量
out dx,al ;读取的扇区数,就读一个扇区
;扇区的读写是连续的,只需要写出第一个扇区的编号;扇区号为28位
inc dx ;0x1f3 8位端口,扇区号低0~7位设置端口(LBA28模式)
mov ax,si
out dx,al ;LBA地址7~0
inc dx ;0x1f4 8位端口,扇区号低8~15位设置端口(LBA28模式)
mov al,ah
out dx,al ;LBA地址15~8
inc dx ;0x1f5 8位端口,扇区号低16~23位设置端口(LBA28模式)
mov ax,di
out dx,al ;LBA地址23~16
inc dx ;0x1f6 低四位设置扇区号低24~27位设置端口(LBA28模式),高四位,第7、5位固定为1;第6位 置0表示CHS模式,置1表示LBA模式;
;第4位 置0表示主硬盘,置1表示从硬盘
mov al,0xe0 ;LBA28模式,主盘
or al,ah ;LBA地址27~24
out dx,al
inc dx ;0x1f7 8位端口,命令/状态端口;向这个端口传输0x20就表示 请求硬盘读,随后硬盘会每时每刻将它的状态信息传输到这个端口,
;一旦硬盘准备好了就可以开始传输数据;第7位 置0表示硬盘不忙,置1表示硬盘忙;第3位 置0表示未准备好与主机交换数据,
;置1 表示准备好与硬盘交换数据;第0位表示前一条命令执行错误,具体原因查看0x1f1端口
mov al,0x20 ;读命令
out dx,al ;请求硬盘读
.waits: ;用于等待,判断硬盘是否忙和数据是否准备好
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输
mov cx,256 ;总共要读取的字数
mov dx,0x1f0 ;0x1f0:16位端口,数据传输端口
.readw: ;硬盘准备好后,开始读取数据到内存1000:0000,读取一个扇区
in ax,dx ;把读到的数据放到ax寄存器中
mov [bx],ax ;加载到DS:BX处,也就是加载到0x1000:0x0000处,第一次循环时bx为0,也就是把读到的数据存入到[bx]
add bx,2 ;每次都是一个字,所以移动两个字节
loop .readw ;重复256次,正好一个扇区
pop dx
pop cx
pop bx
pop ax
ret
;-------------------------------------------------------------------------------
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位汇编地址,低20位有效,且最后四位为零,因为是段地址
;返回:AX=16位段基地址 ,逻辑段地址
push dx ;dx不用做返回,不应该破坏dx中的数据
add ax,[cs:phy_base] ;段的汇编地址与程序的起始物理地址相加。可能会进位,改变cf标志位
adc dx,[cs:phy_base+0x02] ;adc会判断标志寄存器的cf位
shr ax,4 ;逻辑右移
ror dx,4 ;循环右移
and dx,0xf000
or ax,dx ;组合两个寄存器中的值,最终ax中就是逻辑段地址
pop dx
ret
;-------------------------------------------------------------------------------
phy_base dd 0x10000 ;开辟一个32位空间存放,用户程序被加载的物理起始地址
times 510-($-$$) db 0 ;主引导扇区为512字节,使用0进行填充
db 0x55,0xaa ;主引导扇区要以0x55 0xaa结尾