引导加载程序不会跳转到内核代码 [英] Boot loader doesn't jump to kernel code

查看:30
本文介绍了引导加载程序不会跳转到内核代码的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在编写小型操作系统 - 用于练习.我从引导加载程序开始.
我想创建在 16 位实模式下运行的小型命令系统(目前).
我创建了重置驱动器的引导加载程序,然后在引导加载程序之后加载扇区.
问题是因为在 jmp 函数之后没有实际发生.

我没有尝试在 0x7E00 处加载下一个扇区(我不完全确定如何使用 es:bx 指向地址,所以这可能是一个问题,我相信它的地址:偏移量),就在引导加载程序之后.

这是代码:

<代码>;;扇区 0x0;;dl 是引导加载程序所在的硬盘数组织 0x7C00位 16;重置硬盘异或啊,啊整数 0x13;读取扇区液晶显示器mov bx,0x7E00移动 es,bx异或 bx,bxmov ah,0x02 ;函数mov al,0x1 ;要读取的扇区mov ch,0x0 ;曲目mov cl,0x1 ;扇区mov dh,0x0 ;头部整数 0x13;如果没有读到 jmp 出错jc错误;跳转到 0x7E00 - 仅在加载时执行jmp 0x7E00错误:mov si,MSGError.环形:lodsb或 al,aljz.end移动啊,0x0E整数 0x10循环.结尾:高MSGError db启动时出错",0x0时间 0x1FE - ($ - $$) db 0x0数据库 0x55数据库 0xAA;;扇区 0x1;jmp打印测试;定义MSGLoaded db "执行成功", 0x0;;打印功能;si - 发送消息(需要以 0x0 结束)打印测试:mov si,MSGLoaded.环形:lodsb或 al,aljz.end移动啊,0x0E整数 0x10循环.结尾:高时间 0x400 - ($-$$) db 0x0

我一直在使用 VirtualBox 测试此代码,但实际上没有任何反应,未显示读取错误以及应打印的消息.

解决方案

这段代码的主要问题是:

  1. ES:BX 指向错误的段:将内核加载到的偏移量
  2. 加载了错误的扇区,因此内核不是预期的

第一个在这段代码中:

mov bx,0x7E00移动 es,bx异或 bx,bx

问题想要将扇区从磁盘加载到 0x0000:0x7E00(ES:BX).此代码将 ES:BX 设置为 0x7E00:0x0000,它解析为 0x7E000 ((0x7E00<<4)+0x0000 的物理地址).我认为目的是将 0x07E0 加载到 ES 中,这将产生 0x7E00 ((0x07E0<<4)+0x0000) 的物理地址.您可以了解有关 16:16 内存寻址计算的更多信息 此处.将段乘以 16 与将其左移 4 位相同.

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

mov ah,0x02 ;functionmov al,0x1 ;要读取的扇区mov ch,0x0 ;曲目mov cl,0x2 ;扇区号mov dh,0x0 ;头部整数 0x13

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

mov cl,0x2 ;扇区号

引导加载程序开发的一般提示

其他可能导致在各种模拟器、虚拟机和真实物理硬件上运行代码的问题应该解决:

  1. 当 BIOS 跳转到您的代码时,您不能依赖 CSDSESSS,SP 寄存器具有有效值或预期值.它们应该在引导加载程序启动时进行适当设置.您只能保证您的引导加载程序将从物理地址 0x00007c00 加载并运行,并且引导驱动器编号已加载到 DL 寄存器中.
  2. SS:SP 设置为您知道不会与您自己的代码的操作发生冲突的内存.BIOS 可能已将其默认堆栈指针放置在可用和可寻址 RAM 的第一兆字节中的任何位置.无法保证它在哪里以及它是否适合您编写的代码.
  3. lodsbmovsb 等使用的方向标志可以设置或清除.如果方向标志设置不当,SI/DI 寄存器可能会调整到错误的方向.使用 STD/CLD 将其设置为您希望的方向(CLD=forward/STD=backwards).在这种情况下,代码假定向前移动,因此应使用 CLD.可以在 指令集参考
  4. 当跳转到一个内核时,通常一个好主意是 FAR JMP 到它,以便它正确地将 CS:IP 设置为预期值.这可以避免内核代码可能在同一段内执行absolute near JMPsCALLs 的问题.
  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 ;对于这个问题,我们希望 DS 为 0 段mov ds,ax ;根据您的情况将 AX 设置为适当的段值移动 es,ax ;在这种情况下,我们将默认为 ES=DSmov bx,0x8000 ;堆栈段可以是任何可用的内存cli;禁用中断以规避早期 8088 CPU 上的错误mov ss,bx ;这将它放在堆栈顶部@ 0x80000.mov sp,ax ;设置 SP=0 这样堆栈的底部将是@ 0x8FFFF斯蒂;重新启用中断CL ;设置方向标志为正方向

需要注意的几件事.当您更改 SS 寄存器的值时(在本例中通过 MOV),处理器应该关闭该指令的中断并保持它们关闭直到 以下指令之后.通常,如果您更新 SS 后立即更新 SP,则无需担心禁用中断.在早期的 8088 处理器中存在一个错误,该错误没有得到尊重,因此如果您的目标是尽可能广泛的环境,那么明确禁用和重新启用它们是一个安全的选择.如果您不打算在有问题的 8088 上工作,那么可以在上面的代码中删除 CLI/STI 指令.我在 80 年代中期在家用 PC 上所做的工作中第一手知道这个错误.

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

处理在 8086 上运行的堆栈时(不适用于 80286、80386+ 处理器),最好将堆栈指针 (SP) 设置为偶数.在原始 8086 上,如果您将 SP 设置为奇数,您将产生 4 个时钟周期每次访问堆栈空间的惩罚.由于 8088 有一个 8 位数据总线,这个惩罚不存在,但是在 8086 上加载一个 16 位需要 4 个时钟周期,而在 8088(两个 8 位内存)上需要 8 个时钟周期读).

最后,如果您想显式设置 CS:IP 以便在 JMP 完成时正确设置 CS(以您的内核)然后建议执行 FAR JMP(请参阅影响段寄存器的操作/远跳).在 NASM 语法中,JMP 看起来像这样:

jmp 0x07E0:0x0000

某些(即 MASM/MASM32)汇编器不直接支持对 FAR Jmp 进行编码,因此可以通过手动方式完成编码,如下所示:

db 0x0ea ;远跳转指令dw 0x0000 ;抵消dw 0x07E0 ;部分

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

ljmpw $0x07E0,$0x0000

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.

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.

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.

解决方案

The primary problems with this code were:

  1. ES:BX was pointing to the wrong segment:offset to load the kernel into
  2. Wrong sector was being loaded so kernel wasn't what was expected

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.

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:

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. 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.
  3. 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
  4. 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.
  5. 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.
  6. FS and GS segment registers were added to 80386+ CPUs. Avoid them if you intend to target 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 supposed 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.

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.

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).

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:

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:

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

If using GNU assembler it would look like:

ljmpw $0x07E0,$0x0000

这篇关于引导加载程序不会跳转到内核代码的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆