如何从第一阶段加载第二阶段引导程序?

10

我编写了一个简单的第一阶段引导加载程序,使用中断向bios显示“Hello world”。现在作为下一个明显的步骤,需要编写第二阶段,但是它的代码应该存在于哪里,如何从第一阶段加载它?

这里是第一阶段的程序

[BITS 16]   ;Tells the assembler that its a 16 bit code
[ORG 0x7C00]    ;Origin, tell the assembler that where the code will
                ;be in memory after it is been loaded

MOV SI, HelloString ;Store string pointer to SI
CALL PrintString    ;Call print string procedure
JMP $       ;Infinite loop, hang it here.


PrintCharacter: ;Procedure to print character on screen
    ;Assume that ASCII value is in register AL
MOV AH, 0x0E    ;Tell BIOS that we need to print one charater on screen.
MOV BH, 0x00    ;Page no.
MOV BL, 0x07    ;Text attribute 0x07 is lightgrey font on black background

INT 0x10    ;Call video interrupt
RET     ;Return to calling procedure



PrintString:    ;Procedure to print string on screen
    ;Assume that string starting pointer is in register SI

next_character: ;Lable to fetch next character from string
MOV AL, [SI]    ;Get a byte from string and store in AL register
INC SI      ;Increment SI pointer
OR AL, AL   ;Check if value in AL is zero (end of string)
JZ exit_function ;If end then return
CALL PrintCharacter ;Else print the character which is in AL register
JMP next_character  ;Fetch next character from string
exit_function:  ;End label
RET     ;Return from procedure


;Data
HelloString db 'Hello World', 0 ;HelloWorld string ending with 0

TIMES 510 - ($ - $$) db 0   ;Fill the rest of sector with 0
DW 0xAA55           ;Add boot signature at the end of bootloader

你在使用C语言吗?你还有什么其他重要的信息可以分享吗? - Hamish Grubijan
我们已经使用了x86汇编指令作为第一阶段,但我们计划用高级语言如C来编写第二阶段...我应该把第二阶段的二进制文件存储在哪里?如何从第一阶段引导加载它? - Xinus
可能是从汇编(NASM)加载内核的重复问题。 - Ciro Santilli OurBigBook.com
3个回答

8
在x86上,您可以执行以下操作(简化):
  • 让引导加载程序将磁盘/软盘的第n个扇区(无论您从哪里引导)加载到内存中并执行它(即加载段/偏移量并执行retf)。更好的选择是搜索特定文件名的文件系统(例如KERNEL.BIN)--但您需要知道文件系统类型(例如,如果您从软盘映像进行测试,则为FAT12)。
  • 然后内核会以实模式启动。它设置代码描述符、GDT等,激活32位寻址(您应该听说过“A20”),最后进入保护模式。然后,您需要远跳转到一个32位代码段(内核文件必须以一种使32位代码位于绝对位置,例如在16位实模式之后的偏移512处链接在一起的方式进行链接)。
  • 然后,32位内核汇编只是定义EXTERN _mykernel(例如)并调用该符号。
  • 然后,您可以开始编写作为C函数mykernel的内核。

好的,那是我几年前所做的简要概述(大量从互联网复制粘贴;)。如果这不是有用的,请参考一些关于操作系统开发的良好网络资源:

希望这有所帮助^^


1

请查看这里的GRUB实现(第1阶段):

http://src.illumos.org/source/xref/illumos-gate/usr/src/grub/grub-0.97/stage1/stage1.S

首先注意到第一扇区的起始点为0x7c00,结束标志为0xaa55。从反汇编中可以看到:

349 copy_buffer:
350   movw    ABS(stage2_segment), %es
351 
352   /*
353    * We need to save %cx and %si because the startup code in
354    * stage2 uses them without initializing them.
355    */
356   pusha
357   pushw   %ds
358 
359   movw    $0x100, %cx
360   movw    %bx, %ds
361   xorw    %si, %si
362   xorw    %di, %di
363 
364   cld
365 
366   rep
367   movsw
368 
369   popw    %ds
370   popa
371 
372   /* boot stage2 */
373   jmp *(stage2_address)
374 
375 /* END OF MAIN LOOP */
376

本质上,逻辑是将第二阶段代码复制到内存的另一个部分,然后直接跳转到那里,这就是“引导阶段2”。换句话说,“引导阶段1”在BIOS加载扇区到内存后有效触发,而阶段2是您跳转到的地方 - 它可以在任何地方。


汇编器如何知道从磁盘加载的代码的所有标签现在都偏移了RAM中的位置? - James M. Lay

1

最简单的可运行NASM BIOS示例,它加载第二阶段并跳转到它

use16
org 0x7C00

    ; You should do further initializations here
    ; like setup the stack and segment registers.

    ; Load stage 2 to memory.
    mov ah, 0x02
    ; Number of sectors to read.
    mov al, 1
    ; This may not be necessary as many BIOS set it up as an initial state.
    mov dl, 0x80
    ; Cylinder number.
    mov ch, 0
    ; Head number.
    mov dh, 0
    ; Starting sector number. 2 because 1 was already loaded.
    mov cl, 2
    ; Where to load to.
    mov bx, stage2
    int 0x13

    jmp stage2

    ; Magic bytes.    
    times ((0x200 - 2) - ($ - $$)) db 0x00
    dw 0xAA55

stage2:

    ; Print 'a'.
    mov ax, 0x0E61
    int 0x10

    cli
    hlt

    ; Pad image to multiple of 512 bytes.
    times ((0x400) - ($ - $$)) db 0x00

编译并运行:

nasm -f bin -o main.img main.asm
qemu-system-i386 main.img

预期结果:屏幕上打印出a,然后程序停止。
在Ubuntu 14.04上进行测试。
更合理的GAS示例使用链接器脚本和更正确的初始化(段寄存器、堆栈)在我的GitHub上

如果有人能猜到为什么会被踩,请告诉我,这样我就可以学习和改进信息。我从不报复。 - Ciro Santilli OurBigBook.com
1
啊,我正在查看“bootloader”标签的活动列表,我又见到了你。我的一个观察是,我可能会将此问题关闭为过于宽泛。尽管您提供了一种解决方案,但您断言OP只需从MBR后面的下一个扇区加载即可。虽然这是一种解决方案,但我认为得票最多的答案更接近实际情况,因为它讨论了其他想法,比如对文件系统进行文件查找。那个答案也假设了很多东西——是否有包含第二阶段的文件系统?它是什么文件系统?等等... - Michael Petch
@MichaelPetch 你好,又见面了 :-) 我同意那是一个好答案。只是我通常喜欢先运行一些东西来看到它的美丽:这样后面理解更深层次的部分就更容易了。 - Ciro Santilli OurBigBook.com
1
你应该确保_ES_寄存器为零,因为Int13h/AH=2h用于缓冲区地址。我可能也会利用JMP stage2将_CS_设置为零,所以我会执行jmp 0x0000:stage2。我认为为了完整性,当你在stage2时,你应该确保_DS_也是零,因为任何依赖_DS_为特定值的代码可能无法在BIOS将_DS_设置为其他值的环境中正常工作。 - Michael Petch
1
同时,我不建议您将_DL_设置为固定的驱动器号。您应该使用从BIOS传递给引导加载程序控制传输时传入的驱动器号。 - Michael Petch
由于您正在将扇区读入RAM,因此应在特定位置设置SS:SP,以确保磁盘读取不会覆盖它。如果堆栈恰好向下增长0x8000到0x7e00,则堆栈可能会与第二阶段冲突。最简单的方法是将堆栈移动到您知道没有干扰的位置。 - Michael Petch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接