从0写OS内核-2-写一个最小的 linker.ld
这一步,我们要从"无形"变"有形":让你写出第一个可被链接器理解的"内存版图",并且用它生成一个最小的可执行文件。你将认识 linker.ld 的基本结构,明确入口、段与地址布局,建立起对"镜像形状"的直觉。更重要的是,完成本篇后,你的工程就不再是散落的源文件,而是一个可以被分析、被加载的实体。
增量目标
-
写出可用的最小
linker.ld,把入口、段与地址布局明确下来 -
用它链接一个最小目标文件,产出
kernel可执行,并用工具检查段布局 -
为后续的引导与打印留下稳定的基础
环境准备
-
建议系统:WSL/Ubuntu 或任何 Linux 发行版
-
安装依赖:
sudo apt-get install build-essential nasm qemu-system-i386 grub-pc-bin xorriso mtools cmake -
目录约定:顶层目录使用
myos/
本篇新增文件
-
myos/linker.ld:链接脚本,定义入口与段布局 -
myos/stub.asm:一个最小的目标文件,用来验证链接是否成功(下一篇我们会替换为真正的引导代码)
目录结构(完成后)
myos/
- linker.ld
- stub.asm
完整代码
ENTRY(_start)
SECTIONS {
. = 1M;
.text ALIGN(4K) : { *(.multiboot) *(.text*) }
.rodata ALIGN(4K) : { *(.rodata*) }
.data ALIGN(4K) : { *(.data*) }
.bss ALIGN(4K) : { *(.bss*) *(COMMON) }
}
; myos/stub.asm
[BITS 32]
SECTION .text
global _start
_start:
jmp $
构建与验证
-
汇编:
nasm -f elf32 -o build/stub.o myos/stub.asm -
链接:
ld -nostdlib -T myos/linker.ld -o build/kernel build/stub.o -
查看布局:
readelf -h build/kernel && readelf -S build/kernel -
观察:
-
入口应为
_start -
.text/.rodata/.data/.bss段存在且按 4K 对齐 -
虚拟地址起始约为
0x0010_0000(1MB)
为什么从 1MB 开始
-
历史与兼容性:早期实模式与 BIOS 使用的低内存区域复杂而多变;从 1MB 开始能避开大部分兼容性问题
-
后续分页与内核高半区设计更直观:你可以把内核放在一个"干净"的高地址起点,再逐步映射用户空间与设备内存
多引导头预留位(提前铺路)
-
我们在
.text段里预留*(.multiboot),即将在下一篇加入多引导头,这让 GRUB 能识别你的镜像是一个"多引导内核" -
这种"先留位置再填内容"的策略能让你避免后续段重排带来的不必要麻烦
段与对齐
-
.text:代码与常量的执行主体,必须对齐到页边界以便分页映射与性能考虑 -
.rodata:只读数据,通常包含字符串与常量表 -
.data:可写的已初始化数据,反映程序的状态 -
.bss:未初始化数据,由链接器"占位",在运行时被清零 -
4K 对齐是面向分页的默认策略;我们也可以根据需要调整到更大的粒度,但早期保持 4K 更通用
最小可执行并不等于可引导
-
你现在得到的是一个"形状正确"的 ELF 可执行文件,但它还不能在 QEMU 或真机中直接引导
-
引导涉及 BIOS/UEFI、引导加载器(例如 GRUB)、内核入口约定与协议(例如 Multiboot),我们将在下一篇逐步把入口与协议补齐
失败排查
-
链接失败:检查
ld命令中的-T linker.ld路径是否正确;确保stub.asm已经以elf32目标格式在 32 位模式下编译 -
段缺失或未对齐:用
readelf -S build/kernel检查每个段的对齐(AddrAlign)是否为 0x1000;若不是,检查ALIGN(4K)是否正确写入并被识别 -
入口不是
_start:检查ENTRY(_start)是否在脚本第一行;确保stub.asm中定义了global _start
可观测性从第一天开始
-
即使现在我们还不打印任何字符,你仍旧可以"观察到"镜像形状:这就是可观测性的起点
-
任何一步的改变,都应该通过工具验证其结果是否符合预期:
readelf/objdump/file
深入理解:链接脚本的影响力
-
它定义了你的内核"住在哪里",以及各个段之间如何排列;这对后续的分页、异常处理、栈与堆的规划都有深远影响
-
入口位置决定了引导加载器把控制权交给谁;这在后续将直接关联到多引导规范、寄存器约定与堆栈初始化
-
段的对齐关系会影响指令取址与数据访问的性能,以及分页表如何组织映射
进阶阅读建议
-
System V ABI 中对 ELF 格式的定义
-
GNU ld 链接脚本官方文档:段、符号、表达式与控制语句
-
Multiboot 规范:内核如何声明自己,以便引导加载器识别
下一步扩展(为第3篇预告)
-
在
.multiboot段写入多引导头;在.text段提供一个真正的入口_start -
用最小汇编代码把"Hello"写到 VGA 文本缓冲区,确认我们有能力"看见"
-
准备把
CMakeLists.txt引入工程,让构建与链接成为标准化流程
思考题(建议动手)
-
如果把起始地址从 1MB 改到 2MB,会发生什么?你能用
readelf观察到哪些变化? -
去掉
.multiboot的预留,你后续要在链接脚本里做哪些额外修改来保证多引导头的位置正确? -
如果你未来想把
.text放到更高的地址(例如 16MB),你会如何调整脚本并验证?
里程碑感知
-
你已经把"形无"的源文件变成了"形有"的可执行,理解了入口、段与地址的基本概念
-
接下来,我们会在这块地基上竖起第一面旗帜:一个属于你的
Hello,它将从屏幕左上角出现