C++ 与构建 — 类、虚函数与那个被修掉的加载 bug
C++ 与构建 — 类、虚函数与那个被修掉的加载 bug
这一章讲什么: 上一章的白屏是「瞎画」——每个字节写 255,什么格式都是白。 本章先把像素格式正经处理掉,实现「任何位置、任何颜色」的画点; 然后把画点的 if-else 换成 C++ 的虚函数,顺带搞懂 vtable 这个「编译器替你做的查表」; 最后修一个真实 bug:加载器按文件大小分配内存,内核有个段在文件里只占 0 字节、 在内存里却要占地方——不修这个 bug,加两个全局变量内核就会神秘崩溃。
1. 先看现象:50 万次 if 是怎么消失
主线例子,画一个 200×100 像素的绿色矩形,屏幕一共约 50 万像素:
写法一(4.2 节,WritePixel 函数):
每画一个像素都问一次「格式是 RGB 还是 BGR?」
→ 50 万个像素 = 50 万次条件分支,问的全是同一个答案
写法二(4.3 节,PixelWriter 类):
开机时问一次格式,挑好对应的「画家」子类
→ 之后 pixel_writer->Write() 里一个分支都没有
→ 50 万次分支,坍缩成 switch 一次的 1 次
图说:判断从「每次画点」挪到「创建画家那一刻」。源码行数变多了,
但作者说「我们希望程序在语义上是简洁的」,宁愿多花一页纸展示它。2. 顶层全景
4.1 make:一条 make 命令替代手敲 clang++/ld.lld(依赖递归 + 模式规则 %.o)
4.2 像素格式:FrameBufferConfig 结构体 + WritePixel(x, y, 颜色)
位置公式:pixel_position = 每行像素数 × y + x;每像素 4 字节
4.3 C++ 化:PixelWriter 纯虚接口 → 两个子类 → 带参数的 new(没有内存管理也能用类)
4.4 vtable:父类指针调用子类函数的查表机制,每个类一张表
4.5 修加载器:ELF 程序头/LOAD 段/.bss → CalcLoadAddressRange 按「内存需求」分配
图说:前三节是「把工具磨好」,4.5 是「磨刀时发现刀柄裂了」——
这个 bug 是全书第一次「书上之前写的代码是错的」。3. 核心原理
3.1 make:把编译变成声明
make = make 命令 + Makefile(一组规则)。规则三要素:目标 : 前提条件,下一行(制表符开头)是方法。 核心递归(一层套一层地往下处理)逻辑只有一句:目标比前提新就什么都不做,否则执行方法——所以改一个 .cpp 只会重编译它1。
| 概念 | 一句话 |
|---|---|
| 假目标(.PHONY) | all、clean 不是真文件,只是规则名 |
模式规则 %.o | 一个规则管所有 .cpp→.o,% 匹配的部分叫词干 |
| 自动变量 | $< 第一个前提、$^ 全部前提、$@ 目标 |
| Makefile 当前提 | 改了 Makefile 本身也要触发重编译 |
作者的态度很松弛:「要阅读本书的其余部分,并不需要完全理解它……不理解也不要停下」;
真出问题了还有 make -B(全部重来)2。
3.2 像素格式:一个像素 = 4 字节的颜色配方
屏幕每个像素是三原色(红绿蓝,RGB)光的混合,每种颜色的强度叫色阶。
UEFI 定义了四种像素格式,本书只支持前两种:
PixelRGBResv8BitPerColor 和 PixelBGRResv8BitPerColor——
每种颜色 8 位(256 阶),一个像素 4 字节,差别只在排列顺序(RGB+保留 / BGR+保留)3。
保留区纯属对齐:三原色只有 24 位,凑满 32 位好算地址4。
写点的全部数学就两行:pixel_position = 每行像素数 × y + x(每行像素数含右侧不显示的边距),
字节地址 = 帧缓冲区首地址 + 4 × pixel_position;然后按格式把 r/g/b 依次写进 3 个字节5。
引导加载器这边,把 GOP 信息复制进 FrameBufferConfig 结构体,传结构体指针给 KernelMain() 的第一个参数
——上一章传两个裸整数,今后信息越来越多,结构体是唯一的活法6。
3.3 类与纯虚函数:把判断挪出热路径
类粗略说就是「带操作函数的结构体」,核心思想是接口与实现分离:对外只暴露 Write(), 内部怎么写帧缓冲区是私事7。设计三层:
- PixelWriter(父类):
virtual int Write(x, y, c) = 0是纯虚函数——=0表示没实现, 只是「返回值、参数、名字」的约定,可以有无限多个实现并动态切换8; - RGBResv8Bit… / BGRResv8Bit…(子类):继承父类,各自覆盖(override) Write();
- 构造函数把配置信息存进成员变量 config_,以后画点不用每次传配置9。
子类没写构造函数,using PixelWriter::PixelWriter; 一行让子类直接沿用父类的构造函数10。
没有操作系统,new 怎么用? 普通 new 会向操作系统要内存——可操作系统还没出生。
答案是带参数的 new(placement new):不分配内存,在你指定的内存区域上构造实例
(数组是你自己开的,构造由 new 负责)。定义它只需几行:operator new 收到指针就原样返回,
真正的魔法是编译器看到 new 会自动插入「调用构造函数」的指令——因为构造函数程序员不能手动调,
创建类实例的唯一办法就是用 new11。配套的 operator delete 是为了不让链接报错,
「C++ 真难啊」——作者的原话12。
专栏 4.2 还送了一个态度:编译器错误是你的朋友。报错里有文件名、行列号, 模式是固定的;读懂它们比运行时崩了再查便宜得多13。
3.4 vtable:父类指针怎么调到子类函数
把子类实例的指针塞进父类类型的变量,是合法操作;通过父类指针调用 Write() 时, 运行时得知道「这个实例到底是哪个子类」——编译器的答案叫 vtable(虚函数表):
- 每个类一张表(不是每个实例),列出自己的虚函数都指向哪份实现;
- 含虚函数的实例开头藏一个指向本类 vtable 的指针(程序员摸不到,编译器调用函数时引用它);
- 父类里没加 virtual 的函数不进表,子类写了同名同参也不算覆盖14。
书里给的实例:Base* p = new Sub; 调 Func()(非虚)→ Base::Func;调 VFunc1()(虚)→ Sub::VFunc1; VFunc2 子类没覆盖 → 表里仍是 Base::VFunc215。这就是为什么覆盖仅限虚函数、 为什么 override 关键字是好东西(写错名字编译期就报错,而不是悄悄变成新函数)16。
3.5 修加载器:ELF 程序头与 .bss 的陷阱
绿方块跑出来了,但书里主动坦白:「内核加载过程中存在一个错误……计算内存大小的过程是错误的」 ——第 3 章按文件大小分配内存,而 ELF 里有个段,文件大小 ≠ 内存大小17。
ELF 文件的结构:文件头(整个文件的信息)→ 程序头(加载器要看的)→ 节体 → 节头(链接器看的)。 程序头里 LOAD 段告诉加载器:这段数据在文件哪个偏移、多大,要放到内存哪个地址、占多大, 标志 R/W/E 是干嘛的(先不管)18。作者的 kernel.elf 有三个 LOAD 段,第三个文件大小 0、内存大小 0x18—— 因为那是 .bss 段:放「没有初始值的全局变量」,既然没初值,文件里就不用记内容, 但程序跑起来它们必须有内存19。pixel_writer_buf 这种全局数组就住在 .bss—— 上一节的 C++ 新功能恰好把这个休眠 bug 激活了。
修复策略三步:① 把 kernel.elf 读到临时区;② 遍历程序头,用 CalcLoadAddressRange() 找出所有 LOAD 段的地址范围(最小起始、最大终止);③ 按这个范围分配最终内存,逐段复制,释放临时区20。 顺手还讲了「求 N 个数最小值」的通用算法(拿一个足够大的值初始化,逐个取小)21。
4. 作者的判断与证据
判断:语义简洁比源码行数重要。 类的版本行数更多,但作者「不厌其烦地用了一页来展示」—— 50 万次分支变 1 次,是运行时的事,值得22。
判断:C++ 值得,哪怕是「没有操作系统的 C++」。 全章其实是一次论证:placement new 补上内存管理缺位, 虚函数补上 if-else,override 补上笔误——每个语言特性都对应一个裸机开发的真实痛点。
证据: 4.5 的 bug 是「自查式」证据——不是靠崩溃报告,而是作者重读自己的加载逻辑时 意识到「文件大小 ≠ 内存需求」,再用 readelf -l 把三个 LOAD 段摆出来给你看23。 这种「作者亲手拆自己台的诚实」,是这本书可信度的来源之一。
5. 边界与局限
- 只支持两种像素格式:PixelBitMask(格式复杂)和 PixelBltOnly(只能整屏拷贝)被明确跳过24; 真机上 GOP 几乎总是给前两种之一,所以侥幸够用,但不是规范保证。
- vtable 一节讲的是 Clang 的实现:C++ 标准只规定行为,不规定查表;别的编译器可以别的方式实现25。
- .bss 的坑只露出冰山一角:第 19 章加载「应用」时还会回到 ELF 程序头,那时才是完全体。
6. 可带走的
本章主线例子,一行写完: 按格式选子类 → 50 万次 if 变 1 次 switch → 绿色矩形上屏;加载器按 LOAD 段内存范围分配 → .bss 陷阱填平。
- make 的心脏一句话:目标比前提新就跳过,否则执行方法;模式规则
%.o管所有源文件; - 像素 = 4 字节(RGB 各 8 位 + 保留),地址 = 首地址 + 4×(行宽×y + x);行宽含不显示的边距;
- 跨模块传信息,结构体指针是唯一的活法——裸整数参数很快会不够用;
- 纯虚函数 = 只定接口不给实现;override 关键字把「写错函数名」从运行时炸弹变成编译期错误;
- 没有内存管理也能用类:placement new 在你自己的数组上构造实例;
- 虚函数靠 vtable(每类一张表、实例头里藏指针)实现运行时选择;非虚函数不进表,谈不上覆盖;
- ELF 程序头的 LOAD 段才是「要占多少内存」的真相;.bss 文件里 0 字节、内存里必须有地儿;
- 「最小值算法」这类通用小套路,书里也不省略——跟着写一遍就是自己的。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| make 规则与递归执行 | 4.1 make简介(osbook_day04a) | text/35-ch04-01-4-1-make-osbook-day04a.txt:4(搜「自动执行编译和链接」) · text/35-ch04-01-4-1-make-osbook-day04a.txt:27(搜「前提条件」) |
| 假目标、模式规则、自动变量 | 4.1 make简介(osbook_day04a) | text/35-ch04-01-4-1-make-osbook-day04a.txt:45(搜「phony」) · text/35-ch04-01-4-1-make-osbook-day04a.txt:79(搜「模式规则」) · text/35-ch04-01-4-1-make-osbook-day04a.txt:82(搜「$<」) |
| 不理解也继续、make -B | 4.1 make简介(osbook_day04a) | text/35-ch04-01-4-1-make-osbook-day04a.txt:75(搜「不理解也不要停下」) |
| 像素格式四选一、只支持两种 | 4.2 自由绘制像素(osbook_day04b) | text/36-ch04-02-4-2-osbook-day04b.txt:22(搜「四种像素数据格式」) · text/36-ch04-02-4-2-osbook-day04b.txt:48(搜「本书只支持」) |
| WritePixel 与位置公式 | 4.2 自由绘制像素(osbook_day04b) | text/36-ch04-02-4-2-osbook-day04b.txt:78(搜「WritePixel」) · text/36-ch04-02-4-2-osbook-day04b.txt:99(搜「pixel_position」) |
| 保留区与 4 字节像素 | 4.2 自由绘制像素(osbook_day04b) | text/36-ch04-02-4-2-osbook-day04b.txt:124(搜「保留区」) |
| 类 = 接口与实现分离 | 4.3 利用C++语言特性重写程序(osbook_day04c) | text/37-ch04-03-4-3-c-osbook-day04c.txt:7(搜「数据及其操作的组合」) |
| 纯虚函数 =0 | 4.3 利用C++语言特性重写程序(osbook_day04c) | text/37-ch04-03-4-3-c-osbook-day04c.txt:14(搜「纯虚函数」) |
| 覆盖、override、using 构造 | 4.3 利用C++语言特性重写程序(osbook_day04c) | text/37-ch04-03-4-3-c-osbook-day04c.txt:49(搜「覆盖」) · text/37-ch04-03-4-3-c-osbook-day04c.txt:56(搜「using」) |
| 普通 new 与带参数的 new | 4.3 利用C++语言特性重写程序(osbook_day04c) | text/37-ch04-03-4-3-c-osbook-day04c.txt:71(搜「堆」) · text/37-ch04-03-4-3-c-osbook-day04c.txt:88(搜「带参数的new」) |
| 编译器错误是朋友 | 4.3 利用C++语言特性重写程序(osbook_day04c) | text/38-ch04-04-4-4-vtable.txt:3(搜「错误信息的第一行」;专栏4.2) |
| vtable 定义与每类一张表 | 4.4 vtable | text/38-ch04-04-4-4-vtable.txt:30(搜「虚函数」) · text/38-ch04-04-4-4-vtable.txt:30(搜「每个类都有一个vtable」) |
| 实例开头的 vtable 指针 | 4.4 vtable | text/38-ch04-04-4-4-vtable.txt:59(搜「嵌入」) |
| 非虚函数不进表、调用对照 | 4.4 vtable | text/38-ch04-04-4-4-vtable.txt:44(搜「覆盖仅适用于虚函数」) · text/38-ch04-04-4-4-vtable.txt:66(搜「Base* base_ptr」) |
| 加载 bug 与 ELF 程序头 | 4.5 改进加载器(osbook_day04d) | text/39-ch04-05-4-5-osbook-day04d.txt:2(搜「存在一个错误」) · text/39-ch04-05-4-5-osbook-day04d.txt:20(搜「段是ELF文件的一部分」) |
| 三个 LOAD 段与 .bss | 4.5 改进加载器(osbook_day04d) | text/39-ch04-05-4-5-osbook-day04d.txt:32(搜「LOAD段的信息」) · text/39-ch04-05-4-5-osbook-day04d.txt:41(搜「.bss」) |
| 修复三步与 CalcLoadAddressRange | 4.5 改进加载器(osbook_day04d) | text/39-ch04-05-4-5-osbook-day04d.txt:91(搜「修改策略」) · text/39-ch04-05-4-5-osbook-day04d.txt:120(搜「CalcLoadAddressRange」) |
Footnotes
-
出处:「4.1 make简介」(
text/35-ch04-01-4-1-make-osbook-day04a.txt:27,搜「前提条件」;:55,搜「如果目标比必填字段新」)。 ↩ -
出处:「4.1 make简介」(
text/35-ch04-01-4-1-make-osbook-day04a.txt:75,搜「不理解也不要停下」;:76,搜「make -B」)。 ↩ -
出处:「4.2 自由绘制像素」(
text/36-ch04-02-4-2-osbook-day04b.txt:22,搜「四种像素数据格式」;:48,搜「本书只支持」)。 ↩ -
出处:「4.2 自由绘制像素」(
text/36-ch04-02-4-2-osbook-day04b.txt:124,搜「保留区」)。 ↩ -
出处:「4.2 自由绘制像素」(
text/36-ch04-02-4-2-osbook-day04b.txt:81,搜「pixels_per_scan_line」;:99,搜「含边距水平方向的像素数」)。 ↩ -
出处:「4.2 自由绘制像素」(
text/36-ch04-02-4-2-osbook-day04b.txt:51,搜「从UEFI的GOP中获取的信息复制到之前创建」))。 ↩ -
出处:「4.3 利用C++语言特性重写程序」(
text/37-ch04-03-4-3-c-osbook-day04c.txt:7,搜「数据及其操作的组合」;:10,搜「模块化的基本思想」)。 ↩ -
出处:「4.3 利用C++语言特性重写程序」(
text/37-ch04-03-4-3-c-osbook-day04c.txt:14,搜「纯虚函数的特点」)。 ↩ -
出处:「4.3 利用C++语言特性重写程序」(
text/37-ch04-03-4-3-c-osbook-day04c.txt:27,搜「构造函数」)。 ↩ -
出处:「4.3 利用C++语言特性重写程序」(
text/37-ch04-03-4-3-c-osbook-day04c.txt:56,搜「using」)。 ↩ -
出处:「4.3 利用C++语言特性重写程序」(
text/37-ch04-03-4-3-c-osbook-day04c.txt:71,搜「堆」;:75,搜「是否调用类的构造函数」;:88,搜「带参数的new」;:97,搜「operator」)。 ↩ -
出处:「4.3 利用C++语言特性重写程序」(
text/37-ch04-03-4-3-c-osbook-day04c.txt:104,搜「链接时」;text/37-ch04-03-4-3-c-osbook-day04c.txt:113(搜「C++ 難しい」——日文残页)。 ↩ -
出处:「4.3」专栏4.2(
text/38-ch04-04-4-4-vtable.txt:1起,搜「错误信息的第一行」;:19,搜「强大的盟友」)。 ↩ -
出处:「4.4 vtable」(
text/38-ch04-04-4-4-vtable.txt:29,搜「vtable」;:40,搜「virtual关键字」;:59,搜「嵌入」)。 ↩ -
出处:「4.4 vtable」(
text/38-ch04-04-4-4-vtable.txt:66,搜「Base* base_ptr」)。 ↩ -
出处:「4.4 vtable」(
text/38-ch04-04-4-4-vtable.txt:44,搜「覆盖仅适用于虚函数」)与「4.3」(text/37-ch04-03-4-3-c-osbook-day04c.txt:51,搜「override关键字」)。 ↩ -
出处:「4.5 改进加载器」(
text/39-ch04-05-4-5-osbook-day04d.txt:2,搜「存在一个错误」;:4,搜「程序头」)。 ↩ -
出处:「4.5 改进加载器」(
text/39-ch04-05-4-5-osbook-day04d.txt:20,搜「段是ELF文件的一部分」;:23,搜「LOAD段」;:29,搜「R=Readable」)。 ↩ -
出处:「4.5 改进加载器」(
text/39-ch04-05-4-5-osbook-day04d.txt:41,搜「.bss部分」;:43,搜「文件中的大小为0」)。 ↩ -
出处:「4.5 改进加载器」(
text/39-ch04-05-4-5-osbook-day04d.txt:91,搜「修改策略」;:111,搜「最终目的地」)。 ↩ -
出处:「4.5 改进加载器」(
text/39-ch04-05-4-5-osbook-day04d.txt:133,搜「最小值」)。 ↩ -
出处:「4.3 利用C++语言特性重写程序」(
text/37-ch04-03-4-3-c-osbook-day04c.txt:127,搜「没有条件分支」;:130,搜「语义上是简洁的」)。 ↩ -
出处:「4.5 改进加载器」(
text/39-ch04-05-4-5-osbook-day04d.txt:6,搜「readelf -l」)。 ↩ -
出处:「4.2 自由绘制像素」(
text/36-ch04-02-4-2-osbook-day04b.txt:48,搜「PixelBitMask」;:49,搜「PixelBltOnly」)。 ↩ -
出处:「4.4 vtable」(
text/38-ch04-04-4-4-vtable.txt:28,搜「实现方法由编译器决」))。 ↩