通过 USB 驱动器启动的自定义引导加载程序在某些计算机上会产生不正确的输出
我是组装的新手,但我正在尝试深入低水平计算的世界。我正在尝试学习如何编写将以引导加载程序代码运行的汇编代码;因此与Linux或Windows(例如Linux或Windows)无关。阅读此页面和其他一些x86指令集的列表,我提出了一些应该在屏幕上打印10 A的汇编代码,然后是1B。
BITS 16
start:
mov ax, 07C0h ; Set up 4K stack space after this bootloader
add ax, 288 ; (4096 + 512) / 16 bytes per paragraph
mov ss, ax
mov sp, 4096
mov ax, 07C0h ; Set data segment to where we're loaded
mov ds, ax
mov cl, 10 ; Use this register as our loop counter
mov ah, 0Eh ; This register holds our BIOS instruction
.repeat:
mov al, 41h ; Put ASCII 'A' into this register
int 10h ; Execute our BIOS print instruction
cmp cl, 0 ; Find out if we've reached the end of our loop
dec cl ; Decrement our loop counter
jnz .repeat ; Jump back to the beginning of our loop
jmp .done ; Finish the program when our loop is done
.done:
mov al, 42h ; Put ASCII 'B' into this register
int 10h ; Execute BIOS print instruction
ret
times 510-($-$$) db 0 ; Pad remainder of boot sector with 0s
dw 0xAA55
因此,输出应该看起来像这样:
AAAAAAAAAAB
我使用在Windows 10 Ubuntu Bash程序上运行的NASM汇编程序组装了代码。生成.bin文件后,我使用十六进制编辑器将其打开。我使用相同的HEX编辑器将该.bin文件的内容复制到闪存驱动器的前512个字节中。将程序写入Flash Drive后,我将其断开并将其插入具有Intel Core i3-7100的计算机中。在启动时,我选择了USB Flash Drive作为引导设备,只是为了获得以下输出:
A
更改程序中的各种内容后,我终于感到沮丧,并在另一台计算机上尝试了程序。另一台计算机是带有i5-2520m的笔记本电脑。我遵循与以前提到的相同过程。果然,它给了我预期的输出:
AAAAAAAAAAB
我立即使用i3在原始计算机上尝试了它,但它仍然没有用。
所以我的问题是:为什么我的程序可以与一个X86处理器一起使用而不是另一个处理器?他们都支持X86指令集。什么给?
解决方案:
好的,我能够在一些帮助下追踪真正的解决方案。如果您在下面阅读Michael Petch的答案,您会找到一种解决我的问题的解决方案,以及BIOS寻找BPB的另一个问题。
这是我的代码的问题:我正在将程序写入闪存驱动器的第一个字节。这些字节被加载到内存中,但是一些BIOS中断为自己使用这些字节。因此,我的程序被BIOS覆盖。为了防止这种情况,您可以添加BPB说明,如下所示。如果您的BIOS以我的方式工作,它将仅在内存中覆盖BPB,而不是您的程序。另外,您可以将以下代码添加到程序的顶部:
jmp start
resb 0x50
start:
;enter code here
此代码(由Ross Ridge提供)将您的程序将您的程序推向内存位置0x50(从0x7C00偏移),以防止执行过程中BIOS覆盖它。
还要记住,每当您致电任何子例程时,您使用的寄存器的值都可能被覆盖。确保您要么使用push
,pop
,要么在调用子例程之前将值保存到内存中。看看下面的马丁·罗塞瑙(Martin Rosenau)的答案,以了解有关此的更多信息。
感谢所有回答我的问题的人。现在,我对这种低级东西的工作原理有了更好的了解。
I am fairly new to assembly, but I'm trying to dive into the world of low level computing. I'm trying to learn how to write assembly code that would run as bootloader code; so independent of any other OS like Linux or Windows. After reading this page and a few other lists of x86 instruction sets, I came up with some assembly code that is supposed to print 10 A's on the screen and then 1 B.
BITS 16
start:
mov ax, 07C0h ; Set up 4K stack space after this bootloader
add ax, 288 ; (4096 + 512) / 16 bytes per paragraph
mov ss, ax
mov sp, 4096
mov ax, 07C0h ; Set data segment to where we're loaded
mov ds, ax
mov cl, 10 ; Use this register as our loop counter
mov ah, 0Eh ; This register holds our BIOS instruction
.repeat:
mov al, 41h ; Put ASCII 'A' into this register
int 10h ; Execute our BIOS print instruction
cmp cl, 0 ; Find out if we've reached the end of our loop
dec cl ; Decrement our loop counter
jnz .repeat ; Jump back to the beginning of our loop
jmp .done ; Finish the program when our loop is done
.done:
mov al, 42h ; Put ASCII 'B' into this register
int 10h ; Execute BIOS print instruction
ret
times 510-($-$) db 0 ; Pad remainder of boot sector with 0s
dw 0xAA55
So the output should look like this:
AAAAAAAAAAB
I assembled the code using the nasm assembler running on the Windows 10 Ubuntu Bash program. After it produced the .bin file, I opened it using a hex editor. I used the same hex editor to copy the contents of that .bin file into the first 512 bytes of a flash drive. Once I had written my program to the flash drive, I disconnected it and plugged it into a computer with an Intel Core i3-7100. On bootup, I selected my USB flash drive as the boot device, only to get the following output:
A
After changing various things in the program, I finally got frustrated and tried the program on another computer. The other computer was a laptop with an i5-2520m. I followed the same process as I mentioned before. Sure enough, it gave me the expected output:
AAAAAAAAAAB
I immediately tried it on my original computer with the i3, but it still didn't work.
So my question is: Why does my program work with one x86 processor but not the other? They both support the x86 instruction set. What gives?
Solution:
Ok, I've been able to track down the real solution with some help. If you read Michael Petch's answer below, you'll find a solution that will fix my problem, and another problem of a BIOS looking for a BPB.
Here was the problem with my code: I was writing the program to the first bytes of my flash drive. Those bytes were loaded into memory, but some BIOS interrupts were using those bytes for itself. So my program was being overwritten by the BIOS. To prevent this, you can add a BPB description as shown below. If your BIOS works the same way mine does, it will simply overwrite the BPB in memory, but not your program. Alternatively, you can add the following code to the top of your program:
jmp start
resb 0x50
start:
;enter code here
This code (courtesy of Ross Ridge) will push your program to memory location 0x50 (offset from 0x7c00) to prevent it from being overwritten by the BIOS during execution.
Also keep in mind that whenever you call any subroutine, the values of the registers you were using could be overwritten. Make sure you either use push
, pop
or save your values to memory before calling a subroutine. Look at Martin Rosenau's answer below to read more about that.
Thank you to all who replied to my question. I now have a better understanding of how this low-level stuff works.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
这可能会成为该主题的规范答案。
真实硬件/USB/笔记本电脑问题
如果您尝试使用 USB 在真实硬件上启动,那么即使您在 BOCHS 和 QEMU 中正常工作,您也可能会遇到另一个问题。如果您的 BIOS 设置为进行 USB FDD 模拟(而不是 USB HDD 或其他),您可能需要添加 BIOS 参数块 (BPB) 到引导加载程序的开头。您可以像这样创建一个假的:
将
ORG
指令调整为您需要的内容,或者如果您只需要默认的 0x0000,则忽略它。如果您要修改代码以具有 Unix/Linux
file
命令上方的布局,则可能能够转储出它认为构成磁盘映像中的 VBR 的 BPB 数据。运行命令file disk.img
,您可能会得到以下输出:如何修改此问题中的代码
在此 OP 的情况下 原始代码可以修改为如下所示:
其他建议
正如已经指出的 - 您不能
ret
结束引导加载程序。您可以将其置于无限循环中,或者使用cli
后跟hlt
停止处理器。如果您在堆栈上分配了大量数据或开始写入引导加载程序 512 字节之外的数据,您应该将自己的堆栈指针 (SS:SP) 设置为赢得的内存区域不要干扰你自己的代码。这个问题中的原始代码确实设置了一个堆栈指针。这是阅读本问答的其他人的一般观察。我在我的 Stackoverflow 答案中有更多信息,其中包含 常规引导加载程序提示。
测试代码以查看 BIOS 是否覆盖 BPB
如果您想知道 BIOS 是否可能覆盖 BPB 中的数据并确定它写入的值,您可以使用此引导加载程序代码转储 BPB,因为引导加载程序在控制后会看到它被转移到它。正常情况下前3个字节应该是
EB 3C 90
,后面跟着一系列AA
。任何不是AA
的值都可能被 BIOS 覆盖。此代码位于 NASM 中,可以使用nasm -f bin boot.asm -o boot.bin
组装到引导加载程序中,对于任何未执行此操作的 BIOS,输出应如下所示在将控制权转移到引导加载程序代码之前更新 BPB:
This could probably be made into a canonical answer on this subject.
Real Hardware / USB / Laptop Issues
If you are attempting to use USB to boot on real hardware then you may encounter another issue even if you get it working in BOCHS and QEMU. If your BIOS is set to do USB FDD emulation (and not USB HDD or something else) you may need to add a BIOS Parameter Block(BPB) to the beginning of your bootloader. You can create a fake one like this:
Adjust the
ORG
directive to what you need or omit it if you just need the default 0x0000.If you were to modify your code to have the layout above the Unix/Linux
file
command may be able to dump out the BPB data that it thinks makes up your VBR in the disk image. Run the commandfile disk.img
and you may get this output:How the Code in this Question Could be Modified
In the case of this OPs original code it could have been modified to look like this:
Other Suggestions
As has been pointed out - you can't
ret
to end a bootloader. You can put it into an infinite loop or halt the processor withcli
followed byhlt
.If you ever allocate a large amount of data on the stack or start writing to data outside the 512 bytes of your bootloader you should set your own stack pointer (SS:SP) to a region of memory that won't interfere with your own code. The original code in this question does setup a stack pointer. This is a general observation for anyone else reading this Q/A. I have more information on that in my Stackoverflow answer that contains General Bootloader Tips.
Test Code to See if Your BIOS is Overwriting the BPB
If you want to know if the BIOS might be overwriting data in the BPB and to determine what values it wrote you could use this bootloader code to dump the BPB as the bootloader sees it after control is transferred to it. Under normal circumstances the first 3 bytes should be
EB 3C 90
followed by a series ofAA
. Any value that isn'tAA
was likely overwritten by the BIOS. This code is in NASM and can be assembled into a bootloader withnasm -f bin boot.asm -o boot.bin
Output should look like this for any BIOS that didn't update the BPB before transferring control to the bootloader code:
它不是处理器,而是 BIOS:
int
指令实际上是call 的一种特殊变体代码>指令。该指令调用一些子例程(通常用汇编程序编写)。
(您甚至可以用您自己的子例程替换该子例程 - 例如,这实际上是由 MS-DOS 完成的。)
在两台计算机上,您有两个不同的 BIOS 版本(甚至供应商),这意味着由
int 10h
指令是由不同的程序员编写的,因此其功能并不完全相同。我怀疑这里的问题是第一台计算机上的 int 10h 调用的子例程不保存寄存器值,而第二台计算机上的例程则保存寄存器值。
换句话说:
在第一台计算机上,由
int 10h
调用的例程可能如下所示:...因此在
int 10h
调用ah
之后> 寄存器不再包含值0Eh
,甚至可能出现cl
寄存器被修改的情况(此时将陷入无限循环)。为了避免这个问题,您可以使用
push
保存cl
寄存器(您必须保存整个cx
寄存器)并在之后恢复它>int
指令。您还必须在每次调用int 10h
子例程之前设置ah
寄存器的值,因为您无法确定此后它没有被修改:请思考 Peter Cordes 的评论:
ret
指令如何工作以及它是如何相关的到sp
和ss
寄存器?这里的
ret
指令肯定不会做你期望的事情!在软盘上,引导扇区通常包含以下代码:
int 19h
完全按照ret
指令执行您所期望的操作。然而,BIOS 将再次启动计算机,这意味着它将从 USB 记忆棒加载代码并再次执行。
您将得到以下结果:
因此插入
int 16h
指令。当ax寄存器的值为0时,这将等待用户按下键盘上的按键,然后再调用int 16h子例程。或者,您可以简单地添加一个无限循环:
当这两条指令之间发生中断时:
...
sp
和ss
寄存器的组合并不代表值的“有效”表示。如果您不幸运,中断会将数据写入您不需要的内存中的某个位置。它甚至可能会覆盖您的程序!
因此,您通常在修改
ss
寄存器时锁定中断:It is not the processors but the BIOSes:
The
int
instruction actually is a special variant of thecall
instruction. The instruction calls some sub-routine (typically written in assembler).(You can even replace that sub-routine by your own one - which is actually done by MS-DOS, for example.)
On two computers you have two different BIOS versions (or even vendors) which means that the sub-routine called by the
int 10h
instruction has been written by different programmers and therefore does not exactly do the same.The problem I suspect here is that the sub-routine called by
int 10h
on the first computer does not save the register values while the routine on the second computer does.In other words:
On the first computer the routine called by
int 10h
may look like this:... so after the
int 10h
call theah
register does no longer contain the value0Eh
and it may even be the case that thecl
register is modified (which will end in an endless loop then).To avoid the problem you could save the
cl
register usingpush
(you have to save the entirecx
register) and restore it after theint
instruction. You also have to set the value of theah
register before each call of theint 10h
sub-routine because you cannot be sure that it has not modified since then:Please think about Peter Cordes' comment:
How does the
ret
instruction work and how is it related to thesp
andss
registers?The
ret
instruction here will definitely not do what you expect!On floppy disks the boot sectors typically contain the following code instead:
int 19h
does exactly what you expect from theret
instruction.However the BIOS will boot the computer again which means that it will load the code from your USB stick and execute it again.
You'll get the following result:
Therefore the
int 16h
instruction is inserted. This will wait for the user to press a key on the keyboard when theax
register has the value 0 before calling theint 16h
sub-routine.Alternatively you can simply add an endless loop:
When an interrupt occurs between these two instructions:
... the combination of the
sp
andss
registers does not represent a "valid" representation of values.If you are unlucky the interrupt will write data somewhere to memory where you don't want it. It may even overwrite your program!
Therefore you typically lock interrupts when modifying the
ss
register: