16 位实模式下的位置无关代码,bootloading/floppy 读取

Position independent code in 16-bit real mode, bootloading/floppy read

以下源文件被单独组装(成原始二进制文件)并分别加载到虚拟软盘的扇区 1 和扇区 2。然后这张软盘用作 qemu-system-i386 VM 的引导介质。

"bootloader"从软盘的第2扇区读取"first program",然后跳转到包含刚才读取的代码的内存。以下代码按预期工作(即打印 "first program" 欢迎消息),但我必须在 "first program" 的源代码中指定 ORG 0x001E (通过检查引导加载程序的十六进制代码获得编辑)。 0x001E 是 temp 缓冲区的偏移量,它保存从软盘读取的代码。

"bootloader":

BITS 16

bootloader_main:
    mov bx, 0x07C0      ; Set data segment to bootloader's default segment
    mov ds, bx


    mov ah, 0x02        ; BIOS int13h "read sector" function
    mov al, 1           ; Number of sectors to read
    mov cl, 2           ; Sector to read
    mov ch, 0           ; Cylinder/track
    mov dh, 0           ; Head
    mov dl, 0           ; Disk number (here, the floppy disk)
    mov bx, 0x07C0      ; Segment containing the destination buffer
    mov es, bx
    mov bx, temp        ; Destination buffer offset
    int 0x13

    jmp temp

    ret
;end bootloader_main




temp: times 60 db 17

times 510-($-$$) db 0       ; Pad rest of sector and add bootloader   
dw 0xAA55                      signature

"first program":

BITS 16
ORG 0x001E       ; Assume that this code will be located 0x001E bytes     
                   after start of bootloader (in RAM)

mov bx, string      ; Print a welcome string
mov ah, 0x0E
print_loop:
    mov al, byte [bx]
    int 0x10
    inc bx
    cmp byte [bx], 0
    jne print_loop
;end print_loop


string: db "This is the first program.", 0

或者,我可以使用 ORG 0x2000x200 作为缓冲区而不是 temp (即在引导加载程序之后将程序加载到 RAM 中),但是这些 hack 似乎都不是在创建有用的操作系统方面具有可持续性。如何避免这种地址硬编码?

您可以通过使用段来避免地址的硬编码。在 16 的倍数的地址处加载 "first program" 并加载具有相应段(地址 / 16)的 DS,然后远跳转到 segment:0,其中 segment 是您加载程序的位置。在加载的程序中使用ORG 0

例如:

BITS 16

bootloader_main:
    mov ax, 0x07C0      ; Set data segment to bootloader's default segment
    mov ds, ax

    mov ah, 0x02        ; BIOS int13h "read sector" function
    mov al, 1           ; Number of sectors to read
    mov cl, 2           ; Sector to read
    mov ch, 0           ; Cylinder/track
    mov dh, 0           ; Head
    mov bx, program_seg ; load program at program_seg:0
    mov es, bx
    xor bx, bx
    int 0x13

    mov ax, program_seg
    mov ds, ax
    mov ss, ax          ; set stack to end of program_seg
    mov sp, 0
    jmp program_seg:0

bootloader_end:
program_seg equ (bootloader_end - bootloader_main + 0x7c00 + 15) / 16

times 510-($-$$) db 0       ; Pad rest of sector and add bootloader   
dw 0xAA55                   ;   signature
BITS 16
ORG 0

mov bx, string      ; Print a welcome string
mov ah, 0x0E
print_loop:
    mov al, byte [bx]
    int 0x10
    inc bx
    cmp byte [bx], 0
    jne print_loop
;end print_loop


string: db "This is the first program.", 0

我删除了 mov dl, 0 指令,因为您不应该对该值进行硬编码。 BIOS会在DL中传递启动设备的驱动器号,无需更改。

此示例允许您在之间输入任意数量的代码 jmp Load_Buffer & Load_Buffer,只要它不超过运行 引导扇区的最后 4 个字节。

引导程序

    BOOTSEG     equ 0x7c0
    LOW_MEM     equ 18
    DISKIO      equ 19

确定有多少 4k 页面可用并将其转换为一个段 地址。在我的例子中是 0x8FC0。

xor     cx, cx
mov     cl, 64
int     LOW_MEM             ; Get # of 4k segments to top of memory
sub     ax, cx
shl     ax, 6                       ; 

修改堆栈指针时始终禁用中断。这将堆栈放在内存顶部的一个非常安全的地方,少了 64k

cli
mov     ss, ax
xor     sp, sp
sti

此时 CS 可能为 0 或 0x7c0,具体取决于 bios 供应商,但这并不重要,因为此时只有 ES:BX 很重要

mov     ax, Load_Buffer
shr     ax, 4
add     ax, BOOTSEG
mov     es, ax  
mov     ax, 0x201                   ; Read one sector
mov     cx, 2                       ; Starting @ cylinder 0, sector 2
xor     dh, dh                  ; Head 0 and BIOS has already passed device.

这里的另一种选择是用 0x7c0 加载 ES,用 Load_Buffer 加载 BX,但在我们的例子中,ES = BOOTSEG + Load_Buffer / 16.

xor     bx, bx
int     DISKIO                  ; Read sector 2
jmp     Load_Buffer

页面边界对齐将保证无论在 Load_Buffer 之上添加了多少,加载程序都将始终工作,除非它恰好超过 运行 引导扇区的最后 4 个字节。对齐只需要在页面边界上,所以 16 也可以。

align   32
Load_Buffer:

用 NOP 的 incase 代码填充扇区可能是个好主意 运行s 之后,不会出现段错误。

times   508 - ($-$$)    db  144     ; NOP's 0x90
int     25                      ; Incase codes runs away we'll reboot
dw      0xAA55                  ; Boot sig (no required by some emulators)

第一个节目

打算使用 LODSB,并不是说比你的方法更正确,只是使用的代码更少。

mov     si, string
push    es
pop     ds                      ; DS:SI now points to string
mov     ah, 14                  ; TTY output

Loop:
lodsb                           ; Read from DS:SI
test    al, 255             ; Test if these any of these bits are on.
jz      .Done
int     16
jmp     Loop

标签之前的句号只是NASM声明本地标签的功能

.Done:
hlt
jmp     $

string  db  'This is the first program', 0