资讯动态

系统引导与程序装载:从BIOS到ELF的完整链路实现

发布时间:2026/10/14 12:26:29 来源:尧图企业网站定制
最近又有人来问某高校操作系统课的第三次作业——“系统引导与程序装载”。这一题可以说是把操作系统里最底层的两件事合在一起了机器通电之后第一条指令从哪里来可执行文件又是怎么从磁盘上“站”进内存、变成可以运行的程序。这两件事平时上课讲得挺抽象一旦放到作业里自己动手写很多人才发现原来启动过程这么脆弱早几年的电脑上“开机”两个字背后藏着那么多条指令。这篇东西会把这次作业从原理到实操完整拆一遍引导扇区怎么写、为什么要切保护模式、ELF 文件怎么解析、怎么把用户程序装载到内存再跳过去执行。适合正在被这门课折磨的同学也适合想补齐“从上电到程序跑起来”这段知识盲区的人。如果你只是想应付作业直接抄步骤也能过但建议还是把背后逻辑啃一啃这部分知识面试时是真的会被问到。1. 这题到底想让你写什么1.1 交付物拆解引导扇区、加载器、用户程序先说结论这类作业无论题目表述怎么变最后要交的东西基本都是三样一个引导扇区boot sector写在虚拟软盘或硬盘的第一个扇区里开机后被 BIOS 自动加载到内存运行一个装载器loader负责读取磁盘上的用户程序、解析它的格式、把它放到正确的内存位置一个用户程序通常是用汇编或 C 写的一个小功能比如打印一串字符、做个简单计算加载成功后能跑起来并返回结果。有人会把引导扇区和装载器写成同一个程序这没问题只要你能在 512 字节内塞下读盘逻辑和解析逻辑那算你真本事。实际做的时候最常见的划分是引导扇区只负责从磁盘读取一个更大的二级 loader二级 loader 再负责解析用户程序文件。这个分层的思路和真实操作系统里“bootloader - kernel - init”的结构是一样的作业虽然小但框架是正统的。1.2 为什么是第三次作业承上启下操作系统课的前两次作业通常围绕进程或线程、内存分配这些“操作系统内部视角”的题目。第三次突然变成“系统引导与程序装载”从课程设计角度看是为了把前面学的抽象概念落地你分配的内存地址最终要怎么和可执行文件的地址对应起来你学的“进程地址空间”在程序还没运行之前到底是谁把它搭起来的装载器就是干这个活的人。理解装载器比背十遍“进程地址空间”的概念都管用。而引导过程则是另一条线它让你看到操作系统是怎么从“机器什么都没有”的状态下被唤醒的。这两条线接在一起正好覆盖了一个程序从磁盘到 CPU 执行的完整链条。课程把它放在期中前后就是想让你在学完内存管理和进程之后有一个直观的全程认识。后面再学文件系统、设备驱动你会发现所有东西都挂在同一条启动链路上。2. 引导扇区从 0x7C00 开始的第一段代码2.1 启动流程决定了你只有 512 字节写引导扇区之前必须对启动流程有一个清晰的图景。简化地说传统 BIOS 启动时是这样的上电后 CPU 进入实模式第一条指令从 0xFFFF0 取到这个地址位于 BIOS ROM 中BIOS 完成自检后按照启动顺序把启动设备的第一个扇区读进内存放在物理地址 0x7C00然后跳转到那里执行。也就是说你的代码不是从 0x0 开始的而是从 0x7C00 开始的。这个地址是历史原因加兼容性要求共同决定的现在所有模拟器、真机都默认这一套。引导扇区的大小也只有 512 字节扇区最后一个双字节必须是0x55AA否则 BIOS 不认为这是一个可启动扇区。所以我常跟人说引导扇区不是“写代码”更像是在“一个 512 字节的盒子里做装修”。代码、数据、栈全挤在这一个扇区里你的汇编器如果默认生成段地址不是 0x7C00会出现难以捉摸的乱码现象。最保险的做法是在代码第一行写明[org 0x7C00]这句指令告诉汇编器我们这个程序被加载到 0x7C00所有内部地址都按这个基址来计算。漏了这句后面的mov si, msg里msg的地址就会变成相对 0 的偏移打印出来全是乱码。2.2 手工写一个可打印可读盘的引导扇区这里给一个最精简但五脏俱全的引导扇区。它做三件事在屏幕上打印一行提示从启动盘读取后续扇区到内存 0x1000然后跳过去执行。[org 0x7C00] bits 16 start: ; 初始化数据段和栈 mov ax, 0 mov ds, ax mov ss, ax mov sp, 0x7C00 ; 打印字符串 mov si, boot_msg print: lodsb test al, al jz load_kernel mov ah, 0x0E int 0x10 jmp print load_kernel: ; 读磁盘扇区到 0x1000 mov ah, 0x02 ; BIOS 读扇区功能号 mov al, 64 ; 要读64个扇区 mov ch, 0 ; 柱面0 mov cl, 2 ; 从2号扇区开始1号是引导扇区 mov dh, 0 ; 磁头0 mov dl, 0x80 ; 硬盘0 mov bx, 0x1000 ; 目标内存地址 int 0x13 jc disk_error ; 跳到加载进来的代码 jmp 0x1000 disk_error: mov si, err_msg err_print: lodsb test al, al jz hang mov ah, 0x0E int 0x10 jmp err_print hang: cli hlt jmp hang boot_msg db Booting..., 0 err_msg db Disk read error!, 0 times 510-($-$$) db 0 dw 0x55AA注意几个关键点int 0x13的ah0x02是 BIOS 提供的读扇区功能它工作在实模式下一次能读多少取决于寄存器。al是扇区数这里读 64 个扇区也就是 32KB 内容这个空间足够放一个简单的 loader 和用户程序了。目标地址放 0x1000是故意避开 0x0000 到 0x0FFF 这段中断向量区和 BIOS 数据区。如果在 Windows/Linux 的模拟器上把第二扇区以后的内容写进磁盘镜像dl0x80表示第一块硬盘用虚拟软盘的话要改成dl0x00这个细节非常容易漏漏了读出来的数据全错。还有一个容易忽略的点jmp 0x1000是无条件直接跳转到绝对地址 0x1000。这句执行后CPU 会把 CS 设为 0IP 设为 0x1000。但如果你在前面没有显式初始化段寄存器那么 CS 的值可能是别的这句远跳转也会带偏你。所以务必在初始化阶段把ds/es/ss都清零保持段基址一致。2.3 引导扇区常见的三个“低级错误”第一个错误是文件不足 512 字节就交了。引导扇区必须在磁盘第一个扇区上恰好 512 字节多一字节不行少一字节也不行。少了的话模拟器可能自动补零但真机不一定会买账。所以汇编文件结尾一定要用times 510-($-$$) db 0 dw 0x55AA$-$$表示当前指令相对文件起始的偏移times会把剩余的空位全部填零最后写上启动标志。第二个错是org和实际加载地址不匹配。如果虚拟机加载到 0x7C00你声明org 0那么所有内部访问都会偏移 0x7C00直接跑飞。反过来org 0x7C00但你实际上是用模拟器的-fda参数加载到软盘地址同样是 0x7C00那就没问题。这个地址不能假设最好在代码开头就用调试器确认一遍。第三个错是覆盖了关键内存区域。比如把代码加载到 0x0 到 0x3FF 这段区域那正好覆盖了中断向量表BIOS 的int 0x10、int 0x13可能直接失效。还有把栈设在 0x7C00 以下但不小心和加载目标区域重合也会导致数据被覆盖。写引导扇区时内存布局先画好就像搬家前先规划每个房间放什么不然跑起来一堆诡异的“抖动”。3. 实模式与保护模式的切换3.1 不切保护模式行不行有些人贪省事引导扇区加载完用户程序直接就在实模式下跳过去跑。实模式下所有代码默认是 16 位访问内存靠段:偏移合成物理地址时左移四位。一个段最大 64KB超过 1MB 的内存访问需要特殊处理。你如果只是想打印个字符确实可以不切。但作业既然叫“系统引导与程序装载”很多评分标准里就明确要求有实模式到保护模式的切换过程这不仅是考察 GDT 原理也为后面装载 ELF 做准备。现代操作系统的可执行程序比如你写一个 C 程序编译器默认生成 32 位或 64 位指令入口地址经常在 0x08048000 这种高地址。你要在实模式下用真实地址去访问它一是访问不到二是用户程序用的栈、数据段、代码段全是平坦的内存模型。所以不切保护模式你后面连 ELF 的代码段都执行不了。你说“我作业里的用户程序用汇编写成 16 位不切也能跑啊。”那你赢了但这是绕开了题目本意。切保护模式这个步骤本质上就是在告诉你操作系统要从实模式的“16位分段寻址”过渡到保护模式的“平坦寻址权限控制”这不是没事找事而是必须迈过的一道坎。3.2 最小 GDT 搭建细节保护模式下的内存访问不再靠“段寄存器左移四位”来合成地址而是通过段寄存器里的选择子去查全局描述符表GDT。要进入保护模式你得先造一张 GDT。一个能跑的最简 GDT 只需要三个描述符一个空描述符、一个代码段描述符、一个数据段描述符。代码段和数据段都可以覆盖整个 4GB 线性空间也就是基地址 0、段界限 0xFFFFF4KB 粒度下对应 4GB。代码写法并不复杂gdt_start: dq 0 ; 空描述符 gdt_code: dw 0xFFFF ; 段界限 0-15 位 dw 0x0000 ; 基地址 0-15 位 db 0x00 ; 基地址 16-23 位 db 0x9A ; 存在位1DPL0代码段非一致可读 db 0xCF ; 粒度为4KB32位模式上限 0xFFFFF db 0x00 ; 基地址 24-31 位 gdt_data: dw 0xFFFF dw 0x0000 db 0x00 db 0x92 ; 数据段可读写 db 0xCF db 0x00 gdt_end: gdt_ptr: dw gdt_end - gdt_start - 1 dd gdt_start注意这里的dd gdt_start在 16 位汇编里会被解析为 32 位地址因为lgdt指令需要的是线性地址而不是段相对地址。如果你用org 0x7C00那么gdt_start的地址是0x7C00 偏移这是对的。但如果你的引导扇区被加载到别的地址这里的地址就要相应变化。进入保护模式的经典三部曲是cli ; 关中断 lgdt [gdt_ptr] ; 加载 GDT mov eax, cr0 or eax, 1 mov cr0, eax ; 打开保护模式开关 jmp 0x08:flush_pm ; 远跳到代码段选择子 0x08那个jmp 0x08:flush_pm是一个远跳转0x08是代码段选择子对应 GDT 里第二个描述符。远跳转有两个作用一是刷新 CPU 流水线里预取的模式切换前指令二是把 CS 段寄存器更新为保护模式下的代码段选择子。少了这一步后面执行第一条保护模式指令就可能异常。3.3 切换保护模式后必须马上处理的三件事进入保护模式不是终点后面还有三件事要做否则程序活不过三秒。第一件事重新初始化所有段寄存器。cli后立即执行mov ax, 0x10; mov ds, ax; mov es, ax; mov ss, ax这里0x10是数据段选择子。不重新设置ds你后面访问内存用的还是实模式下的段值保护模式下这个值的含义完全不同大概率立即异常。第二件事设置栈指针。实模式下栈顶常设在 0x7C00 附近进入保护模式后栈可以重新设到一个高位地址比如mov esp, 0x90000。注意栈是向下增长的别把栈设在代码段可能覆盖的区域。要是栈设得太低函数调用时直接压进代码区调试起来那叫一个惨。第三件事重新加载一个保护模式适用的中断描述符表IDT。切换后 BIOS 中断都不可用如果你还需要键盘输入或输出就要自己接管硬件中断或直接操作显存。很多作业在保护模式阶段为了避免麻烦干脆直接往地址 0xB8000 写显存来打印字符这样既不依赖中断又能在屏幕上看到输出。我见过一个特别典型的翻车现场代码明明打印出了第一行“Entering protected mode”然后立刻死机。后来查出来是 GDT 的gdt_ptr里的表长度算错了。长度应该是gdt_end - gdt_start - 1有人写成了gdt_end - gdt_start多了一个字节也能跑但描述符边界错位到代码段访问时就会异常。“看起来就差一个字节”的错误调试起来特别费时间。4. 程序装载的核心解析 ELF4.1 装载器到底在做什么先说清楚一个概念装载器不是把文件 copy 到内存就完事。它要解决几个问题用户程序是哪种可执行格式格式里有哪些段每个段要放在内存的什么位置哪些段需要从磁盘读内容哪些段只需要在内存里清零程序入口地址在哪数据段、代码段的权限分别是什么作业里最常用的可执行文件格式是 ELF也就是 Linux 下的标准格式。如果你在 Linux 上用gcc编译一个 C 小程序生成的默认就是 ELF。ELF 设计得比当年 DOS 的EXE复杂不少但你只需要关心两个结构ELF 文件头ELF header和程序头表program header table。用 C 写装载器是最舒服的因为 C 的结构体可以直接对应 ELF 的二进制布局。你只需要读文件开头固定字节数的 ELF 头校验魔数、架构、位宽根据 ELF 头里的e_phoff和e_phnum读程序头表遍历每个程序头找到p_type PT_LOAD的段根据段的p_offset、p_filesz、p_memsz、p_vaddr决定拷贝多少、清零多少。4.2 ELF 头与程序头逐项拆解ELF 头位于文件最开头64 位格式下固定 64 字节。你要看的关键字段包括e_ident[0]魔数一般是0x7F E L Fe_machine架构i386 是 3x86-64 是 62e_phoff程序头表在文件中的偏移e_phentsize每个程序头的大小e_phnum程序头的数量e_entry程序入口地址。这里最容易踩的坑是e_phentsize不是sizeof(Elf32_Phdr)虽然多数情况下相等但你的装载器不能假设编译平台和目标可执行文件一致。应该显式把e_phentsize当作输入用e_phoff i * e_phentsize去定位每一个程序头。程序头是一个更细的结构重点字段是字段含义常见值p_type段类型1 表示 PT_LOAD也就是要加载的段p_offset段内容在文件中的偏移通常是 0x1000 的倍数p_vaddr段第一个字节要放到的虚拟地址例如 0x08048000p_filesz文件中的大小可能比 p_memsz 小p_memsz内存中的大小包含 bss 段通常比 p_filesz 大p_flags段的权限读4写2执行1你要做的就是找到所有p_type PT_LOAD的段。对一个典型的 ELF第一个 PT_LOAD 段通常是代码段权限是 RXp_filesz和p_memsz相等第二个 PT_LOAD 段通常是数据段加 bss 段权限是 RWp_memsz比p_filesz大一些。多出来的部分就是 bss 段里面是未初始化的全局变量装载时要清零。4.3 一个可运行的简易装载流程假设你已经用某种方式把用户程序文件读到了内存缓冲区file_buf里目标物理内存从LOAD_ADDR开始那么装载的核心流程用 C 写出来大概是这样#include stdint.h #include string.h typedef struct { unsigned char e_ident[16]; uint16_t e_type; uint16_t e_machine; uint32_t e_version; uint32_t e_entry; uint32_t e_phoff; uint32_t e_shoff; uint32_t e_flags; uint16_t e_ehsize; uint16_t e_phentsize; uint16_t e_phnum; } Elf32_Ehdr; typedef struct { uint32_t p_type; uint32_t p_offset; uint32_t p_vaddr; uint32_t p_paddr; uint32_t p_filesz; uint32_t p_memsz; uint32_t p_flags; uint32_t p_align; } Elf32_Phdr; void load_elf(char *file_buf, size_t file_size, uint32_t load_addr) { Elf32_Ehdr *ehdr (Elf32_Ehdr *)file_buf; if (ehdr-e_ident[0] ! 0x7F || ehdr-e_ident[1] ! E || ehdr-e_ident[2] ! L || ehdr-e_ident[3] ! F) { return; } Elf32_Phdr *phdr (Elf32_Phdr *)(file_buf ehdr-e_phoff); for (int i 0; i ehdr-e_phnum; i) { if (phdr[i].p_type ! 1) continue; // PT_LOAD uint32_t dst load_addr phdr[i].p_vaddr; memcpy((void *)dst, file_buf phdr[i].p_offset, phdr[i].p_filesz); if (phdr[i].p_memsz phdr[i].p_filesz) { memset((void *)(dst phdr[i].p_filesz), 0, phdr[i].p_memsz - phdr[i].p_filesz); } } void (*entry)(void) (void (*)(void))(load_addr ehdr-e_entry); entry(); }这个写法非常简化因为真实装载器还要考虑段对齐、物理内存分配、页表建立、权限设置。但在课程作业的裸机环境里你如果已经把内存统一映射成“从 load_addr 起的连续空间”那这种直接用p_vaddr做偏移的映射方式是能跑的。注意这里我用了一个load_addr作为基址用户程序里所有的地址都是相对这个基址加上p_vaddr。如果你的用户程序链接时用了-Ttext 0x0那么代码里所有符号地址都是从 0 开始的直接加load_addr就是最终物理地址。如果用户程序链接地址是0x08048000你想把它装到0x10000那就不能用我上面这种简单加法需要重定位或要求用户程序链接成位置无关代码PIC。最省事的做法是统一约定链接地址、虚拟地址、物理地址三者一致至少在这个作业里先保证一致。4.4 “装载”和“拷贝文件”不是一回事很多人写完装载器就奇怪为什么我明明把 ELF 文件整体 copy 到内存跳进去就崩原因很简单ELF 文件在磁盘上是“文件视图”而程序运行需要的是“内存视图”。文件里除了代码段和数据段还混着 ELF 头、程序头表、节头表以及各种调试信息。这些内容对应的内存区域可能根本不应该被映射为可执行代码。而且 ELF 的各个段是按页对齐的p_offset和p_vaddr都对齐到页边界。如果你把整个文件从偏移 0 直接搬到内存 0x1000 处那么 ELF 头所在的位置在内存视图里是文件开头的那一段处理器跳转到入口地址时正好跳到一段可能是 ELF 头或其它元数据的位置自然跑不起来。你得按“程序头”来搬只搬PT_LOAD段而且每个段搬到它该去的地址。这就像搬家时你不会把整份装修图纸也搬进客厅墙上只会把家具摆到它该在的位置。理解了这一点你才能理解为什么操作系统装载一个程序要逐段映射而不是简单“读文件到内存”。5. 地址约定与整条链路的串联5.1 一张内存布局约定表裸机环境下没有虚拟内存所有地址都是物理地址所以你得提前和用户程序约法三章。我建议在说读里用统一约定写进作业报告里也是加分项。我常用的布局是区域地址范围用途引导扇区0x7C00 - 0x7DFF启动代码二级 loader0x1000 - 0x7BFF从磁盘加载的装载器代码用户程序0x10000 - 0x1FFFF从 ELF 解析后装载的用户程序栈0x90000 向下增长保护模式下的栈显存0xB8000 - 0xBFFFF文本模式显存这个表最关键的是“不冲突”。引导扇区加载到 0x7C00二级 loader 放在 0x1000两者之间有一段空闲但这些区域本身互不覆盖。用户程序放 0x10000是在 64KB 以下之外需要保护模式才能稳定访问所以也逼着你把切换保护模式的代码放在用户程序运行之前。如果你用[org 0x1000]写二级 loader那么在引导扇区里jmp 0x1000就正好跳到 loader 开头。如果二级 loader 内部再解析 ELF 并跳转用户程序那么整个链路就是BIOS - boot sector (0x7C00) - loader (0x1000) - user program (0x10000)这条链路越短出问题的可能性越低。有些同学喜欢把 loader 塞进引导扇区硬凑 512 字节这是很好的挑战但如果你第一次做课程作业还是老老实实分层吧。调试起来清晰得多。5.2 从引导到装载再到跳转的执行顺序整个程序的执行顺序可以拆成六个阶段我在报告里也建议按这个结构来写BIOS 把引导扇区读到 0x7C00跳到 0x7C00引导扇区初始化段寄存器和栈用int 0x13把磁盘 2 号扇区开始的内容读到 0x1000引导扇区通过jmp 0x1000跳到 loaderloader 进入保护模式解析磁盘上的 ELF 用户程序装载到 0x10000设置好栈、数据段、入口地址跳转用户程序执行。这里每一步之间都有“交接”。交接最容易出问题因为两边可能对内存地址、寄存器状态、栈的位置都有自己的假设。比如 loader 跳到用户程序之前一定要把ds、es、esp全部设置为用户程序可用的状态。引导到 loader 的时候如果 loader 是 16 位代码你还需要把cs设为正确的实模式段值。一个常见的体验是前四步都正常到了跳转用户程序就崩溃。这种情况十有八九是地址假设不一致。比如你在 ELF 头里看到程序入口是 0x08048000但你实际把段搬到了 0x10000入口地址就找不到对应的数据了。解决办法就是在链接用户程序时指定加载地址比如用ld -Ttext 0x10000或者在装载器里按相对偏移重新映射入口地址。无论如何必须让“链接地址 装载地址”至少保证入口地址在装载后有内容。5.3 用户程序入口和链接地址如何对齐这里展开说说链接地址的问题。假设用户程序是一个 16 位实模式程序你直接用 nasm 编org 0x10000 bits 32 start: ; ...这样汇编期就把所有符号地址定到 0x10000 了装载时只要把代码放到 0x10000跳转就没问题。如果用户程序是 C 写的比如void main() { ... }编译时默认会生成一个链接地址很高、带 ELF 头的可执行文件。你想把它放到裸机上需要告诉链接器不要使用默认的入口地址。比较粗暴但好用的方法是gcc -m32 -ffreestanding -nostdlib -Wl,-Ttext0x10000 -Wl,--no-dynamic-linker -o user.elf user.c这样入口地址会被定位到 0x10000装载器把 PT_LOAD 段映射到 0x10000然后跳到e_entry就行。实际作业里有人用 32 位模式有人用 64 位都有可能。我一般建议第一次做这题的同学用 32 位模式因为地址布局、GDT 都短调试信息也少64 位还需要处理长模式和页表复杂度高了不少。但话说回来如果你连用户程序都还没跑通别急着上 C 语言。先用一个最简汇编程序比如打印一个字符把整条链路验证通了再把用户程序换成一个 C 写的编译器产物难度会平滑很多。这是做底层开发最通用的“增量验证”思路。6. 踩坑实录与调试方法6.1 QEMU GDB 联合调试配置裸机程序最难受的地方是没有断点没有异常日志一个段错误直接黑屏。我强烈建议学会用 QEMU 加 GDB 的组合来调试这比反复烧录真机不知道高到哪里去了。启动 QEMU 时加上-s -Sqemu-system-i386 -fda disk.img -s -S-S让 QEMU 启动后暂停在第一条指令-s开启默认端口 1234 的 GDB 调试服务。然后在另一个终端里启动 GDBgdb target remote :1234 set architecture i386因为引导代码是 16 位实模式第一次断点通常下在*0x7C00b *0x7C00 c接下来就能用si单步执行、用x/10i $eip看当前指令、用info registers看寄存器状态。痛苦的地方在于实模式下的地址不是纯eip你用x/10i $cs*16$eip才能看到当前指令。进入保护模式后GDB 对分段的理解时常会乱这时候我习惯直接把断点下在保护模式的入口地址上比如b *0x1000配合c跳过去让 GDB 重新获取上下文。如果发现某条int 0x13之后CF1那么读盘逻辑有问题可以直接看ah寄存器里的 BIOS 错误码。ah0x01是非法命令ah0x02是地址标记找不到ah0x04是目标扇区找不到。这些错误码背下来调试读盘代码能少走一半弯路。6.2 问题速查表我把带学生做这类作业时遇到的问题整理成了表按“现象 - 大概率原因 - 排查动作”的方式列出来直接对着找就行。现象大概率原因排查动作屏幕无输出引导扇区标志 0x55AA 没写对或org声明不对用xxd boot.bin看最后两字节在0x7C00下断点看第一条指令输出了第一个字符后乱码lodsb读到非字符串内容msg地址偏移不对检查[org 0x7C00]用 GDB 看si的值和访问的地址读盘后跳转死循环目标地址没代码或读取扇区数不够用objdump -d确认 loader 大小调大al扇区数int 0x13后 CF1dl设备号错误或磁盘镜像里 loader 没写进去确认使用-fda还是-hda用dd把 loader 写入正确的扇区偏移进入保护模式后 ubuntu 虚拟机重启或 GDB 报错没有远跳转刷新流水线或 GDT 长度错检查jmp 0x08:flush_pm用si单步跟进 CR0 置位后的位置装载 ELF 后跳转崩溃p_vaddr、p_offset、入口地址三者的换算错误用readelf -h和readelf -l逐个对比确认每个 PT_LOAD 段的映射用户程序能进但数据全错段描述符权限不对或数据段选择子没重新加载确认 GDT 数据段描述符设置了写权限进入保护模式后给ds/es/ss赋值对应选择子栈溢出覆盖了代码栈顶设在低地址且用户程序也在低地址把栈设到 0x90000 或更高确保和代码区、数据区隔离这张表不能代替你从头到尾看懂流程但在卡壳时能帮你快速缩小范围。我调过一个人的作业死活加载不了用户程序最后发现是他用dd写磁盘镜像时把 loader 写到第 0 扇区了覆盖了引导扇区而引导扇区自己还在那儿打印“Booting...”看起来好像一切正常但一读第二扇区就读出自己。6.3 我的一些操作习惯裸机调试有一个原则每次只改一个变量。很多同学一上来就写 500 行汇编编译错误消除后直接跑挂了也不知道哪一步出的问题。正确做法是一步一步来先写一个只打印字符的引导扇区跑通再加读盘逻辑把 loader 加载上来并跳到 loader 里打印字符跑通再加保护模式切换在保护模式下打印字符跑通再写用户程序装载器先装载一个最简的裸二进制用户程序跑通最后才正儿八经解析 ELF。每步都有一个视觉反馈点屏幕上有输出就是“活着”没输出就是“挂了”定位起来飞快。另外生成磁盘镜像用dd的时候注意convnotrunc不然第二条命令会截断文件dd ifboot.bin ofdisk.img bs512 count1 convnotrunc dd ifloader.bin ofdisk.img bs512 seek1 convnotrunc如果多写几个扇区用seek1、seek2依次指定扇区号。至于 ELF 用户程序文件怎么放进去就看作业规定了有的作业要求直接从 ELF 文件本身做软盘镜像有的要求把 ELF 放在一个固定偏移。多读几遍题目别想当然。最后再说一个小习惯写代码前先用readelf -l user.elf看一遍程序头把每个段的p_vaddr、p_filesz、p_memsz抄在纸上然后手工推算一遍装载器的目标地址。这事看起来很笨但能防止你把程序头的字段搞反。我就见过有人把p_offset当成p_vaddr用结果 ELF 头被当成代码装载跳转进去必然什么都没发生。结尾我个人做了这么多年底层相关的开发回过头看这类课程作业最大的价值不是让你背会 GDT 的结构也不是让你熟练写出int 0x13的调用参数而是逼你在一个“没有系统帮你兜底”的环境里亲手把电源按下之后到 main 函数执行之间那段路走一遍。走完这段路你再回头学虚拟内存、学进程调度会发现那些概念一下子都有了落脚的实处。如果你正在写这个作业我的建议是别急着抄网上的完整代码先把“我要在哪一步看到什么输出”写清楚然后一步一验证。这个习惯不只对这个作业有用以后做操作系统相关的课程设计、内核实验甚至参加工作后做嵌入式开发都能让你少熬几个大夜。等你的引导扇区打印出第一行字符、装载器把用户程序带起来那一刻那种成就感真比调通一个 Web 接口爽多了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑