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

时间:2020-12-01 02:52:55

I'm writing small operation system - for practice. I started with bootloader.
I want to create small command system that runs in 16 bit real mode (for now).
I've created bootloader that resets drive, then loads sector after bootloader.
The problem is because after jmp function nothing actually happening.

我正在编写一个小型操作系统——以供练习。我开始与引导装载程序。我想创建一个运行在16位真实模式(目前)的小型命令系统。我已经创建了引导加载程序来重置驱动器,然后在引导加载程序之后加载扇区。问题是在jmp函数之后实际上什么都没有发生。

I't trying to load next sector at 0x7E00 (I'm not totally sure how to point address using es:bx so that may be a problem, I believe that its Address:offset), just after bootloader.

我没有尝试在0x7E00加载下一个扇区(我不完全确定如何使用es:bx指出地址,因此这可能是一个问题,我认为它的地址是:offset),就在引导加载之后。

This is the code:

这是代码:

;
; 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

I've been testing this code using VirtualBox but nothing actually happens, The read error doesn't shows, as well as message that should be printed.

我一直在使用VirtualBox测试这段代码,但实际上什么都没有发生,读取错误没有显示,应该打印的消息也没有显示。

1 个解决方案

#1


38  

The primary problems with this code were:

本守则的主要问题是:

  1. ES:BX was pointing to the wrong segment:offset to load the kernel into
  2. ES:BX指向错误的段:偏移量,将内核加载到
  3. Wrong sector was being loaded so kernel wasn't what was expected
  4. 错误的部门正在被加载,所以内核并不是预期的。

The first one was in this code:

第一个是在这个代码中:

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

The question wants to load the sector from disk to 0x0000:0x7E00(ES:BX). This code sets the ES:BX to 0x7E00:0x0000 which resolves to a physical address of 0x7E000 ((0x7E00<<4)+0x0000). I think the intention was to load 0x07E0 into ES which would yield a physical address of 0x7E00 ((0x07E0<<4)+0x0000). You can learn more about 16:16 memory addressing calculations here. Multiplying the segment by 16 is the same as shifting it left 4 bits.

问题希望将扇区从磁盘加载到0x00x7e00 (ES:BX)。这段代码将ES:BX设置为0x7ex0000,它解析为0x7E000的物理地址(0x7E00< 4)+0x0000)。我认为目的是将0x07E0加载到ES中,从而产生0x7E00的物理地址(0x07E0< 4)+0x0000)。您可以在这里了解更多关于16:16内存寻址计算的信息。将段乘以16就等于移动了4位。

The second problem in the code is here:

代码中的第二个问题是:

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

The number for the second 512 block sector on the disk is 2, not 1. So to fix the above code you need to set CL accordingly:

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

mov cl,0x2  ;sector number

General Tips for Bootloader Development

Other issues that can trip up running code on various emulators, virtual machines and real physical hardware that should be addressed are:

在各种仿真器、虚拟机和实际物理硬件上运行代码时可能遇到的其他问题是:

  1. When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP registers having valid or expected values. They should be set up appropriately when your bootloader starts. You can only be guaranteed that your bootloader will be loaded and run from physical address 0x00007c00 and that the boot drive number is loaded into the DL register.
  2. 当BIOS跳转到您的代码时,您不能依赖于CS、DS、ES、SS、SP寄存器的有效或期望值。应该在启动引导加载程序时适当地设置它们。只能保证从物理地址0x00007c00加载和运行引导加载程序,并将引导驱动器号加载到DL寄存器。
  3. Set SS:SP to memory that you know won't conflict with the operation of your own code. The BIOS may have placed its default stack pointer anywhere in the first megabyte of usable and addressable RAM. There is no guarantee as to where that is and whether it will be suitable for the code you write.
  4. 设置SS:SP到内存,您知道它不会与您自己代码的操作冲突。BIOS可能在可用的和可寻址的RAM的前兆字节中放置了它的默认堆栈指针。对于它的位置和是否适合您编写的代码,没有任何保证。
  5. The direction flag used by lodsb, movsb etc could be either set or cleared. If the direction flag is set improperly SI/DI registers may be adjusted in the wrong direction. Use STD/CLD to set it to the direction you wish (CLD=forward/STD=backwards). In this case the code assumes forward movement so one should use CLD. More on this can be found in an instruction set reference
  6. lodsb、movsb等使用的方向标志可以设置或清除。如果方向标志设置不当,SI/DI寄存器可能会调整到错误的方向。使用STD/CLD将其设置为您希望的方向(CLD=forward/STD=back)。在这种情况下,代码假设向前移动,因此应该使用CLD。关于这一点的更多信息可以在指令集引用中找到
  7. When jumping to a kernel it is generally a good idea to FAR JMP to it so that it properly sets CS:IP to expected values. This can avoid problems with kernel code that may do absolute near JMPs and CALLs within the same segment.
  8. 当跳转到内核时,将JMP扩展到它通常是一个好主意,这样它就可以正确地将CS:IP设置为期望值。这可以避免内核代码的问题,这些代码可能在JMPs附近执行绝对操作,并在同一段中调用。
  9. If targeting your boot loader for 16-bit code that works on 8086/8088 processors (AND higher) avoid usage of 32 bit registers in assembly code. Use AX/BX/CX/DX/SI/DI/SP/BP instead of EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP. Although not an issue in this question, it has been an issue for others seeking help. A 32 bit processor can utilizes 32 bit registers in 16-bit real mode, but an 8086/8088/80286 can't since they were 16 bit processors without access to extended 32 bit registers.
  10. 如果将引导加载程序定位为在8086/8088处理器上工作的16位代码(或更高的处理器),则避免在汇编代码中使用32位寄存器。使用AX/BX/CX/DX/SI/DI/SP/BP代替EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP。虽然在这个问题上不是问题,但它已经成为寻求帮助的其他人的问题。一个32位处理器可以使用32位寄存器在16位的实模式中,但是8086/8088/80286不能因为它们是16位处理器而不能访问扩展的32位寄存器。
  11. FS and GS segment registers were added to 80386+ CPUs. Avoid them if you intend to target 8086/8088/80286.
  12. 将FS和GS段寄存器添加到80386+ cpu中。如果你打算以8086/8088/80286为目标,避免使用它们。

To resolve the first and second item this code can be used near the start of the boot loader:

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

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

A couple things to note. When you change the value of the SS register (in this case via a MOV) the processor is suppose to turn off interrupts for that instruction and keep them off until after the following instruction. Normally you don't need to worry about disabling interrupts if you update SS followed immediately by an update of SP. There is a bug in very early 8088 processors where this wasn't honored so if you are targeting the widest possible environments it is a safe bet to explicitly disable and re-enable them. If you don't intend to ever work on a buggy 8088 then the CLI/STI instructions can be removed in the code above. I know about this bug first hand with work I did in the mid 80s on my home PC.

有几点需要注意。当您更改SS寄存器的值(在本例中是通过MOV)时,处理器假定要关闭该指令的中断,并将其关闭到以下指令之后。通常你不需要担心如果你禁用中断更新党*之后立即更新的SP。有一个错误在非常早的8088个处理器,这不是荣幸如果你的目标是尽可能广泛的环境可以肯定的是显式禁用和启用它们。如果您不打算使用一个有bug的8088,那么可以在上面的代码中删除CLI/STI指令。我在80年代中期在我的家庭电脑上做的工作中第一次知道这个bug。

The second thing to note is how I set up the stack. For people new to 8088/8086 16-bit assembly the stack can be set a multitude of ways. In this case I set the top of the stack (lowest part in memory) at 0x8000(SS). I then set the stack pointer (SP) to 0. When you push something on the stack in 16-bit real mode the processor first decrements the stack pointer by 2 and then places a 16-bit WORD at that location. Thus the first push to the stack would be at 0x0000-2 = 0xFFFE (-2). You'd then have an SS:SP that looks like 0x8000:0xFFFE . In this case the stack runs from 0x8000:0x0000 to 0x8000:0xFFFF.

需要注意的第二件事是我如何设置堆栈。对于新到8088/8086 16位装配的人来说,栈可以设置多种方式。在本例中,我将堆栈顶部(内存中的最低部分)设置为0x8000(SS)。然后我将堆栈指针(SP)设置为0。当你以16位的实模式在堆栈上按一些东西时,处理器首先将堆栈指针衰减2,然后在那个位置放置一个16位的单词。因此,对堆栈的第一次推送是在0x0000-2 = 0xFFFE(-2)。然后有一个SS:SP,看起来像0x8000:0xFFFE。在这种情况下,堆栈从0x8000:0x0000运行到0x8000:0xFFFF。

When dealing with the stack running on an 8086(doesn't apply to 80286,80386+ processors) it is a good idea to set the stack pointer (SP) to an even number. On the original 8086 if you set SP to an odd number you would incur a 4 clock cycle penalty for every access to stack space. Since the 8088 had an 8 bit data bus this penalty didn't exist, but loading a 16-bit word on 8086 took 4 clock cycles whereas it took 8 clock cycles on the 8088 (two 8 bit memory reads).

当处理在8086上运行的堆栈(不适用于80286、80386+处理器)时,最好将堆栈指针(SP)设置为偶数。在最初的8086中,如果你将SP设置为一个奇数,那么你将会为每个存取堆栈空间而产生4个时钟周期的惩罚。由于8088有一个8位的数据总线,所以这种惩罚并不存在,但是在8086上装载一个16位的字需要4个时钟周期,而在8088上装载8个时钟周期(2个8位内存读取)。

Lastly, If you want to explicitly set CS:IP so that CS is properly set by the time the JMP is complete (to your kernel) then it is recommended to do a FAR JMP (See Operations that affect segment registers/FAR Jump). In NASM syntax the JMP would look like this:

最后,如果您想要显式地设置CS:IP,以便在JMP完成(对您的内核)时正确地设置CS,那么建议进行远程JMP(请参阅影响段寄存器/远程跳转的操作)。在NASM语法中,JMP是这样的:

jmp 0x07E0:0x0000

Some (ie MASM/MASM32) assemblers don't have direct support to encode a FAR Jmp so one way it can be done is manually like this:

一些(例如MASM/MASM32)汇编器没有直接支持编码一个远的Jmp,因此可以这样手工地进行这样的操作:

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

If using GNU assembler it would look like:

如果使用GNU汇编程序,则会如下:

ljmpw $0x07E0,$0x0000

#1


38  

The primary problems with this code were:

本守则的主要问题是:

  1. ES:BX was pointing to the wrong segment:offset to load the kernel into
  2. ES:BX指向错误的段:偏移量,将内核加载到
  3. Wrong sector was being loaded so kernel wasn't what was expected
  4. 错误的部门正在被加载,所以内核并不是预期的。

The first one was in this code:

第一个是在这个代码中:

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

The question wants to load the sector from disk to 0x0000:0x7E00(ES:BX). This code sets the ES:BX to 0x7E00:0x0000 which resolves to a physical address of 0x7E000 ((0x7E00<<4)+0x0000). I think the intention was to load 0x07E0 into ES which would yield a physical address of 0x7E00 ((0x07E0<<4)+0x0000). You can learn more about 16:16 memory addressing calculations here. Multiplying the segment by 16 is the same as shifting it left 4 bits.

问题希望将扇区从磁盘加载到0x00x7e00 (ES:BX)。这段代码将ES:BX设置为0x7ex0000,它解析为0x7E000的物理地址(0x7E00< 4)+0x0000)。我认为目的是将0x07E0加载到ES中,从而产生0x7E00的物理地址(0x07E0< 4)+0x0000)。您可以在这里了解更多关于16:16内存寻址计算的信息。将段乘以16就等于移动了4位。

The second problem in the code is here:

代码中的第二个问题是:

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

The number for the second 512 block sector on the disk is 2, not 1. So to fix the above code you need to set CL accordingly:

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

mov cl,0x2  ;sector number

General Tips for Bootloader Development

Other issues that can trip up running code on various emulators, virtual machines and real physical hardware that should be addressed are:

在各种仿真器、虚拟机和实际物理硬件上运行代码时可能遇到的其他问题是:

  1. When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP registers having valid or expected values. They should be set up appropriately when your bootloader starts. You can only be guaranteed that your bootloader will be loaded and run from physical address 0x00007c00 and that the boot drive number is loaded into the DL register.
  2. 当BIOS跳转到您的代码时,您不能依赖于CS、DS、ES、SS、SP寄存器的有效或期望值。应该在启动引导加载程序时适当地设置它们。只能保证从物理地址0x00007c00加载和运行引导加载程序,并将引导驱动器号加载到DL寄存器。
  3. Set SS:SP to memory that you know won't conflict with the operation of your own code. The BIOS may have placed its default stack pointer anywhere in the first megabyte of usable and addressable RAM. There is no guarantee as to where that is and whether it will be suitable for the code you write.
  4. 设置SS:SP到内存,您知道它不会与您自己代码的操作冲突。BIOS可能在可用的和可寻址的RAM的前兆字节中放置了它的默认堆栈指针。对于它的位置和是否适合您编写的代码,没有任何保证。
  5. The direction flag used by lodsb, movsb etc could be either set or cleared. If the direction flag is set improperly SI/DI registers may be adjusted in the wrong direction. Use STD/CLD to set it to the direction you wish (CLD=forward/STD=backwards). In this case the code assumes forward movement so one should use CLD. More on this can be found in an instruction set reference
  6. lodsb、movsb等使用的方向标志可以设置或清除。如果方向标志设置不当,SI/DI寄存器可能会调整到错误的方向。使用STD/CLD将其设置为您希望的方向(CLD=forward/STD=back)。在这种情况下,代码假设向前移动,因此应该使用CLD。关于这一点的更多信息可以在指令集引用中找到
  7. When jumping to a kernel it is generally a good idea to FAR JMP to it so that it properly sets CS:IP to expected values. This can avoid problems with kernel code that may do absolute near JMPs and CALLs within the same segment.
  8. 当跳转到内核时,将JMP扩展到它通常是一个好主意,这样它就可以正确地将CS:IP设置为期望值。这可以避免内核代码的问题,这些代码可能在JMPs附近执行绝对操作,并在同一段中调用。
  9. If targeting your boot loader for 16-bit code that works on 8086/8088 processors (AND higher) avoid usage of 32 bit registers in assembly code. Use AX/BX/CX/DX/SI/DI/SP/BP instead of EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP. Although not an issue in this question, it has been an issue for others seeking help. A 32 bit processor can utilizes 32 bit registers in 16-bit real mode, but an 8086/8088/80286 can't since they were 16 bit processors without access to extended 32 bit registers.
  10. 如果将引导加载程序定位为在8086/8088处理器上工作的16位代码(或更高的处理器),则避免在汇编代码中使用32位寄存器。使用AX/BX/CX/DX/SI/DI/SP/BP代替EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP。虽然在这个问题上不是问题,但它已经成为寻求帮助的其他人的问题。一个32位处理器可以使用32位寄存器在16位的实模式中,但是8086/8088/80286不能因为它们是16位处理器而不能访问扩展的32位寄存器。
  11. FS and GS segment registers were added to 80386+ CPUs. Avoid them if you intend to target 8086/8088/80286.
  12. 将FS和GS段寄存器添加到80386+ cpu中。如果你打算以8086/8088/80286为目标,避免使用它们。

To resolve the first and second item this code can be used near the start of the boot loader:

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

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

A couple things to note. When you change the value of the SS register (in this case via a MOV) the processor is suppose to turn off interrupts for that instruction and keep them off until after the following instruction. Normally you don't need to worry about disabling interrupts if you update SS followed immediately by an update of SP. There is a bug in very early 8088 processors where this wasn't honored so if you are targeting the widest possible environments it is a safe bet to explicitly disable and re-enable them. If you don't intend to ever work on a buggy 8088 then the CLI/STI instructions can be removed in the code above. I know about this bug first hand with work I did in the mid 80s on my home PC.

有几点需要注意。当您更改SS寄存器的值(在本例中是通过MOV)时,处理器假定要关闭该指令的中断,并将其关闭到以下指令之后。通常你不需要担心如果你禁用中断更新党*之后立即更新的SP。有一个错误在非常早的8088个处理器,这不是荣幸如果你的目标是尽可能广泛的环境可以肯定的是显式禁用和启用它们。如果您不打算使用一个有bug的8088,那么可以在上面的代码中删除CLI/STI指令。我在80年代中期在我的家庭电脑上做的工作中第一次知道这个bug。

The second thing to note is how I set up the stack. For people new to 8088/8086 16-bit assembly the stack can be set a multitude of ways. In this case I set the top of the stack (lowest part in memory) at 0x8000(SS). I then set the stack pointer (SP) to 0. When you push something on the stack in 16-bit real mode the processor first decrements the stack pointer by 2 and then places a 16-bit WORD at that location. Thus the first push to the stack would be at 0x0000-2 = 0xFFFE (-2). You'd then have an SS:SP that looks like 0x8000:0xFFFE . In this case the stack runs from 0x8000:0x0000 to 0x8000:0xFFFF.

需要注意的第二件事是我如何设置堆栈。对于新到8088/8086 16位装配的人来说,栈可以设置多种方式。在本例中,我将堆栈顶部(内存中的最低部分)设置为0x8000(SS)。然后我将堆栈指针(SP)设置为0。当你以16位的实模式在堆栈上按一些东西时,处理器首先将堆栈指针衰减2,然后在那个位置放置一个16位的单词。因此,对堆栈的第一次推送是在0x0000-2 = 0xFFFE(-2)。然后有一个SS:SP,看起来像0x8000:0xFFFE。在这种情况下,堆栈从0x8000:0x0000运行到0x8000:0xFFFF。

When dealing with the stack running on an 8086(doesn't apply to 80286,80386+ processors) it is a good idea to set the stack pointer (SP) to an even number. On the original 8086 if you set SP to an odd number you would incur a 4 clock cycle penalty for every access to stack space. Since the 8088 had an 8 bit data bus this penalty didn't exist, but loading a 16-bit word on 8086 took 4 clock cycles whereas it took 8 clock cycles on the 8088 (two 8 bit memory reads).

当处理在8086上运行的堆栈(不适用于80286、80386+处理器)时,最好将堆栈指针(SP)设置为偶数。在最初的8086中,如果你将SP设置为一个奇数,那么你将会为每个存取堆栈空间而产生4个时钟周期的惩罚。由于8088有一个8位的数据总线,所以这种惩罚并不存在,但是在8086上装载一个16位的字需要4个时钟周期,而在8088上装载8个时钟周期(2个8位内存读取)。

Lastly, If you want to explicitly set CS:IP so that CS is properly set by the time the JMP is complete (to your kernel) then it is recommended to do a FAR JMP (See Operations that affect segment registers/FAR Jump). In NASM syntax the JMP would look like this:

最后,如果您想要显式地设置CS:IP,以便在JMP完成(对您的内核)时正确地设置CS,那么建议进行远程JMP(请参阅影响段寄存器/远程跳转的操作)。在NASM语法中,JMP是这样的:

jmp 0x07E0:0x0000

Some (ie MASM/MASM32) assemblers don't have direct support to encode a FAR Jmp so one way it can be done is manually like this:

一些(例如MASM/MASM32)汇编器没有直接支持编码一个远的Jmp,因此可以这样手工地进行这样的操作:

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

If using GNU assembler it would look like:

如果使用GNU汇编程序,则会如下:

ljmpw $0x07E0,$0x0000