为什么无法从 .data 节加载我的 HELLO_WORLD 字符串?

Tha*_*red 6 assembly nasm bootloader x86-16

我正在制作引导加载程序,作为学习汇编的一种方式。我已经研究过使用部分来组织和优化我的代码,但是当我调用 printf 函数时,一件事不起作用。当我在 .data 部分中有 HELLO_WORLD 字符串时,它根本不想加载该字符串

; Set Code to run at 0x7c00
org 0x7c00
; Put into real mode
bits 16 

; Variables without values
section .bss

; Our constant values
section .data
    HELLO_WORLD: db 'Hello World!', 0

; Where our code runs
section .text
    _start:
        mov si, HELLO_WORLD ; Moves address for string into si register
        call printf ; Calls printf function
        jmp $ ; Jump forever
        
    printf:
        lodsb ; Load the next character
        cmp al, 0 ; Compares al to 0
        je _printf_done ; If they are equal...
        call print_char ; Call Print Char
        jmp printf ; Jump to the loop
    _printf_done:
        ret ; Return
    
    print_char:
        mov ah, 0x0e ; tty mode
        int 0x10 ; Video interrupt
        ret ; Return

; Fills the rest of the data with 0
times 510-($-$$) db 0
; BIOS boot magic number
dw 0xaa55   
Run Code Online (Sandbox Code Playgroud)

结果:

Booting into hard drive...

Run Code Online (Sandbox Code Playgroud)

但是,如果我将字符串移到外面并将其放在 printf 的底部,它似乎可以工作。

; Set Code to run at 0x7c00
org 0x7c00
; Put into real mode
bits 16 

; Variables without values
section .bss

; Our constant values
section .data

; Where our code runs
section .text
    _start:
        mov si, HELLO_WORLD ; Moves address for string into si register
        call printf ; Calls printf function
        jmp $ ; Jump forever
        
    printf:
        lodsb ;  Loads next character
        cmp al, 0 ; Compares al to 0
        je _printf_done ; If they are equal...
        call print_char ; Call Print Char
        jmp printf ; Jump to the loop
    _printf_done:
        ret ; Return
    
    print_char:
        mov ah, 0x0e ; tty mode
        int 0x10 ; Video interrupt
        ret ; Return

    HELLO_WORLD: db 'Hello World!', 0

; Fills the rest of the data with 0
times 510-($-$$) db 0
; BIOS boot magic number
dw 0xaa55   
Run Code Online (Sandbox Code Playgroud)

结果:

Booting into hard drive...
Hello World!
Run Code Online (Sandbox Code Playgroud)

这是为什么?

Pet*_*des 8

$ - $$计算.text部分内的位置,因此您将填充.text到 510 字节 + 2 字节签名。因此该.data部分在引导签名之后结束,而不是引导扇区的一部分。

我通过查看文件大小注意到了这一点:525 字节。使用十六进制转储查看内容去了哪里:

$ nasm -fbin bad.asm
$ hd bad               # equivalent to hexdump -C
00000000  be 00 7e e8 02 00 eb fe  ac 3c 00 74 05 e8 03 00  |..~......<.t....|
00000010  eb f6 c3 b4 0e cd 10 c3  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200  48 65 6c 6c 6f 20 57 6f  72 6c 64 21 00           |Hello World!.|
Run Code Online (Sandbox Code Playgroud)

我们看到Hello World!ASCII 字节从文件中的偏移量 512开始,因此它不是固件在传统 BIOS 模式下启动时加载的第一个 512 字节扇区的一部分。

平面二进制文件没有节或 ELF 或 PE 程序段,并且将加载具有读+写+执行权限的所有内容(CPU 处于实模式,因此没有分页或段权限)。最简单的想法可能是创建一个平面二进制文件,以及将内容放置在前 512 个字节中,而不是可执行文件的 .data 和 .text 部分。

可以将 放在section .bss 后面dw 0xaa55,因为紧邻 MBR 加载位置(线性地址 0x7C00)之后的空间往往可以自由使用。将其放在源代码中的启动签名之后,使您的源代码与 NASM 布局平面二进制文件的方式相匹配。请注意,它不会像主流操作系统下的 .bss 空间那样对您进行零初始化。


如果您确实想使用section指令并在代码之后但在启动签名之前有一些.rodata指令.data,那么您需要执行$-$$.

就像可能在每个部分的开始/结束处放置标签一样,这样您就可以执行totalsize equ (text_end-text_start) + (data_end-data_start)/ times (510-totalsize) db 0/ dw 0xaa55。但是您必须在 NASM 最后放置的任何部分中执行此操作,否则您会将某些部分推出超过 512 字节边界。幸运的是,文件大小可以轻松检查这一点。

您可以控制 NASM 在平面二进制文件中排列各部分的顺序。这是 NASM 的一个特例;它充当链接器和汇编器,填充符号偏移量而不仅仅是创建重定位条目。第一次出现新部分时,请使用指令上的start=x和属性。(感谢 @ecm 指出这一点。)但默认情况下已经先排序,这正是您所需要的,因为执行从 MBR 的第一个字节开始。follows=ysection.text


起初,我假设 NASM 会按照首次出现的顺序将各节输出到平面二进制文件中,在这种情况下,问题将是执行db 'Hello World!', 0as machine code

事实证明,这不是 NASM 所做的;它将该.text部分放在平面二进制文件的第一位,即使section .data它位于源代码的第一位。


顺便说一句,您的引导加载程序依赖于一些无法保证的东西,并且在某些 BIOS 上会失败。

(通常建议使用 Bochs 对引导加载程序进行单步调试。特别是当您执行任何有关分段或切换到保护模式的操作时;连接到 Qemu 的 GDB 并不像 Bochs 那样了解分段。)

  • NASM 的多节二进制输出格式可以使用属性“start=x”和“follows=y”对其 progbits 节进行任意排序,这些属性需要在定义新节的第一个“section”指令中使用。 (2认同)