我正在制作一个引导程序,作为学习汇编的一种方式。我已经研究过使用节来组织和优化我的代码,但有一件事不起作用,那就是当我调用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
结果:
Booting into hard drive...
然而,如果我把字符串移到它之外,并把它放在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
结果:
Booting into hard drive...
Hello World!
为什么?
$ - $$
计算.text
部分中的位置,因此将.text
填充为510字节+2字节签名。因此,.data
部分结束于引导签名之后,而不是引导扇区的一部分。
我通过查看文件大小注意到了这一点:525字节。使用hexdump查看发生了什么:
$ 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!.|
我们看到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的一种特殊情况;它既是一个链接器,也是一个汇编程序,填充符号偏移量,而不仅仅是重新定位条目。section
指令第一次出现在新节中时,请使用该指令上的属性start=x
和follows=y
。(感谢@cem指出这一点。)但默认情况已经是先订购.text
,这正是您所需要的,因为执行从MBR的第一个字节开始。
起初,我假设NASM会按照第一次出现的顺序将部分输出到平面二进制文件中,在这种情况下,问题将是将db 'Hello World!', 0
作为机器代码执行。
事实证明,NASM不是这么做的;即使section .data
在源中是第一个,它也会将.text
部分放在平面二进制中的第一个。
BTW,您的引导程序依赖于一些无法保证的东西,并且在某些BIOS上会失败
在使用
lodsb
从DS:SI
加载之前,您不会初始化DS或ES以匹配org
设置。在循环之前不要
cld
,以确保lodsb
将递增SI而不是递减。与32位和64位模式下的标准调用约定/ABI不同,您不能假设在进入引导加载程序时DF=0。事实上,假设尽可能少,只假设它加载在线性地址7C00h,并且DL=您可以用来从同一设备加载更多扇区的驱动器号。有关本期和上一期的内容,请参阅Michael Petch的Bootloader开发一般提示。
在调用
int 10h
/AH=0Eh
之前,您没有将BH/BL设置为页码/颜色(https://en.wikipedia.org/wiki/INT_10H/http://www.ctyme.com/intr/rb-0106.htm)。参见- 如何使用INT 10H显示寄存器值
- 使用DOS或BIOS显示字符
- 调用int 10h时bl的值
您不会为BPB留下空间,BIOS会从字节#3开始在扇区的一些字节上乱写。(参见上面Michael Petch的一般提示。)这在QEMU和Bochs上很好,但如果从USB启动,在一些真正的硬件上会失败。
(Bochs通常被推荐用于引导加载程序的单步调试。特别是当你做任何分割或切换到保护模式时;连接到Qemu的GDB不像Bochs那样了解分割。)