汇编语言一发入魂 0x00 - 计算机是如何启动的?
这个系列的首篇老李打算为大家介绍一下计算机是如何启动的。我的设想是把我们写出来的程序直接放在CPU
上去跑,因为汇编语言就是和硬件打交道的语言,如果在我们的程序和硬件之间隔一层操作系统的话,总感觉没那么舒服。结合这篇文章的知识,我们就可以让硬件直接加载我们自己的程序并执行。
通常计算机的启动方式分为两种:传统的BIOS-MBR
启动模式和新的UEFI-GPT
启动模式。在这里我们介绍传统的BIOS-MBR
启动模式。
先来看一下按下计算机的电源或者复位键之后CPU
中寄存器的初始值。
我们重点关注一下cs
寄存器和eip
寄存器,这两个寄存器决定了CPU
执行哪里的代码,地址计算的方式为:EA = Base + EIP
。
由上图的寄存器状态可知CPU
在加电后首先会去0xFFFF0000 + 0x0000FFF0
处即0xFFFFFFF0
处执行第一条指令。下面我们看一看计算机加电后的内存状态是怎样的。
当计算机上电初始化时,物理内存被设置成从地址0
开始的连续区域。除了地址从0xA0000
到0xFFFFF
(640K
到1M
共384K
)和0xFFFE0000
到0xFFFFFFFF
(4G
处的最后64K
)范围以外的所有内存都可用作系统内存。这两个特定范围被用于I/O
设备和BIOS
程序。640K
--1M
之间的384K
用作下图中指明的用途。其中地址0xA0000
开始的128K
用作显存缓冲区,随后部分用于其他控制卡的ROM BIOS
或其映射区域,而0xF0000
到1M
范围用于高端系统ROM BIOS
的映射区。
ROM-BIOS
是一段固化在主板上的程序,这段程序在计算机加电后会自动被加载到内存中,主要用于计算机的自检和初始化。根据上面的分析可知0xFFFFFFF0
正好处于这段程序中,位于4G
空间最后一个64K
的最后16
字节处。这里会被安排一条jmp
指令,用于跳转到BIOS
代码中64KB
范围内的某一条指令开始执行。BIOS
在执行了一系列硬件检测和初始化操作之后,会把与原来PC
机兼容的64KB BIOS
代码和数据复制到内存低端1M
末端的64K
处,然后跳转到这个地方并让CPU
运行在实地址模式下。过程如下图所示。
最后,如果硬盘或软盘是首选的启动设备的话,BIOS
会读取其中的0
柱面0
磁道1
扇区,并检测是否为可引导设备,如果是的话,这个扇区将被加载到内存0x7c00
处并被执行。可引导的标志是扇区的最后两个字节为0x55
和0xAA
。
在这篇文章中我们先不写程序,但我们可以做一个小实验验证一下上面学到的这些知识。
实验环境如下:
- 系统:
Ubuntu 18.04.4 LTS
- 虚拟机:
QEMU emulator version 2.11.1(Debian 1:2.11+dfsg-1ubuntu7.21)
- 二进制编辑器:
hexedit
我们将用虚拟机代替物理机,用磁盘映像文件代替真实的硬盘。因为实验中我们需要操作主引导记录,主引导记录对于操作系统是非常重要的,如果操作不慎将会导操作系统无法启动。
首先我们创建一个空的磁盘映像文件,使用dd
命令。
$ dd if=/dev/zero of=disk.img bs=1024 count=100
上面我们创建了一个100KB
大的硬盘映像,好像有点小,但是对于我们的实验来说足够了。
查看一下硬盘映像中的内容。因为我们在创建时输入使用的是产生0
的设备文件,所以现在的这块"硬盘"中的内容全部为零,为了加深印象我们还是查看一下。
$ xxd -a disk.img
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
*
00018ff0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
可以看到disk.img
中的内容全为零。
下面我们试试看如果直接用虚拟机去启动这块硬盘的话会发生什么。
$ qemu-system-i386 disk.img
虚拟机启动后结果如下,Boot failed: not a bootable disk
。提示磁盘不可引导。
现在我们将disk.img
文件的第510
、511
字节改为0x55
、0xAA
,然后重新启动看看结果。
关于如何直接修改这两个字节,如果有二进制文件编辑器的话会简单很多。当然如果没有的话我们可以自己写一个工具来完成这个目标。
先使用工具来直接对这两个字节进行编辑,我们用到的工具是hexedit
。
$ hexedit disk.img
通过键盘方向键定位到位置0x1FE
即十进制510
这个位置,将连续的两个字节分别修改为0x55
、0xAA
,Ctrl + X
保存退出。
再次查看,可以看出已经有了我们需要的可引导标记。
$ xxd -a disk.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 ................
*
00018ff0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
我们再次尝试启动虚拟机,看看这次的结果。
$ qemu-system-i386 disk.img
如我们所愿,虚拟机这次告诉我们已经从硬盘开始引导了。但是我们的硬盘里一行指令也没有,所以现在虚拟机就傻傻的在哪里等着。
至此我们已经知道了计算机是如何启动的,剩下的内容作为老李赠送给大家的一点小礼物。老李会教大家写一个小工具,用于将主引导记录,也就是磁盘的0
柱面0
磁道1
扇区,对应磁盘映像文件的前512
个字节设置为可引导的。
我们的程序功能很简单:
- 读入一个不大于
510
字节的文件 - 将它补齐到
510
字节 - 将第
510
、511
字节(从0
开始计数)设置为0x55
、0xAA
- 写入原文件
程序代码如下:
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void die(const char *str, ...)
{
va_list args;
va_start(args, str);
vfprintf(stderr, str, args);
va_end(args);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
int fd;
ssize_t size;
char buf[512];
if (argc != 2)
{
die("usage: ./sign bin_file");
}
if ((fd = open(argv[1], O_RDWR)) == -1)
{
die("open %s error", argv[1]);
}
if ((size = read(fd, buf, 1024)) == -1)
{
die("read %s error", argv[1]);
}
if (size > 510)
{
die("boot block too large: %d bytes (max 510)", size);
}
while ((510 - size) != 0)
{
buf[size++] = 0;
}
buf[510] = 0x55;
buf[511] = 0xAA;
if (lseek(fd, 0, SEEK_SET) == -1)
{
die("lseek error");
}
if ((size = write(fd, buf, 512)) != 512)
{
die("write %s error", argv[1]);
}
return 0;
}
程序的结构很简单,不多解释了。主逻辑之外封装了一个用于打印错误信息并退出的函数die
,这个函数来自linux
的源码,功能上没有什么深奥的地方,重点在于它接收可变参数。以后有时间了跟大家介绍一下C
语言中可变参数怎么用,下面我们看一下这个工具怎么使用。
将文件保存为sign.c
,编译链接:
$ make sign
cc sign.c -o sign
我们使用make
来进行编译链接,可以少敲几个字母。真正执行的命令也在控制台打印出来了。
创建一个空文件,并查看:
$ touch boot
$ ls -l
总用量 20
-rw-rw-r-- 1 laoli laoli 0 3月 4 14:15 boot
-rwxrwxr-x 1 laoli laoli 12776 3月 4 14:14 sign
-rw-rw-r-- 1 laoli laoli 928 3月 4 14:14 sign.c
可以看到boot
文件长度为0
,内容为空。接下来用我们的工具处理一下这个文件:
$ ./sign boot
$ ls -l
总用量 24
-rw-rw-r-- 1 laoli laoli 512 3月 4 14:16 boot
-rwxrwxr-x 1 laoli laoli 12776 3月 4 14:14 sign
-rw-rw-r-- 1 laoli laoli 928 3月 4 14:14 sign.c
此时boot
文件的长度已经是512
字节了。查看其内容:
$ xxd -a boot
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
*
000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa ..............U.
用虚拟机从这个磁盘映像文件启动:
$ qemu-system-i386 boot
结果和之前使用hexedit
手动编辑是一样的,引导成功。
之所以要教大家自己写一个创建可引导磁盘映像的工具是因为以后我们写的汇编代码需要用到。这篇文章中我们并没有涉及到汇编语言,所以虚拟机启动起来之后我们无法观察到太多的信息。下一篇文章中,老李将正式开始教大家学习汇编语言。加油,奥里给!
文章中部分内容和图片摘录自《Linux 内核完全注释(修正版v5.0)- 赵炯 编著》