【问题标题】:Position independent code in 16-bit real mode, bootloading/floppy read16 位实模式下的位置无关代码,引导加载/软盘读取
【发布时间】:2016-08-08 21:53:52
【问题描述】:

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

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

“引导加载程序”:

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

“第一个程序”:

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),但是在创建有用的操作时,这些黑客似乎都不可持续系统。如何避免这种地址硬编码?

【问题讨论】:

  • 发布汇编问题时请标记处理器架构。
  • 我想基地址是需要用引导加载程序指定的。我在这方面的大部分工作都是在嵌入式系统上进行的,我知道 BIOS 为您做了很多事情。对我来说,当我使用引导加载程序时,它通常有文档告诉我预期的入口点在哪里。
  • 因此,如果有人正在组装一个程序,该程序将被加载到软盘上并由引导加载程序读取,他们会被告知在他们的源代码中添加类似ORG 512(或其他一些明确的地址)的东西?并且引导加载程序总是将程序加载到内存的同一部分?很难想象现代操作系统是如何由此演变而来的,但我想我应该去阅读更多有关历史的信息。
  • 再说一次,我的经验是使用嵌入式系统,所以如果我从 NXP 获得一个引导加载程序来安装 Kinetis 部件,文档会告诉我引导加载程序将从 0x0000 放置到 0x3fff,并且我需要链接我的程序以使其入口点的地址位于 0x4000。分段的技巧是在您的情况下有意义的方式,但这是大多数嵌入式系统中没有的功能。

标签: assembly nasm x86-16 bootloader position-independent-code


【解决方案1】:

您可以通过使用段来避免对地址进行硬编码。将“第一个程序”加载到 16 的倍数的地址,并使用相应的段(地址 / 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 中传递引导设备的驱动器号,因此您无需更改它。

【讨论】:

  • 另一个常见的技巧是将您的引导加载程序代码复制到其他地方(您正在复制以便知道在哪里)并在原始引导扇区上加载第二个扇区,再次为您提供一个已知位置。很多方法。
  • @DavidHoelzer 这个问题似乎是关于如何在非硬编码的“已知”地址加载程序。除了如何选择加载段之外,这个示例本质上是 MS-DOS 处理加载 .COM 文件的方式。
【解决方案2】:

此示例允许您输入任意数量的代码 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 填充扇区可能是个好主意,以防代码跑掉,不会出现段错误。

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

【讨论】:

  • int 12h 实际上返回的是低内存的 KiB 数量,而不是 4 KiB 页面的数量。但是,从 KiB (2 ** 10) 到段落 (2 ** 4) 向左移动 6 是正确的。 (但shl ax, 6 不是 8086-clean。)
猜你喜欢
  • 2011-10-06
  • 2012-08-13
  • 2015-09-17
  • 1970-01-01
  • 2023-03-23
  • 2018-10-17
  • 1970-01-01
  • 2014-12-19
  • 2015-12-22
相关资源
最近更新 更多