到第三篇了,对应<x86>第8章的内容。这次分两个代码:一个是加载器,复位后首先得到执行的程序,从磁盘读取用户程序,并为用户程序的执行初始化环境,最后跳转到用户程序;另一个是用户程序,无所事事的运行着,仅仅是证明加载器没出错。

    两个程序最重要部分是用户程序的程序头,程序头就像是一份协议,用户程序告诉加载器关于整个程序的属性,加载器获得这些属性后,在指定内存位置加载。

    首先,加载器在指定扇区读到用户程序头,头部最开始部分是长度,告诉加载器要读多少扇区哪些内容是有效的(uboot还在头部加上魔术字,确定存储的用户程序是否有效(其他一些可执行程序的头部也是魔术字,这不简单起见,去了魔术字的验证)。

    其次,是程序入口点和代码段地址,以及后面的重定位表。除了程序入口点是相对与代码段开始处的偏移,不需要重定位,其他段都需要重定位。为什么需要重定位?因为程序被加载的位置不确定。假设,程序编写时,从0x0000开始,数据段在整个程序开始位置,此时,ds段被硬编码指向0x0000(这种说法可能不确切,其实形如 mov ax,0x0000 mov ds,ax),以后的数据访问都是从0x0000开始,如mov ax,[0];当程序被加载到0x10000段时,此时数据段其实已经被加载到0x10000,如果ds的段值没有进行相应的修改,执行mov ax,[0]时,将获得错误的数据。因此,对所有段经行重定位很重要,当然,也可以写出与位置无关的代码,不过过程挺麻烦的。重定位的过程大致是:1)加载器在内存中寻找一片空闲的地址,计算出这片地址的基址(想法是美好的,但是现实不是这样,加载器从LoadPhyBase指定的位置加载);2)找到空闲内存后,加载器遍历并修改用户程序段表(段表存放了编译时,用户程序各个段的基址,当加载器加载后,需要将各个段的基址+LoadPhyBase,重新得到基址);3)当用户程序得到执行后,不要急着运行自己的代码逻辑,而应该先获取用户头中各个段的重定位信息,并修改相应的段寄存器,最后再开始执行。

    最后,所有的准备工作完成后,需要远跳转跳转到用户程序中执行。为嘛是远跳转?加载器所在的段这里是0x0000,而用户程序所在的段难以保证也在0x0000,intel 手册上说,对于不同段之间的跳转,需要借助远跳转。跳转的目标地址在程序头中指定,处理器将低16字节加载到ip,高16字节加载到cs。因此程序头的结构被设计为

CodeEntry     dw start
CodeSeg     dd section.CodeSeg.start

以满足这个要求。(这个是调试出来的,开始时,我写成段地址在低位,入口偏移在高位,跳转到未知世界)

贴代码

加载器部分:

DiskDataReg   equ 0x01f0
DiskErrReg equ 0x01f1
DiskSectCntReg equ 0x01f2
DiskLoLBAAddr equ 0x01f3
DiskMeLBAAddr equ 0x01f4
DiskHiLBAAddr equ 0x01f5
DiskModReg equ 0x01f6
DiskCmdStatReg equ 0x01f7

DiskReadCmd equ 0x20
DiskWriteCmd equ 0x30

Arg1Off equ 0x06 ;第一个参数相对bp偏移
Arg2Off equ 0x08 ;第二个参数相对bp偏移
Arg3Off equ 0x0A ;第三个参数相对bp偏移
Arg4Off equ 0x0C ;第四个参数相对bp偏移

StackBase equ 0x0000
StackEnd equ 0x2000
DataBase equ 0x0300
DataEnd equ 0x1FFF

AppEntryOff equ 0x04
AppCodeSegOff equ 0x06
AppSegNumsOff equ 0x0A
AppRelocTab equ 0x0C
;==========================
ProgLocSect equ 0x04

section bootloader align=16 vstart=0x7c00
jmp start

Relocation:
;前面取出没重定位前保存在用户程序中的地址,加上重定位的基址,得到最终的地址
;实模式下,地址20位。ax取低16位,dx取高16位,其中低4位有效
;20位右移4位,得到段地址。ax先右移4位,空出高位4位;
;dx低4位左移12位,即15-12位为段地址高位,再与ax的低12位相或,得到16为段地址
add ax,word [cs:LoadPhyBase]
adc dx,word [cs:LoadPhyBase+2]
shr ax,0x04
shl dx,0x0c
or ax,dx
ret

SetSectAddr:
;设置逻辑扇区的地址
push bp
mov bp,sp
push bx
;发扇区总数
mov dx,DiskSectCntReg
mov al,byte [bp+Arg4Off]
out dx,al
;发扇区地址
mov dx,DiskLoLBAAddr
mov al,byte [bp+Arg3Off]
out dx,al

mov dx,DiskMeLBAAddr
mov al,byte [bp+Arg3Off+1]
out dx,al

mov dx,DiskHiLBAAddr
mov al,byte [bp+Arg2Off]
out dx,al

mov dx,DiskModReg
mov al,byte [bp+Arg2Off+1]
out dx,al

pop bx
mov sp,bp
pop bp

ret

WaitDiskReady:
;等待磁盘就绪
mov dx,DiskCmdStatReg
.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits

;判断是否有错误
mov dx,DiskErrReg
.getErr:
in al,dx
cmp al,0x00
;al不为0出错
jnz .resume
;没有出错 返回0
xor ax,ax
ret

.resume:
;出错 返回
mov ax,0x01
ret

ReadFromDisk:

call SetSectAddr

;发读命令
mov dx,DiskCmdStatReg
mov al,DiskReadCmd
out dx,al

call WaitDiskReady

xor bx,bx ;准备拷贝,bx做索引
;读取保存在扇区上的数据的有效长度
mov dx,DiskDataReg
in ax,dx
mov [bx],ax ;程序的长度也要保存起来,要不然用户头部都不完整了
add bx,2

shr ax,0x01
;比较要读取数据的长度和有效数据的长度,取最短的
;磁盘上保存的数据,长度按字节计数,但是DiskDataReg
;端口是16位端口,每次读取1字,因此循环次数需要除2
mov cx,ax
mov dx,DiskDataReg
.readw:
in ax,dx
mov [bx],ax
add bx,2
loop .readw

ret

start:
xor ax,ax
xor dx,dx
;计算用于加载程序的内存段地址
mov ax,word [cs:LoadPhyBase]
mov dx,word[cs:LoadPhyBase+2]
mov bx,0x10
div bx
;dx:ax/bx->商存放在ax中
;商即为段地址
;为后面用户程序设置段寄存器(es/ds),指向LoadPhyBase所指的内存起址
;如果没有设置ds,用户程序运行时,程序不知道到何处去取出段头信息,程序大小
mov ds,ax
mov es,ax
push word 0x0001
push word ProgLocSect
push word 0xe000
push word 0x0200
;从4号扇区先读一个扇区,扇区里前部是程序头,感觉跟pe文件头有点像
call ReadFromDisk
;恢复堆栈
add sp,0x08
;从0x1000:0000开始读
xor bx,bx
mov ax,word [bx]
mov dx,word [bx+0x02]
;从用户程序的总长度决定还有多少扇区要读取
mov bx,0x200
div bx
;ax:程序占了完整的几个扇区,dx:还多余几个字节
;多余的字节占用一个扇区,故,共占用ax+1个扇区
;前面已经读取了一个扇区,剩下ax个扇区还要读取

cmp ax,0x0000
jz ReadSectComplete
;循环读取次数
mov cx,ax
xor di,di
mov di,ProgLocSect
;段地址每次增加0x200,存储扇区内容
mov ax,ds
RemainSector:
add ax,0x20
mov ds,ax
;继续读剩下的扇区
push word 0x0001
push word ProgLocSect
push word 0xe000
push word 0x0200
call ReadFromDisk
add sp,0x08
loop RemainSector
;现在程序全在从LoadPhyBase开始的内存中,由ds指向
ReadSectComplete:
;用户程序重定位
RelocationCodeSeg:
mov ax,word [AppCodeSegOff]
mov dx,word [AppCodeSegOff+2]
call Relocation
;修正用户头中代码段基址,先清空以前的内容
mov word [AppCodeSegOff],0x0000
mov word [AppCodeSegOff+2],0x0000
mov word [AppCodeSegOff],ax
RelocationTabElem:
;修正用户头中重定位表中各项基址,先清空以前的内容
mov cx,word [AppSegNumsOff]
;重定位表偏移
xor bx,bx
mov bx,AppRelocTab
RelocRound:
mov ax,word [bx] ;表项低位
mov dx,word [bx+0x02] ;表项高位
call Relocation
mov word [bx],0x0000
mov word [bx+0x02],0x0000
mov word [bx],ax
add bx,0x04
loop RelocRound
;重定位结束,跳转到用户程序
;代码在[AppEntryOff]开始的位置
;开始时没加far,结果是近跳转,过不去
;远跳转,从内存中取出双字,修改cs:ip的值
jmp far [AppEntryOff]
LoadPhyBase dd 0x10000
times 510-($-$$) db 0
db 0x55,0xaa


用户程序部分:

section ProgHead align=16 vstart=0
ProgSize dd ProgEnd
CodeEntry dw start
CodeSeg dd section.CodeSeg.start
ProgSegNums dw (ProgSegEntryTabEnd-ProgSegEntryTab)/4
ProgSegEntryTab:
CodeSegEntry dd section.CodeSeg.start
DataSegEntry dd section.DataSeg.start
StackSegEntry dd section.StackSeg.start
ProgSegEntryTabEnd:

section CodeSeg align=16 vstart=0x0000
nop
nop
nop
;执行到这,要设置数据段堆栈段等信息,
;加载器在加载时,设置ds/es指向LoadPhyBase
;既然这样,加载器跳转过来后ds:[n]能正确访问
;自己定义的用户头
start:
;现在section.CodeSeg.start,section.DataSeg.start中的内容是段基地址
;开始时,我先加载了ds,再用[ds:StackSegEntry],出错了
;因为ds改变后不能在用
;mov ax,[StackSegEntry]
;mov ss,ax
;语句加载ss了 因为ds指向其他段,不再是LoadPhyBase,因此应该在ds改变前先加载ss段

mov ax,[StackSegEntry]
mov ss,ax
mov sp,stack_end
mov ax,[DataSegEntry]
mov ds,ax
lab1:
xor ax,ax
inc ax
inc ax
jmp lab1

section DataSeg align=16 vstart=0x0000
times 64 db 0xcc

section StackSeg align=16 vstart=0x0000
resb 256
;汇编中的标号都表示偏移位置,偏移堆栈基址256,堆栈向下生长,即堆栈保留了256字节。
;好吧,我承认stack_end这段我完全抄书的
stack_end:

section tail align=16
ProgEnd:

注,加载器目前只加载用户代码大小小于512B的程序,超过这个范围的bin文件还未调试。

另外,本文中提到了与位置无关代码和与位置相关代码的概念,我应该会另起一篇文章解释他们