PC启动过程
pc的启动过程是由BIOS和OS配合完成的
- 当按下电源键之后CPU初始化,初始化时CPU处于16位实模式的,从0xffff0开始执行第一行代码,这个地址被映射到BIOS,因为从0xffff0开始的地址只有16byte,所以BIOS在这里放了一段跳转指令,跳转到其他位置去执行自检POST功能。
- BIOS的POST是Power On Self Test的简称,就是加电自检。这个过程主要是对硬件进行检查,POST之后,BIOS调用int $0x19加载MBR。
- 加载并之行MBR,MBR是Master Boot Recode的简称,就是主引导记录。BIOS调用int $0x19,按照CMOS中设置的启动顺序,挨个设备查找MBR,如果没有找到MBR,BIOS会报错,一般的错误是"Operation System not found"。如果找到了MBR就将MBR加载到0x7c00地址,然后跳转到这个地址开始之行,至此BIOS就将控制权交给了MBR。
MBR
PC的启动过程中控制权会由BIOS移交到MBR,然后MBR在进一步的引导OS,但是MBR长什么样子呢?BIOS如何在众多的引导设备中识别MBR?
MBR位于引导设备的第一个扇区(0道0柱面1扇区),它必须是512字节并且最后两个字节必须是0xaa55,这两个字节是MBR的Magic Number,只有看到一个扇区的最后两个字节是0xaa55时,BIOS才认为这个扇区是MBR,否则不是。
BIOS中断和内存布局
BIOS的功能不仅仅是加载MBR这么简单,它还提供了很多中断供MBR以及后续的系统使用,同时对于最初的1M内存BIOS的布局是固定的。BIOS提供了很多实用的中断,int $0x13用于读写磁盘,int $0x10用于显式,加载MBR都是通过中断int $0x19实现的。
实例
这样看来引导一个OS的第一步就是MBR,因为MBR之前都是BIOS的工作,BIOS在固件里(当然你也可以修改BIOS让它以其他的方式引导OS那就另当别论了)。根据上面对于PC启动过程和MBR的描述就能够很简单的写一个MBR了。《自己动手写操作系统》这本书的作者在第一章就写了一个MBR的例子,但是用的nasm的语法,这里我想把它修改成AT&T格式的并且用gas编译。
.code16
.section .text
#.org 0x7c00
.globl _start
_start:
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw $boot_message, %ax
movw %ax, %bp
movw $0x1301, %ax
movw $16, %cx
movw $0x0c, %bx
movw $0x00, %dx
int $0x10
jmp .
boot_message:
.ascii "Hello, OS world!"
dummy:
.space 510-(.-_start), 0
magic_number:
.byte 0x55, 0xaa
gas/nasm对比
这段代码要使用gas编译,gas使用AT&T语法,与nasm的语法有很大区别,下面的参考文章中详细的介绍了这些区别。
制作启动镜像
要将gas编译之后的二进制文件制作成启动镜像,写入软盘中,然后能够引导虚拟机或者真是的机器启动才算成功,必须采用下面的步骤来制作启动镜像:
as -o boot.o boot.s
ld -Ttext 0x7c00 -o boot boot.o
objcopy -O binary boot boot.bin
dd if=boot.bin of=boot.img count=1
最后得到的boot.img就是镜像文件,如果使用虚拟机验证最后的dd命令根本用不上,boot.bin直接就可以作为镜像使用了,但是如果你想写入真实的软盘或者U盘,dd命令就十分有用了。
《自己动手写操作系统》的例子使用nasm编译之后直接就可以当作镜像文件使用了,但是使用gas编译我们的例子却做不到这点,nasm命令nasm boot.asm -o boot.bin生成的就是无格式的二进制文件,但是gas编译之后的boot.o确实elf格式的。我们要从这里提取出无格式的二进制文件,首先用ld将.text段定位到0x7c00地址,这样代码内部的地址都是根据这个地址计算的了。然后使用objcopy提取无格式的二进制文件boot.bin。
将boot.img写入U盘来制作U盘启动盘,首先使用fdisk -l查看一下U盘设备,我的U盘被识别为/dev/sdb这样使用dd命令将boot.img写入U盘:
dd if=boot.img of=/dev/sdb count=1
写入的时候要小心,如果不慎将sda破坏就废废了。还有就是我在写入的时候犯了一个错误,但是我的U盘是格式化的被Linux识别为/dev/sdb1,我写入的时候就是用了of=/dev/sdb1这样的参数,结果在真是的机器上怎么也启动不了,后来才知道/dev/sdb和/dev/sdb1其实不是一个概念,使用of=/dev/sdb1并没有将boot.img写入到第一个扇区中。
测试
镜像制作好了,我们可以测试一下我们的代码是否能够工作,可以使用qemu+vnc终端来显式结果
/usr/libexec/qemu-kvm -fda boot.img -boot a -vnc :1
-vnc :1 表示vnc协议的地址是127.0.0.1:5901
结果如下图,证明我们的代码成功的启动了虚拟机:
调试
没有十全十美的东西,也没有十全十美的代码,任何代码都会出现问题,有了问题就需要解决,对于调试kernel来说qemu简直是神器。我们可以通过qemu+gdb来调试刚才的代码。
在一个终端中启动qemu虚拟机
qemu-kvm -fda boot.img -boot a -s -S
在另一个终端中启动gdb,并且连接qemu启动的gdbserver
(gdb) target remote localhost:1234
(gdb) set architecture i8086
(gdb) stepi
(gdb) b *0x7c00
(gdb) c
安装最新版本的qemu之后会有qemu-system-i386命令,这样就不存在断点挺不下的问题了。