一个loader程序
之前写的boot程序(2) 烂尾了,这里是后续。 这里不使用BIOS的中断服务程序,而是直接采用LBA28的方法直接操作固定大小的vhd。为此改了下 vhdtool 将bootloader写在0扇区,用户程序从1扇区开始写入。在bootloader代码中直接从1扇区加载用户程序。
这个程序可以分为几部分:
- 加载用户程序
- 转换物理地址到逻辑地址(段+偏移)
- 跳转到用户程序
加载用户程序
约定
要加载用户程序需要知道以下几点:
- 用户程序放在哪儿
- 有多大
- 入口代码在哪儿
和函数调用类似,用户程序可以看作一个函数,我们调用这个函数就需要知道它需要什么参数,用户程序和bootloader可以是完全不同的人写的,这时我们需要一些约定: 1, 用户程序的说明必须放在开头 2, 用户程序的说明必须指定用户程序的大小 3, 用户程序的说明必须指定入口代码地址
这里的“说明” 指用户程序对自己的描述,比如这个用户程序:
section .text vstart=0
dd program_end
dw main
base dd section.code.start
dw (header_end-table_start)/4
table_start dd section.code.start
header_end:
section code align=16 vstart=0
main:
mov ax, 0x7c00
mov ss, ax
mov sp, 0x7c00
mov ax, 0xb800
mov es, ax
mov ax, cs ; why cs is equal to table_start? it was because, the `jmp` instruction will set both cs and ip register
mov bx, 16
; 2^16 => 65536 need at most 5 placeholders
mov cx, 5
.loop:
xor dx, dx
div bx
push dx
loop .loop
mov cx, 5
mov di, 0
.again:
pop dx
add dl, 0x30
mov [es:di], dl
inc di
mov byte [es:di], 0x17
inc di
loop .again
sti
.run:
hlt
jmp .run
program_end:
代码中.text
段的内容就是用户程序自己的描述,program_end
即是最后一个标签,也代表了整个程序的大小,当loader读到这个双字后就知道了整个用户程序的大小,如果这个大小大于了扇区大小,则loader会接着读取。main
即是code
段的标签也是用户程序的入口,接下来是这个地址的段地址,code
中代码的偏移都是基于这个段地址的(注意vstart=0),因此,需要在loader中进行转换后在写入回到这个地址
mov [0x6], ax ; 这里的[0x6]即用户代码中的 [base], ax为转换后的地址
如果还有其它的段,都需要计算后重新写回去结果,其它段需要放在table_start
和head_end
中间,这里把它叫做段重定位表,每个表项必须是double word即dd
(define double word)。
再跳转到用户程序后,通常都需要重新设置段寄存器,例如用户程序自定义了栈段
scetion stack align=16 vstart=0
resb 256
stack_end:
假设这个段在段重定位表中是 stack_segment
那么在程序中要想使用这256字节作为栈就需要
mov ax, [stack_segment]
mov ss, ax
mov sp, stack_end
这里同时设置了sp 中断会在栈上push东西。
加载
这里直接采用LBA28方式读取裸盘,将整个用户程序读取到物理地址 0x10000 处,下面是读扇区函数
; LBA28 mode
; input:
; from `di` sector to read `si` sectors
; outout:
; read to 0x10000
; ds: 0x10000
; bx: 0 at the very beginning, user should save bx
; example(read 2 sectors):
; mov di, 1
; mov si, 1
; mov ax, 0x10000
; mov ds, ax
; mov bx, 0
; xor ax, ax
;
; call read_sector
; inc si
; call read_sector
read_sector:
push ax
push bx
push dx
push cx
; read one sector
mov dx, 0x1f2
mov ax, si
out dx, al
; low bits is enough for reading upto 65536 sectors
inc dx
mov ax, di
out dx, al
; mid bits to zero
inc dx
mov al, 0
out dx, al
; high bits to zero
inc dx
mov ax, 0
out dx, al
; rest 4bits 24~27 to zero
inc dx
mov al, 0xe0
or al, ah
out dx, al
; send read command
inc dx
mov al, 0x20
out dx, al
.loop:
; read state
in al, dx ; NOTE: 0x1f7 is also state port
and al, 0x88
cmp al, 0x08
jnz .loop ; not ready yet
; read whole block: 512 bytes
mov cx, 256
mov dx, 0x1f0 ; data port
.read_2bytes:
in ax, dx
mov [bx], ax ; read 2 bytes
add bx, 2 ; change offset
loop .read_2bytes
pop cx
pop dx
pop bx
pop ax
ret
实际上真正的方式是CHS,只不过支持LBA的磁盘驱动器会自动的将LBA转为CHS,用户不需要关系数据在哪个柱面、磁道和扇区,只需要传入要操作的扇区数以及起始扇区偏移量。
LBA28映射的端口是0x1f2到0x1f7,这里通过in
、out
指令操作端口在磁盘驱动器之间传递指令和数据。
重定位段地址
前面说了,bootloader对用户程序一无所知,所以做了一个约定,用户程序需要提供一个段重定位表,bootloader负责将重定位后的地址写回去,后面就没有bootloader的事了。
section .text align=16 vstart=0x7c00
...
...
realloc:
mov dx, [bx+0x2] ; high 16 bits
mov ax, [bx] ; low 16 bits
call physic_to_logic
mov [bx], ax ; write back to item
add bx, 4
loop realloc
physic_to_logic:
push dx
add ax, [cs:physic_base]
adc dx, [cs:physic_base+0x02]
shr ax, 4
ror dx, 4
and dx, 0xf000
or ax, dx
pop dx
ret
这里从段重定位表开始循环,由于约定中表项都是双字,而是现在是16位实模式,所以不得不将表项通过dx:ax表示,其中dx表示表项的高16字节,ax表示表项的低16字节。
这里物理地址转为逻辑地址也很简单,先将地址加高低位,再右移4位即可,也就是20位地址换算成16位表示,要注意的是前一个加法add
可能有进位,所以接着使用带进位加法adc
。
虽然dx:ax有32位,但实际只有低20位是有效的(因为cpu只有20根地址线),ax存着低16位,dx的低4位存着20位中的高4位,在ax右移后,它的高4为就空出来了,只需要将dx的低4位放在那里即可,可以这样
shl dx, 12
or ax, dx
方法有很多。
另外,这里使用了cs
作为前缀,因为另外的段寄存器有其他用途,并且这里的偏移直接使用了physic_base (0x10000)没有加上0x7c00是因为cs一开始是0x0并且section加上了vstart=0x7c00
所以这里的cs本身就是相对的(0x0000:0x7c00)所以不需要再加0x7c00。
跳转到用户程序
jmp far [ds:0x4]
用户程序入口在约定中就是[0x4]
,这里显式使用了超越前缀ds
,在执行跳转之前所以的地址都已经重定位了。
注意:必须使用远跳,即 jmp far
这是因为我们在段间跳转
完整代码 loader.asm
示例