
| 《机器人学导论》专栏 | 点击进入 |
| 《自己写一个内核》专栏 | 点击进入 |
自己写一个内核(一):最小引导扇区
你按下电源。
CPU 醒过来,干的第一件事不是加载操作系统——它先去跑 BIOS。BIOS 是焊在主板 ROM 里的程序,出厂写死,掉电不丢。它把硬件点一遍,然后把磁盘最开头的 512 字节搬进内存 0x7c00,跳过去。
跳过去之后,跑的就是我们的代码了。
从这一秒起,这 512 字节就是你的全部地盘。这一篇要做的,就是在这块地盘上,让屏幕出现一个 A。
后面还长:512 字节装不下东西,所以要读更多扇区;16 位模式不够用,所以要切保护模式;汇编写着太累,所以要换成 C。再往后是分页、是多任务。这条路从今天这 8 行汇编开始。
开工前:需要两样东西。
Linux 一行搞定:sudo apt install nasm qemu-system-x86(Fedora 用 dnf)。
Windows 去 nasm.us 和 qemu.org 下载安装包,装完把安装目录加进 PATH。注意 Windows 上没有 make、rm 这些命令,后面用 Makefile 那一段可以跳过,直接手敲那两条命令,效果一样。
验证:nasm -v 和 qemu-system-i386 –version,能打出版本号就成。
一、先看结果:屏幕上出现一个 A
代码就这几行:
[bits 16]
[org 0x7c00]
start:
mov ah, 0x0e
mov al, 'A'
int 0x10
jmp $
times 510 – ($ – $$) db 0
dw 0xaa55
存成 boot.asm,敲两条命令:
nasm -f bin boot.asm -o os.img
qemu-system-i386 -drive format=raw,file=os.img
屏幕上出现一个 A。 
答案先给到这。 先敲下去看一眼,再回来读原理,完全来得及——下面每一行都会拆开讲,包括那两行看起来像天书的 times 和 dw。
顺手把命令存起来(可以跳过)
如果不想每次手敲,建一个叫 Makefile 的文件:
AS = nasm
QEMU = qemu-system-i386
all: os.img
os.img: boot.asm
$(AS) -f bin boot.asm -o os.img
run: os.img
$(QEMU) -drive format=raw,file=os.img
clean:
rm -f os.img
之后敲 make 就编译,敲 make run 就运行,敲 make clean 就清场。
规则只有一条:冒号前面是名字,冒号后面是它依赖的东西,命令写在下一行、用 Tab 开头。run: os.img 的意思是"要跑 run,得先有 os.img"。
(rm 那一行是 Linux 写法,Windows 下要用 Git Bash 或者 MSYS2 才能跑。)
二、这 8 行,一行一行看
[bits 16]:告诉汇编器,现在只有 16 位
CPU 刚上电是实模式,这个模式只认 16 位指令。这行是写给 nasm 看的,它不产生任何机器码,只是在说"接下来按 16 位编译"。
[org 0x7c00]:告诉汇编器,代码住在哪
先说 0x 是什么。
平时数数用十进制,逢十进一。电脑用十六进制,逢十六进一——数字只有 0 到 9 十个,不够十六个用,就拿字母顶上:A=10、B=11、C=12、D=13、E=14、F=15。
0x 是记号,表示"后面这串是十六进制"。
换算看每个位的权重。从右往左,第一位是 1,第二位是 16,第三位是 256,第四位是 4096——每次乘 16:
| 0x10 | 1×16 | 16 |
| 0x41 | 4×16 + 1 | 65(字符 A) |
| 0x100 | 1×256 | 256 |
| 0x400 | 4×256 | 1024(1K) |
| 0x7c00 | 7×4096 + 12×256 | 31744 |
0x7c00 就是 BIOS 放我们的位置,它不随你改。
而 [org 0x7c00] 的含义是:告诉 nasm"我这段代码会被放在 0x7c00",后面所有跟地址有关的计算都从这个基点算起。
注意:这一行只在代码里出现地址的时候才重要。这一篇还没有地址,所以即使写错也看不出来——它是下一篇读磁盘时的第一个雷。
mov ah, 0x0e 和 mov al, 'A':往格子里放数
指令干活得有地方放东西。CPU 里的格子叫寄存器,这一篇只用到两个:AH 和 AL。
它俩是 AX 的两半——AX 是 16 位,可以拆成两个 8 位用,高的一半叫 AH(H = High),低的一半叫 AL(L = Low)。
这套名字不是乱码,是 Intel 定的规则:同一个家族,加宽就换前缀,拆半就加后缀。
| A | AH/AL | AX | EAX | RAX | 累加器 |
| B | BH/BL | BX | EBX | RBX | 基址 |
| C | CH/CL | CX | ECX | RCX | 计数 |
| D | DH/DL | DX | EDX | RDX | 数据 |
四个宽度、四个家族,记住"加宽换前缀、拆半加后缀"这一条就全认识了。(SI、DI、SP、BP 这几个是另一类,它们没有 8 位的一半,只有 16/32/64 三种宽度,用到的时候再说。)
mov 就是 move,放数:
- mov ah, 0x0e:把 14 放进取 AH。
- mov al, 'A':把字符 A 放进 AL。
但 CPU 不认"A"这个形状,只认数字。美国人给字母排过号,叫 ASCII,大写 A 是 65。所以 'A' 这个写法,nasm 编译时会替你换成 65——写 'A'、写 0x41、写 65,编出来是同一个东西。
回头看前面那张表:0x41 = 4×16 + 1 = 65。对上了。
int 0x10:喊 BIOS 干活
int 是 interrupt,中断。执行到它,CPU 会跳去 BIOS 的 10 号中断服务程序——一段现成的代码,BIOS 里早就写好了。
这段代码干什么,是看约定的:
所以前面那两条 mov 是在按 BIOS 的约定摆参数,int 0x10 才是真正下命令。这个约定叫调用约定,后面每一步都要跟它打交道。
jmp $:原地打转
jmp 是 jump,跳转。$ 表示"这一行自己的地址",jmp $ 就是跳到自己头上,原地打转,永远停在这。
这句不能省。 不加的话,CPU 执行完 int 0x10 会继续往下走,一路走进后面的数据区、走出这 512 字节,跑进一片不知道是什么的内存里,行为完全不可预料。
三、为什么必须正好 512 字节
代码到 jmp $ 就完了。数一下刚才那四条指令编出来多少字节:
| mov ah, 0x0e | B4 0E | 2 |
| mov al, 'A' | B0 41 | 2 |
| int 0x10 | CD 10 | 2 |
| jmp $ | EB FE | 2 |
一共 8 个字节。
汇编是人看的写法,机器码是 CPU 读的东西。nasm 干的活就是查表翻译:每条汇编换成固定的一串字节,一条对一条,没有别的花样。规律很直白——编号加数据:
- B4 是"往 AH 放数"的编号,0E 是放进去的数
- B0 是"往 AL 放数"的编号,41 是那个 A
- CD 是"中断"的编号,10 是喊 10 号
- EB 是"跳转"的编号,FE 表示往回跳
8 字节,离 512 还差 504 个字节。而这 504 个字节不能空着——因为 BIOS 认不认这个扇区,是看末尾两个字节的。
最后两行就是干这个的:
times 510 – ($ – $$) db 0
dw 0xaa55
拆开看:
- $$ 是这一段代码的起始地址(这里就是 0x7c00)
- $ 是当前地址(写到哪算到哪)
所以 $ – $$ 就是"已经写了多少字节"。510 – ($ – $$) 就是"还差多少能凑到 510"。而 times N db 0 的意思是"把 db 0 重复 N 遍",也就是填 N 个零。
合起来:一直填零,填到第 510 个字节为止。
然后 dw 0xaa55 写下去两个字节,正好把第 510、511 两格占满,凑出完整的 512。
0xaa55 这两个字节在磁盘上按小端存放,实际写进去的顺序是 55 AA。BIOS 读磁盘第一个扇区,只看最后两字节是不是 55 AA:
- 是 → 把这 512 字节搬到 0x7c00,跳进去执行
- 不是 → 当这里没有可启动的东西,跳过
这样两行的分工就清楚了:
- times …:撑满 512 字节,少一个字节扇区就不完整
- dw 0xaa55:贴合格证,没有它 BIOS 根本不理你
这就是"引导扇区"这四个字的全部含义:正好 512 字节,末尾必须是 55 AA,放在磁盘最开头。
四、最容易踩的三个坑
一、忘了 jmp $。 CPU 会继续往下冲进数据区,结果不可预料。这是最隐蔽的一个,因为它不是"报错",是"跑飞"。
二、忘了 times 或者 dw 0xaa55。 少任何一个,BIOS 都不认。表现不是黑屏,而是明确告诉你找不到可启动设备——看见这句提示,先回来查这里。
三、[org] 写错。 这一篇还看不出来,下一篇一开始读磁盘,所有地址会整体错位。先记着。
五、万一没出现 A
别急着重写。先分清是文件不对还是代码不对——这是两件事,分开查快得多。
第一步:文件对不对
stat -c%s os.img
应该输出 512。不是 512,就是 times 或 dw 那两行的问题。
再看末尾两个字节:
od -A x -t x1z os.img | tail -2
0001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa >…………..U.<
000200
第一列是地址:0x1f0 是第 496 字节,往后数 14 个零正好到第 510 字节——然后 55 aa。只要最后两字节不是 55 aa,BIOS 就不会理你。
(顺便,hexdump 在一些精简的 Linux 上默认没装。od 是 coreutils 自带的,任何 Linux 上都有,所以这里用 od。想看纯十六进制可以用 od -A x -t x1 os.img | tail -2。)
第二步:QEMU 说了什么
QEMU 启动时不会一声不吭:
- 它打印了找不到启动设备之类的话 → 它没认 55 AA,回第一步。
- 它什么都没说,窗口停在那 → 它认了,问题在代码本身。
第三步:进内存看
想看更细的,把 QEMU 的监视器挂上:
qemu-system-i386 -drive format=raw,file=os.img -monitor stdio
出现 (qemu) 提示符后,依次敲:
xp/2hx 0x7dfe → 看那两格,应该是 0xaa55
xp/8xb 0x7c00 → 看我们的机器码,应该是 8 个字节
xp/4hx 0xb8000 → 看显存最开头(BIOS 的横幅)
正常的话,第一条会回:
00007dfe: 0xaa55 0x0000
第二条会回:
00007c00: 0xb4 0x0e 0xb0 0x41 0xcd 0x10 0xeb 0xfe
这 8 个字节,就是你上面那张表里的机器码,原样躺在内存里。看到这一行,说明编译没问题、加载地址也没问题。 剩下要查的就只有代码逻辑了。
最后:A 到底出现在哪
实测提示:在 QEMU 里跑,A 不在屏幕左上角。因为 BIOS 先打了自己的横幅,光标早就被推下去了,A 落在光标停住的那个位置(实测在第 8 行第 0 列,也就是贴着左边,但不是第一行)。
如果你照着"左上角"找,可能第一眼看不见。沿着最左边一列从上往下扫,更快。
六、下一段路
512 字节,8 行汇编,屏幕上一个 A。
这是你和这台机器说的第一句话。
但 512 字节实在太小——装不下一个内核,甚至装不下几句像样的话。所以下一篇只有一件事:怎么把磁盘上更多的代码读进来。这就是"两段式引导"。
路已经开了,接着走。
下一篇见。 
网硕互联帮助中心



评论前必须登录!
注册