编写过程的好处是只用编写一次,以后只需要调用即可。
8-1的第24~27行用于读取app_lba_start扇区的内容,即用户程序在硬盘上的起始逻辑扇区号。
因为不知道用户程序到底多大,占了多少个扇区,所以可以先读第一个扇区。
8-1,79~131行是编写的过程。
过程的第一条指令需要一个标号,以方便引用过程。read_hard_disk_0
每次读硬盘时的起始逻辑扇区号和数据保存位置都不相同,就涉及参数传递。
通过寄存器,主程序起始逻辑扇区号的高16位存放在DI
中(高4位为0),低16位存放在SI
中。并约定读出来的数据存放到DS
指向的数据段中,起始偏移地址在BX
在过程的开头,将本过程要用到的寄存器临时压栈,并返回到调用点以前出栈恢复。
后面的内容在上一节基本涉及
mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数
inc dx ;0x1f3
mov ax,si
out dx,al ;LBA地址7~0
inc dx ;0x1f4
mov al,ah
out dx,al ;LBA地址15~8
inc dx ;0x1f5
mov ax,di
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
然后调用过程前各个寄存器的内容从栈中恢复。
每次往内存中加载一个扇区前,都重新在前面的数据尾部构造一个新的逻辑段,并把要读取的数据加载到这个新段内。
;以下判断整个程序有多大
mov dx,[2] ;起始的大小高16位和低16位分别存
mov ax,[0]
mov bx,512 ;512字节每扇区
div bx
cmp dx,0
jnz @1 ;未除尽,因此结果比实际扇区数少1
dec ax ;已经读了一个扇区,扇区总数减1
@1:
cmp ax,0 ;考虑实际长度小于等于512个字节的情况
jz direct
;读取剩余的扇区
push 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 ;循环读,直到读完整个功能程序
用于加载用户程序的物理地址phy_base是16字节对齐的,而用户程序中,每个段的汇编地址也是16字节对齐的。因此,每个段在内存中的起始地址也是16字节对齐的,将它们分别右移4位,就是它们各自的逻辑段地址。
55行,从栈中恢复数据段寄存器DS的内容,使其指向用户程序被加载的起始位置,也就是用户程序头部。
第58~62行用于重定位用户程序入口点的代码段。用户程序头部内,偏移为0x06处的双字,存放的是入口点代码段的汇编地址。加载器首先将高字和低字分别传送到DX
和AX
,然后调用过程calc_segment_base
来计算该代码段在内存中的段地址、
direct:
mov dx,[0x08]
mov ax,[0x06]
call calc_segment_base
mov [0x06],ax ;回填修正后的入口点代码段基址
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位物理地址
;返回:AX=16位段基地址
push dx
add ax,[cs:phy_base];将用户程序在内存物理其实地址的低16位加到AX
adc dx,[cs:phy_base+0x02];高16位加到DX中,adc是带进位加法,将目的操作数和源操作数相加,然后再加上CF位的值。分两步完成32位数的加法运算。
shr ax,4
ror dx,4
and dx,0xf000
or ax,dx
pop dx
ret
在DX:AX
中得到了入口点代码段的起始物理地址,只需要将32位数右移4位即可得到逻辑段地址。
需要分别移动两个寄存器然后拼接。使用shr逻辑右移指令。(shl是逻辑左移指令)
注意,源操作数为1的逻辑右移指令是特殊设计的优化指令。
代码使用ror循环右移(ROtate Right),循环右移指令执行时,每右移一次,移出的比特既送到标志寄存器的CF位,也送进左边空出的位。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2EIJSXK1-1678975958324)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20230316174812359.png)]
ror循环左移,与shl,shr的指令格式是相同的。
DX
循环右移四位后,DX
的低12位是我们不需要的用and将其清零。
or ax,dx。将其内容合并。
现在回到62行,将计算出来的逻辑段地址写到原处覆盖低16位,高16位不用理会。
处理了入口点代码段的重定位,还要处理在用户程序的所有段,它们位于用户程序头部的段重定位表中。
重定位表的表项数存放在用户程序头部偏移0x0a处,65行,将它传到CX
,供循环指令使用。
段重定位表的首地址存放在用户用于将它从该内存地址处存放在用户程序头部偏移0x0c处,因此,第66行,将0x0c传送到BX
中。以后每次只要将BX
的内容加上4,就指向下一个重定位表项。
68~74行是循环体,每次循环开始后,BX总是指向需要重定位的段的汇编地址,而且都是双字,需要分别传送到寄存器DX
和AX
。然后调用过程calc_segment_base
计算相应的逻辑段地址,并覆盖到原来的位置(低字),最后将基址寄存器的内容化加4,以指向下一个表项。当CX
为0吗,循环结束,所有段处理完毕。
此时,用户程序在内存中就绪,76行,加载器通过一个16位间接绝对远转移指令,跳转到用户程序入口点。
jmp far [0x04]
访问DS
所指向的数据段,从偏移地址为0x04的地方取出两个字,并分别传送到CS
和IP
于是,处理器自行转移到指定位置开始执行。