通常计算机的启动方式有两种,传统的BIOS-MBR启动模式和新的UEFI-GPT启动模式,本文将介绍传统的BIOS-MBR启动模式。

intel 80386计算机加电时,寄存器的值

首先,我们看一下按下计算机的电源或者复位键之后CPU中寄存器的初始值。

我们需要重点关注一下cs寄存器和eip寄存器,初始化状态的CSEIP确定了处理器的初始执行地址,此时CS中可见部分-选择子(selector)的值为0xF000,而其不可见部分-基地址(base)的值为0xFFFF0000;EIP的值是0xFFF0,这样实际的线性地址(由于没有启动页机制,所以线性地址就是物理地址)为CS.base+EIP=0xFFFFFFF0。在0xFFFFFFF0这里只是存放了一条跳转指令,通过跳转指令跳到BIOS例行程序起始点。

在这里我们先暂停,做个简单的实验,用硬件模拟机器qemu来进一步认识上述结果。

实验1:通过qemu了解Intel 80386启动后的CS和EIP值,并分析第一条指令的内容

  1. 首先,启动qemu并让其停到执行第一条指令前,这需要增加一个参数"-S", 如下:
1
qemu-system-i386 -S
  • -S 参数告诉虚拟机启动后先不运行。

这时qemu会弹出一个没有任何显示内容的图形窗口,显示如下:

  1. 然后通过按Ctrl+Alt+2进入qemumonitor界面,为了了解80386此时的寄存器内容,在monitor界面下输入命令info registers

我们可以看到EIP=0xfff0CSselector=0xf000CSbase=0xffff0000

intel 80386计算机启动流程

由上节我们知道,intel 80386计算机加电后,执行的第一条指令的位置是:0xFFFFFFF0,该位置其实是BIOS程序,它做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行。

其实,了解了如上信息,就足够了,着急的读者可以直接跳到下一节。不过我还是想详细介绍了计算机的启动流程。

当计算机上电初始化时,物理内存被设置成从地址0开始的连续区域。除了地址从0xA00000xFFFFF(640K1M384K)和0xFFFE00000xFFFFFFFF(4G处的最后64K)范围以外的所有内存都可用作系统内存。这两个特定范围被用于I/O设备和BIOS程序。640K–1M之间的384K用作下图中指明的用途。其中地址0xA0000开始的128K用作显存缓冲区,随后部分用于其他控制卡的ROM BIOS或其映射区域,而0xF00001M范围用于高端系统ROM BIOS的映射区。

ROM-BIOS是一段固化在主板上的程序,这段程序在计算机加电后会自动被加载到内存中,主要用于计算机的自检和初始化。根据上面的分析可知0xFFFFFFF0正好处于这段程序中,位于4G空间最后一个64K的最后16字节处。这里会被安排一条ljmp指令,用于跳转到BIOS代码中64KB范围内的某一条指令开始执行。BIOS在执行了一系列硬件检测和初始化操作之后,会把与原来PC机兼容的64KB BIOS代码和数据复制到内存低端1M末端的64K处,然后跳转到这个地方并让CPU运行在实地址模式下。过程如下图所示。

最后,如果硬盘或软盘是首选的启动设备的话,BIOS会读取其中的0柱面0磁道1扇区,并检测是否为可引导设备,如果是的话,这个扇区将被加载到内存0x7c00处并被执行。可引导的标志是扇区的最后两个字节为0x550xAA

引导扇区

上面提到,BIOS程序完成计算机硬件的自检和初始化后,会选择一个启动设备,并读取该设备的第一个扇区到特定的地址0x7c00处,然后将CPU控制权转移到那个地址继续执行。

其实引导扇区是有规范的,如果不符合规范,BIOS程序会提示找不到启动磁盘的。下面我们就演示一下。

首先我们创建一个空的磁盘映像文件,使用dd命令。

1
# dd if=/dev/zero of=disk0.img bs=1024 count=200

上面我们创建了一个200KB大的硬盘映像。

查看一下硬盘映像中的内容。因为我们在创建时输入使用的是产生0的设备文件,所以现在的这块硬盘中的内容全部为零,为了加深印象我们还是查看一下。

1
2
3
4
# xxd -a disk0.img 
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
00031ff0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

可以看到disk0.img中的内容全为零。

下面我们试试看如果直接用虚拟机去启动这块硬盘的话会发生什么。

1
# qemu-system-i386 disk0.img

虚拟机启动后结果如下,Boot failed: not a bootable disk。提示磁盘不可引导。

我们将disk0.img复制一份,命名为disk1.img,现在将disk1.img文件的第510511字节改为0x550xAA,然后重新启动看看结果。

这里我们使用hexedit这个工具,对disk1.img进行编辑。

1
2
# cp disk0.img disk1.img
# hexedit disk1.img

通过键盘方向键定位到位置0x1FE即十进制510这个位置,将连续的两个字节分别修改为0x550xAACtrl + X保存退出。

再次查看,可以看出已经有了我们需要的可引导标记。

1
2
3
4
5
6
7
# xxd -a disk1.img 
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.
00000200: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
00031ff0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

我们再次尝试启动虚拟机,看看这次的结果。

1
# qemu-system-i386 disk1.img

如我们所愿,虚拟机这次告诉我们已经从硬盘开始引导了。但是我们的硬盘里一行指令也没有,所以现在虚拟机就傻傻的在那里等着。

小工具

本节介绍一个小工具,用于生成合法的主引导记录,也就是磁盘的0柱面0磁道1扇区,即对磁盘映像文件的前512个字节设置为可引导的。

程序的功能如下:

  1. 读入一个不大于510字节的文件
  2. 将它补齐到510字节
  3. 将第510511字节(从0开始计数)设置为0x550xAA
  4. 写入新的文件

程序的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// copy form: https://github.com/chyyuu/os_kernel_lab/blob/master/labcodes/lab1/tools/sign.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>

int
main(int argc, char *argv[]) {
    struct stat st;
    if (argc != 3) {
        fprintf(stderr, "Usage: <input filename> <output filename>\n");
        return -1;
    }
    if (stat(argv[1], &st) != 0) {
        fprintf(stderr, "Error opening file '%s': %s\n", argv[1], strerror(errno));
        return -1;
    }
    printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);
    if (st.st_size > 510) {
        fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);
        return -1;
    }
    char buf[512];
    memset(buf, 0, sizeof(buf));
    FILE *ifp = fopen(argv[1], "rb");
    int size = fread(buf, 1, st.st_size, ifp);
    if (size != st.st_size) {
        fprintf(stderr, "read '%s' error, size is %d.\n", argv[1], size);
        return -1;
    }
    fclose(ifp);
    buf[510] = 0x55;
    buf[511] = 0xAA;
    FILE *ofp = fopen(argv[2], "wb+");
    size = fwrite(buf, 1, 512, ofp);
    if (size != 512) {
        fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
        return -1;
    }
    fclose(ofp);
    printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
    return 0;
}

编译连接

1
# gcc -o sign sign.c

创建一个小文件(小于510字节),并查看:

1
2
3
# echo "Hello, World." > boot
# xxd -a boot 
00000000: 4865 6c6c 6f2c 2057 6f72 6c64 2e0a       Hello, World..

接下来用我们的工具处理一下这个文件:

1
2
3
# ./sign boot boot.img
'boot' size: 14 bytes
build 512 bytes boot sector: 'boot.img' success!

查看生成的文件boot.img, 此时生成的文件已经是512字节了。

1
2
3
4
5
# xxd -a boot.img 
00000000: 4865 6c6c 6f2c 2057 6f72 6c64 2e0a 0000  Hello, World....
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.

用虚拟机从这个磁盘映像文件启动:

1
# qemu-system-i386 boot.img

结果和之前使用hexedit手动编辑是一样的,引导成功。

进一步debug BIOS的启动过程

上面提到,计算机加电后,执行的第一条指令的位置是:0xFFFFFFF0,实际上,该位置是一个跳转指令,跳到BIOS程序完成计算机硬件自检和初始化,最后读取启动设备的第一个扇区内容到0x7c00处,下面我们通过qemu来验证一下这些内容。

首先我们通过上面的小工具制作一个合法的引导扇区。我们在扇区的开头的位置,写入字符串Hello,World!,然后通过sign工具,将第510511字节(从0开始计数)设置为0x550xAA。命令如下:

1
2
# echo "Hello,World!" > boot.bin
# ./sign boot.bin boot.img

查看其内容如下:

1
2
3
4
5
# xxd -a boot.img 
00000000: 4865 6c6c 6f2c 576f 726c 6421 0a00 0000  Hello,World!....
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.

用虚拟机从这个磁盘映像文件启动:

1
qemu-system-i386 boot.img -S -s
  • -S 参数告诉虚拟机启动后先不运行。
  • -s 参数告诉虚拟机开启一个GDB服务器等待客户端的连接,服务默认监听TCP端口1234

在另外一个终端中,启动GDB

1
2
# gdb -q
(gdb) 
  • -q: 参数表示静默启动,不显示版本信息。

连接到目标服务器:

1
2
3
4
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000fff0 in ?? ()
(gdb) 

设置CPU架构为i8086,因为最开始的这段代码运行在16位实地址模式:

1
2
3
4
5
6
(gdb) set architecture i8086
warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration
of GDB.  Attempting to continue with the default i8086 settings.

The target architecture is assumed to be i8086
(gdb) 

设置当程序停住或单步调试时自动显示指令:

1
2
3
4
(gdb) display/i $cs*16+$pc
1: x/i $cs*16+$pc
   0xffff0:	ljmp   $0xf000,$0xe05b
(gdb) 

从上面可以看出,$cs*16+$pc就是计算机加电时的开始执行的地址,该位置是一条ljmp指令,跳转到BIOS程序中进行初始化,然后会读取引导扇区到内存一个特定的地址0x7c00处,CPU控制权会转移到那个地址继续执行。

接下来,我们在0x7c00处设置一个断点:

1
2
(gdb) b *0x7c00
Breakpoint 1 at 0x7c00

输入c使虚拟机恢复运行:

1
2
3
4
5
6
(gdb) c
Continuing.

Breakpoint 1, 0x00007c00 in ?? ()
1: x/i $cs*16+$pc
=> 0x7c00:	dec    %ax

查看0x7c00处的内容:

1
2
3
(gdb) x/16xb 0x7c00
0x7c00:	0x48	0x65	0x6c	0x6c	0x6f	0x2c	0x57	0x6f
0x7c08:	0x72	0x6c	0x64	0x21	0x0a	0x00	0x00	0x00

上面GDB的输出,正是引导分区开头处的内容,即字符串Hello,World!

总结

本文介绍了Intel 80386加电后的启动过程,并结合qemu分析验证了所学到的知识,为后续开启操作系统的学习打下基础。

参考文章