引导加载程序不会跳转到内核代码

我正在写小操作系统 – 用于练习。 我开始使用bootloader。
我想创build一个运行在16位真实模式(现在)的小型命令系统。
我已经创build了bootloader来重置驱动器,然后在bootloader之后加载扇区。
问题是因为jmp函数没有实际发生。

我试图加载下一个扇区在0x7E00(我不完全确定如何使用es:bx指向地址,这可能是一个问题,我相信它的地址:偏移量),就在bootloader之后。

这是代码:

 ; ; SECTOR 0x0 ; ;dl is number of harddrive where is bootloader org 0x7C00 bits 16 ;reset hard drive xor ah,ah int 0x13 ;read sectors clc mov bx,0x7E00 mov es,bx xor bx,bx mov ah,0x02 ;function mov al,0x1 ;sectors to read mov ch,0x0 ;tracks mov cl,0x1 ;sector mov dh,0x0 ;head int 0x13 ;if not readed jmp to error jc error ;jump to 0x7E00 - executed only if loaded jmp 0x7E00 error: mov si,MSGError .loop: lodsb or al,al jz .end mov ah,0x0E int 0x10 jmp .loop .end: hlt MSGError db "Error while booting", 0x0 times 0x1FE - ($ - $$) db 0x0 db 0x55 db 0xAA ; ; SECTOR 0x1 ; jmp printtest ;definitions MSGLoaded db "Execution successful", 0x0 ; ; Print function ; si - message to pring (NEED TO BE FINISHED WITH 0x0) printtest: mov si,MSGLoaded .loop: lodsb or al,al jz .end mov ah,0x0E int 0x10 jmp .loop .end: hlt times 0x400 - ($-$$) db 0x0 

我一直在使用VirtualBoxtesting这个代码,但实际上没有发生,读取错误不显示,以及应该打印的消息。

这个代码的主要问题是:

  1. ES:BX指向错误的片段:offset来加载内核
  2. 错误的部门正在加载,所以内核不是预期的

第一个是这个代码:

 mov bx,0x7E00 mov es,bx xor bx,bx 

该问题想要从扇区加载到0x0000:0x7E00ES:BX )。 此代码将ES:BX设置0x7E00:0x0000 ,这将parsing为物理地址0x7E000 ((0x7E00 << 4)+ 0x0000)。 我认为其目的是将0x07E0加载到ES中 ,这会产生0x7E00 ((0x07E0 << 4)+ 0x0000)的物理地址。 你可以在这里了解更多关于16:16内存寻址的计算。 将该段乘以16与将其左移4位相同。

代码中的第二个问题在这里:

 mov ah,0x02 ;function mov al,0x1 ;sectors to read mov ch,0x0 ;tracks mov cl,0x2 ;sector number mov dh,0x0 ;head int 0x13 

磁盘上第二个512块扇区的编号是2,而不是1.因此,要修复上述代码,您需要相应地设置CL

 mov cl,0x2 ;sector number 

Bootloader开发的一般技巧

其他可能会影响各种仿真器,虚拟机和实际物理硬件运行代码的问题包括:

  1. 当BIOS跳转到您的代码时,您不能依赖具有有效或期望值的CSDSESSSSP寄存器。 当你的引导程序启动时,它们应该被适当的设置。 您只能保证您的引导加载程序将从物理地址0x00007c00加载并运行,并且启动驱动器号被加载到DL寄存器中。
  2. SS:SP设置为你知道的内存不会与你自己的代码的操作冲突。 BIOS可能已经将其默认堆栈指针放置在第一兆字节的可用和可寻址RAM中的任何地方。 不能保证它在哪里,它是否适合你所写的代码。
  3. lodsbmovsb等使用的方向标志可以被设置或清除。 如果方向标志设置不正确, SI / DI寄存器的调整方向可能会错误。 使用STD / CLD将其设置为您希望的方向(CLD =正向/ STD =反向)。 在这种情况下,代码假定向前移动,所以应该使用CLD 。 有关这方面的更多信息,请参见指令集参考
  4. 当跳转到内核时,通常将FAR JMP设置为一个好主意,以便将CS:IP正确设置为期望值。 这可以避免内核代码的问题,这些问题可能会在同一段内的JMPCALL 附近 进行
  5. 如果将您的引导加载程序定位到适用于8086/8088处理器(及更高版本)的16位代码,请避免在汇编代码中使用32位寄存器。 使用AX / BX / CX / DX / SI / DI / SP / BP代替EAX / EBX / ECX / EDX / ESI / EDI / ESP / EBP 。 虽然在这个问题上不是问题,但对其他人寻求帮助却是一个问题。 一个32位的处理器可以利用16位实模式下的32位寄存器,但8086/8088/80286不能,因为它们是16位处理器,而不能访问扩展的32位寄存器。
  6. FSGS段寄存器被添加到80386 + CPU中。 避免他们,如果你打算瞄准8086/8088/80286。

要解决第一个和第二个项目,可以在引导加载程序启动附近使用此代码:

 xor ax,ax ; We want a segment of 0 for DS for this question mov ds,ax ; Set AX to appropriate segment value for your situation mov es,ax ; In this case we'll default to ES=DS mov bx,0x8000 ; Stack segment can be any usable memory cli ; Disable interrupts to circumvent bug on early 8088 CPUs mov ss,bx ; This places it with the top of the stack @ 0x80000. mov sp,ax ; Set SP=0 so the bottom of stack will be @ 0x8FFFF sti ; Re-enable interrupts cld ; Set the direction flag to be positive direction 

一些事情要注意。 当你改变SS寄存器的值时(在这个例子中是通过MOV ),处理器就会关掉那条指令的中断,直到下一条指令完成。 通常情况下,如果更新SS后立即更新SP,则不需要担心禁用中断。 8088处理器很早就出现了一个bug,在这种情况下,如果你的目标是最广泛的环境,明确地禁用和重新启用它们是一个安全的select。 如果您不打算在8088上运行,那么可以在上面的代码中删除CLI / STI指令。 我在80年代中期在家用电脑上工作时,首先知道这个问题。

第二件要注意的是我如何设置堆栈。 对于新的8088/8086 16位汇编的人来说,堆栈可以设置多种方式。 在这种情况下,我将堆栈的顶部(内存中的最低部分)设置为0x8000SS )。 然后我把堆栈指针( SP )设置为0 。 当你以16位实模式在堆栈上压入某些东西时,处理器首先将堆栈指针递减2,然后在该位置放置一个16位的WORD 。 因此,第一次推入堆栈将在0x0000-2 = 0xFFFE(-2)。 你会有一个SS:SP看起来像0x8000:0xFFFE 。 在这种情况下,堆栈从0x8000:0x00000x8000:0xFFFF

当处理在8086上运行的堆栈(不适用于80286,80386+处理器)时,将堆栈指针( SP )设置为偶数是个好主意。 在原来的8086上,如果将SP设置为奇数,则每次访问堆栈空间都会导致4个时钟周期的惩罚 。 由于8088有一个8位数据总线,所以不存在这种惩罚,但是在8086上加载一个16位需要4个时钟周期,而在8088上需要8个时钟周期(两个8位存储器读)。

最后,如果要显式设置CS:IP,以便在JMP完成(到内核)时正确设置CS ,则build议执行FAR JMP ( 请参阅影响段寄存器 / FAR跳转的操作 )。 在NASM语法中, JMP将如下所示:

 jmp 0x07E0:0x0000 

有些(即MASM / MASM32)汇编程序不能直接支持对FAR Jmp进行编码,因此可以采用如下手动方式:

 db 0x0ea ; Far Jump instruction dw 0x0000 ; Offset dw 0x07E0 ; Segment 

如果使用GNU汇编器,它看起来像:

 ljmpw $0x07E0,$0x0000