云计算百科
云计算领域专业知识百科平台

自己写一个内核(一):最小引导扇区

请添加图片描述

我的专栏直达
《机器人学导论》专栏 点击进入
《自己写一个内核》专栏 点击进入

自己写一个内核(一):最小引导扇区

你按下电源。

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 定的规则:同一个家族,加宽就换前缀,拆半就加后缀。

家族8位16位32位64位来历
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 里早就写好了。

这段代码干什么,是看约定的:

  • 先看 AH 里是几。0x0e 表示"在光标处打印一个字符"。
  • 再看 AL 里是几。里面是 0x41,也就是字符 A。
  • 于是把 A 写到屏幕上,然后跳回来,继续跑我们的代码。
  • 所以前面那两条 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 字节实在太小——装不下一个内核,甚至装不下几句像样的话。所以下一篇只有一件事:怎么把磁盘上更多的代码读进来。这就是"两段式引导"。

    路已经开了,接着走。

    下一篇见。 请添加图片描述

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 自己写一个内核(一):最小引导扇区
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!