引导加载程序不会跳转到内核代码
Boot loader doesn't jump to kernel code
我正在编写小型操作系统 - 用于练习。我从引导加载程序开始。
我想创建在 16 位实模式下运行的小型命令系统(目前)。
我创建了重置驱动器的引导加载程序,然后在引导加载程序之后加载扇区。
问题是因为在 jmp
函数之后什么都没有发生。
我正在尝试加载 0x7E00 处的下一个扇区(我不完全确定如何使用 es:bx 指向地址,所以这可能是个问题,我相信它是 Address:offset),就在引导加载程序之后。
这是代码:
;
; 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
我一直在使用 VirtualBox 测试这段代码,但实际上什么也没发生,读取错误没有显示,也没有显示应该打印的消息。
此代码的主要问题是:
- ES:BX 指向错误的 segment:offset 将内核加载到
- 加载了错误的扇区,因此内核不是预期的
第一个在这段代码中:
mov bx,0x7E00
mov es,bx
xor bx,bx
题目想从磁盘加载扇区到0x0000:0x7E00
(ES:BX)。此代码将 ES:BX 设置为 0x7E00:0x0000
,解析为物理地址 0x7E000
((0x7E00<<4)+0x0000)。我认为其目的是将 0x07E0
加载到 ES 中,这将产生 0x7E00
((0x07E0<<4)+0x0000) 的物理地址。您可以了解有关 16:16 内存寻址计算的更多信息 here。将该段乘以 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
引导加载程序开发的一般提示
其他可能导致运行宁代码在各种模拟器、虚拟机和真实物理硬件上应该解决的问题是:
- 当BIOS跳转到你的代码时你不能依赖CS,DS,ES,SS,SP 寄存器具有有效值或预期值。当你的引导加载程序启动时,它们应该被适当地设置。您只能保证您的引导加载程序将从物理地址 0x00007c00 加载并 运行,并且引导驱动器号被加载到 DL 寄存器中。
- 将SS:SP设置为您知道不会与您自己的代码运行冲突的内存。 BIOS 可能已将其默认堆栈指针放置在可用和可寻址 RAM 的第一个兆字节中的任何位置。不能保证它在哪里以及它是否适合您编写的代码。
lodsb
、movsb
等使用的方向标志可以设置或清除。如果方向标志设置不当,SI/DI 寄存器可能会调整到错误的方向。使用 STD
/CLD
将其设置为您希望的方向 (CLD=forward/STD=backwards)。在这种情况下,代码假定向前移动,因此应该使用 CLD
。有关更多信息,请参阅 instruction set reference
- 当跳转到一个内核时,FAR JMP 通常是一个好主意,以便它正确设置 CS:IP 到预期值。这可以避免内核代码可能在同一段内执行 absolute near JMPs 和 CALLs 的问题。
- 如果将引导加载程序定位为适用于 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 位寄存器。
- FS 和 GS 段寄存器被添加到 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 处理器中存在一个错误,该错误未被接受,因此如果您的目标是尽可能广泛的环境,明确禁用并重新启用它们是一个安全的选择。如果您不打算在有问题的 8088 上工作,那么可以在上面的代码中删除 CLI
/STI
指令。我在 80 年代中期在家用 PC 上做的工作中直接了解到这个错误。
第二个要注意的是我是如何设置堆栈的。对于 8088/8086 16 位汇编的新手,可以通过多种方式设置堆栈。在这种情况下,我将堆栈的顶部(内存中的最低部分)设置为 0x8000
(SS)。然后我将堆栈指针 (SP) 设置为 0
。当您在 16 位实模式下 push something on the stack 时,处理器首先将堆栈指针递减 2,然后在该位置放置一个 16 位 WORD。因此,第一次压入堆栈的位置是 0x0000-2 = 0xFFFE (-2)。然后你会有一个 SS:SP 看起来像 0x8000:0xFFFE
。在这种情况下,堆栈 运行s 从 0x8000:0x0000
到 0x8000:0xFFFF
.
在 8086(不适用于 80286、80386+ 处理器)上处理堆栈 运行ning 时,最好设置堆栈指针 (SP) 为偶数。在原始的 8086 上,如果将 SP 设置为奇数,则每次访问堆栈 space 都会产生 4 clock cycle penalty。由于 8088 有一个 8 位数据总线,这种损失不存在,但是在 8086 上加载一个 16 位 word 需要 4 个时钟周期,而在 8088 上需要 8 个时钟周期(两个8 位内存读取)。
最后,如果您想明确设置 CS:IP,以便 CS 在 JMP 完成(对您的内核)然后建议执行 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 [=16=]x07E0,[=16=]x0000
我正在编写小型操作系统 - 用于练习。我从引导加载程序开始。
我想创建在 16 位实模式下运行的小型命令系统(目前)。
我创建了重置驱动器的引导加载程序,然后在引导加载程序之后加载扇区。
问题是因为在 jmp
函数之后什么都没有发生。
我正在尝试加载 0x7E00 处的下一个扇区(我不完全确定如何使用 es:bx 指向地址,所以这可能是个问题,我相信它是 Address:offset),就在引导加载程序之后。
这是代码:
;
; 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
我一直在使用 VirtualBox 测试这段代码,但实际上什么也没发生,读取错误没有显示,也没有显示应该打印的消息。
此代码的主要问题是:
- ES:BX 指向错误的 segment:offset 将内核加载到
- 加载了错误的扇区,因此内核不是预期的
第一个在这段代码中:
mov bx,0x7E00
mov es,bx
xor bx,bx
题目想从磁盘加载扇区到0x0000:0x7E00
(ES:BX)。此代码将 ES:BX 设置为 0x7E00:0x0000
,解析为物理地址 0x7E000
((0x7E00<<4)+0x0000)。我认为其目的是将 0x07E0
加载到 ES 中,这将产生 0x7E00
((0x07E0<<4)+0x0000) 的物理地址。您可以了解有关 16:16 内存寻址计算的更多信息 here。将该段乘以 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
引导加载程序开发的一般提示
其他可能导致运行宁代码在各种模拟器、虚拟机和真实物理硬件上应该解决的问题是:
- 当BIOS跳转到你的代码时你不能依赖CS,DS,ES,SS,SP 寄存器具有有效值或预期值。当你的引导加载程序启动时,它们应该被适当地设置。您只能保证您的引导加载程序将从物理地址 0x00007c00 加载并 运行,并且引导驱动器号被加载到 DL 寄存器中。
- 将SS:SP设置为您知道不会与您自己的代码运行冲突的内存。 BIOS 可能已将其默认堆栈指针放置在可用和可寻址 RAM 的第一个兆字节中的任何位置。不能保证它在哪里以及它是否适合您编写的代码。
lodsb
、movsb
等使用的方向标志可以设置或清除。如果方向标志设置不当,SI/DI 寄存器可能会调整到错误的方向。使用STD
/CLD
将其设置为您希望的方向 (CLD=forward/STD=backwards)。在这种情况下,代码假定向前移动,因此应该使用CLD
。有关更多信息,请参阅 instruction set reference- 当跳转到一个内核时,FAR JMP 通常是一个好主意,以便它正确设置 CS:IP 到预期值。这可以避免内核代码可能在同一段内执行 absolute near JMPs 和 CALLs 的问题。
- 如果将引导加载程序定位为适用于 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 位寄存器。
- FS 和 GS 段寄存器被添加到 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 处理器中存在一个错误,该错误未被接受,因此如果您的目标是尽可能广泛的环境,明确禁用并重新启用它们是一个安全的选择。如果您不打算在有问题的 8088 上工作,那么可以在上面的代码中删除 CLI
/STI
指令。我在 80 年代中期在家用 PC 上做的工作中直接了解到这个错误。
第二个要注意的是我是如何设置堆栈的。对于 8088/8086 16 位汇编的新手,可以通过多种方式设置堆栈。在这种情况下,我将堆栈的顶部(内存中的最低部分)设置为 0x8000
(SS)。然后我将堆栈指针 (SP) 设置为 0
。当您在 16 位实模式下 push something on the stack 时,处理器首先将堆栈指针递减 2,然后在该位置放置一个 16 位 WORD。因此,第一次压入堆栈的位置是 0x0000-2 = 0xFFFE (-2)。然后你会有一个 SS:SP 看起来像 0x8000:0xFFFE
。在这种情况下,堆栈 运行s 从 0x8000:0x0000
到 0x8000:0xFFFF
.
在 8086(不适用于 80286、80386+ 处理器)上处理堆栈 运行ning 时,最好设置堆栈指针 (SP) 为偶数。在原始的 8086 上,如果将 SP 设置为奇数,则每次访问堆栈 space 都会产生 4 clock cycle penalty。由于 8088 有一个 8 位数据总线,这种损失不存在,但是在 8086 上加载一个 16 位 word 需要 4 个时钟周期,而在 8088 上需要 8 个时钟周期(两个8 位内存读取)。
最后,如果您想明确设置 CS:IP,以便 CS 在 JMP 完成(对您的内核)然后建议执行 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 [=16=]x07E0,[=16=]x0000