一个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_starthead_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,这里通过inout指令操作端口在磁盘驱动器之间传递指令和数据。

重定位段地址

前面说了,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

示例

Screenshot_20201215_165811.png