简介《Oranges一个操作系统的实现》随书光盘面向操作系统初学者和内核开发爱好者提供与教材配套的源代码、实验环境与教学辅助材料。压缩包共2347个文件包含858个C源码、576个头文件、382个汇编文件以及244个inc配置、85个img磁盘映像、79个makefile和75个bochsrc虚拟机配置。其中C与头文件构成内核主体asm与inc负责底层引导和硬件抽象img与bochsrc支撑虚拟环境运行makefile则串联起完整构建流程。资源整体仅12.86MB覆盖引导加载、内核初始化、进程管理、内存管理和文件系统等关键模块目录按章节组织可在Bochs模拟器中直接编译运行。已有400人浏览学习。通过分析loader.asm与各模块源码、修改makefile、运行img映像读者能直观理解实模式与保护模式切换、中断处理、任务调度等核心机制对照书本逐章实践是一份难得的操作系统入门实战资料。1. Orange’s一个操作系统的实现随书光盘从引导扇区到多进程内核的实验场提到操作系统学习路径不少学习者的第一个问题是书读了不少却不知道代码在哪里跑起来。《Orange’s一个操作系统的实现》随书光盘解决的就是这件事——把完整的引导扇区、保护模式切换、进程调度与文件系统源码连同汇编器、编译器、映像制作工具和模拟调试器一起交付让你能在断点单步里把内核启动的每个细节看清楚。这套内容适合三类人准备面试、需要系统补操作系统栈的在校生嵌入式转内核开发的工程师以及读 Linux 源码读不动、想先从小内核建立整体观的爱好者。它不是看视频就算会的课程而是把“理论”落地成“可运行、可修改、可调试”的一手实验环境。2. 光盘内容盘点源码目录、工具链边界与模拟环境的取舍2.1 目录级拆解按章编号的源码与 stage 递进关系拿到光盘后第一步不是运行某个 exe而是把整张 CD 拷贝到一个可写目录这一步决定了后面所有实验能否顺利推进。原因是内核实验的前提是反复修改源码、重新编译、重做磁盘映像在只读光盘上这些操作寸步难行。我一般会新建一个~/orange目录把 CD 根目录的 src、tool、bochs 等子目录完整复制进来然后递归清理只读属性。Windows 环境下还要注意光盘文件的隐藏属性拷完用attrib -r -s -h /s /d清一遍比较稳妥。源码目录的命名非常直白Chapter2 到后面几个章节按书中教学顺序排列。第2章是纯汇编的引导扇区与加载器第3章引入保护模式第5章开始出现 C 语言实现的内核主体后面逐步加上进程、信号量、文件系统。这种递进关系同样体现在每个章内的 stage 子目录里stage1 到 stage2 就是两版代码的迭代升级。拿到源码后第一件值得做的事不是打开某个大文件通读而是先用 diff 找出相邻 stage 的差异这比直接看书更直观地展示知识点。# 对比第3章相邻两个stage的差异定位教学增量 diff -rq src/Chapter3/stage1 src/Chapter3/stage2-rq表示递归并仅列出差异文件名不展开内容。当你想知道上一版引导代码和下一版到底哪里不同时这个命令能立刻给出文件清单随后去掉-q再跑一遍可以逐块看到具体代码变更。这种“先看差异、再读全量代码”的顺序我一直沿用比直接通读两个目录的源码效率高很多。接着要先摸清每个章级目录的产物。第2章的产物是boot.bin和loader.bin第5章之后才会出现kernel.bin。拿到任意一个章的源码先看 Makefile 里的目标依赖再回头读汇编或 C 代码。以第2章为例# 第2章 Makefile核心结构 ASM nasm ASMFLAGS boot.bin: boot.asm $(ASM) $(ASMFLAGS) -o boot.bin boot.asm loader.bin: loader.asm $(ASM) $(ASMFLAGS) -o loader.bin loader.asm image: boot.bin loader.bin dd ifboot.bin offloppy.img bs512 count1 convnotrunc dd ifloader.bin offloppy.img bs512 seek1 count4 convnotrunc clean: rm -f boot.bin loader.bin floppy.img这段文件里两个 dd 命令是整条链路的关键。第一个把boot.bin写入floppy.img的第0扇区BIOS 加电后只会自动加载这一个扇区到物理地址0x7C00并跳转执行第二个把loader.bin写入从第1扇区开始的位置seek1跳过起始扇区count4表示写入四个扇区因为 loader 的代码量已经超过512字节。这些数字对应 x86 传统引导方式不能随手改动。为什么不把代码全塞进一个扇区因为单扇区只有512字节引导代码要在这么小的空间里完成设置栈、读取磁盘、检查启动标志三件事已经很紧张。把引导扇区和加载器拆开是数十年沿袭下来的惯例也是 GRUB、syslinux 等引导方案的基础思想第一阶段做最小硬件初始化第二阶段负责真正的大块代码搬移。为了确认生成的boot.bin真能被 BIOS 识别用 hexdump 查最后两个字节# 检查引导扇区的合法结束标志 0xAA55 hexdump -C boot.bin | tail -2如果末尾两个字节不是55 AABIOS 会直接忽略这个扇区Bochs 启动后表现为黑屏或反复软驱读盘。这个检查应该成为每次修改引导代码后的习惯动作。2.2 工具链五件套nasm、gcc、ld、dd、bochs 的分工与参数边界这套实验环境用到的外部工具可以归纳为五件套。先给一个整体视图工具职责常用参数nasm汇编器生成裸二进制或目标文件-f bin/-f elfgccC 编译器生成 32 位目标文件-m32 -c -fno-builtinld链接器按脚本布局生成内核镜像-m elf_i386 -T link.lddd磁盘工具把二进制写入映像扇区bs512 seekN countM convnotruncbochs模拟器与调试器运行及断点调试-f bochsrc -qnasm 是这套环境里最不容易出问题的环节因为它本身跨平台且能直接输出裸二进制。关键在选择输出格式引导扇区和 loader 必须用-f bin生成的文件没有文件头写入磁盘扇区后 CPU 能直接执行反之如果用-f elf生成目标文件再链接得到的 ELF 文件头里的 magic 符号会被 CPU 当成指令执行第一行就翻车。进入保护模式后的 C 代码则相反必须用-f elf交给编译器和链接器处理两块代码的产物形态完全不同。gcc 必须显式加-m32强制生成32位指令。若宿主环境是64位默认工具链却漏掉这个参数目标文件的反汇编里会出现 REX 前缀和错误的内存寻址表面能编译通过实际链接出来的镜像完全不可用。另一个值得加的参数是-fno-builtin避免编译器把某些函数调用换成内置实现比如把strcpy替换成内联代码破坏你手动搭建的内核运行时环境。-fno-stack-protector也要打开否则 gcc 会往函数入口插入栈保护检查这些代码依赖 libc 的某些外部函数裸机环境下链接根本过不去。ld 的作用是按地址布局方式链接目标文件。和普通应用不同内核镜像没有操作系统帮它决定加载地址所有段的地址必须写进链接脚本。在 x86 下常见做法是把内核虚拟地址设在0x304000或0x100000附近只要这段物理内存没有别的用途具体值由设计者自行决定。链接脚本的具体写法放在第3章这里先记住“必须用-T指定脚本”这个原则。dd 是每次做镜像都会碰到的工具最大的坑是默认可能截断目标文件。假设第一次写入boot.bin后floppy.img总长512字节第二次写loader.bin时若数据长度小于原文件长度且忘记convnotrunc磁盘映像会被截断之前写好的引导扇区可能被覆盖。所以我一直用完全显式的参数写法count 和 seek 一个都不省确保每次写入都是修改指定区域、不伤及无关区域。Bochs 的选择主要看调试能力。它除了能模拟完整 x86 外设还内置调试器可以下断点、单步、查看物理内存和寄存器这是学习引导过程最有价值的特性。相比之下一些通用虚拟化软件更注重图形性能和兼容性对裸机引导的调试支持弱很多。Bochs 的配置通过纯文本文件控制试错成本低适合反复做破坏性实验。2.3 为什么模拟器比物理机更适合内核实验在真实硬件上调试 OS最尴尬的情况是一执行就死机屏幕没有任何输出你根本不知道 CPU 在哪条指令上跑偏。物理机没有寄存器窗口和反汇编视图唯一的排错手段是串口调试卡或示波器门槛高得离谱。Bochs 把这个问题直接抹平可以随时暂停运行查看全部寄存器当前值、内存内容甚至反汇编当前执行指令附近的代码。另一个理由是“可重置性”。内核开发过程中一个野指针就可能把磁盘映像某个扇区写坏。物理机遇到这种情况只能关机重新烧录启动盘而在 Bochs 里直接覆盖floppy.img文件恢复原始状态只要一条 cp 命令。配合快照功能你可以把启动过程从失败之后拉回失败之前反复观察这对排查不稳定复现的 bug 极其有价值。启动 Bochs 的命令很简单但配置文件必须先就位# 启动Bochs-f指定配置文件-q跳过交互菜单 bochs -f bochsrc -qbochsrc 决定模拟器的硬件边界。下面是一个入门级配置# 一个起点足够低的bochsrc megs: 32 floppya: 1_44floppy.img, statusinserted boot: a vga_update_interval: 150000megs: 32分配32MB模拟内存对书里早期实验完全够用floppya行把floppy.img挂接为软驱0并插入软盘boot: a表示从软驱启动vga_update_interval是显示刷新间隔调大一点能减少模拟器 CPU 占用。如果遇到窗口长时间无响应先检查是不是这个参数被改得太夸张。3. 搭建32位编译链接环境从汇编到C内核的第一份Makefile3.1 工具链版本对齐与宿主环境差异写操作系统和写应用程序在编译期最大的区别是目标执行环境完全不同。普通程序由操作系统加载器负责动态链接和重定位而内核镜像要直接运行在裸机上所有地址必须在编译期确定。因此工具链必须按“裸机32位 x86”这个目标来配置不能用宿主平台默认行为。多数读者的宿主环境是64位 Linux 或 Windows。在 Linux 下我通常先确认编译器支持-m32# 验证gcc是否支持32位目标 echo int main(){return 0;} /tmp/test.c gcc -m32 -c /tmp/test.c -o /tmp/test.o file /tmp/test.o如果输出里能看到 “ELF 32-bit LSB relocatable”说明-m32可用如果报错提示缺少32位运行时头文件就需要安装对应 multilib 支持包常见做法是安装gcc-multilib和libc6-dev-i386。这一步完成后目标文件就是纯32位后续链接才不会有意外。书中不同章节对工具链的要求也在变化。第2章汇编阶段完全不需要 gcc第5章引入 C 之后需要同时处理汇编和 C链接脚本里给出的地址必须和引导代码约定的加载地址保持一致。如果镜像内的内核入口地址和链接脚本指定值不一致加载器跳转后第一步就执行越界。这个地址契约是多个文件协同的根基建议在心里保持一张映射表哪个文件决定入口地址哪份配置负责加载地址。3.2 链接脚本把内核映像钉在固定线性地址链接脚本是整套流程里最容易被忽视、但最不该忽视的文件。没有它ld 会按宿主平台默认布局生成目标文件而默认布局对裸机毫无意义。一个用于 x86 32 位内核的链接脚本大致如下/* link.ld —— 32位内核链接脚本 */ OUTPUT_FORMAT(elf32-i386) ENTRY(_start) SECTIONS { . 0x304000; /* 内核从物理地址3MB处开始 */ .text : ALIGN(4) { *(.text) } .rodata : ALIGN(4) { *(.rodata) } .data : ALIGN(4) { *(.data) } .bss : ALIGN(16) { *(.bss) } }OUTPUT_FORMAT强制输出目标文件为 ELF32ENTRY告诉 ld 入口符号是_start。最关键的一行是. 0x304000含义是把当前定位计数器设置到3MB附近之后所有段都从该地址开始排列。选3MB而不是1MB以下是为了避开常规 BIOS 数据和传统内存区域选用0x100000也可以前提是引导加载器和链接脚本约定一致。每个小节的ALIGN(N)表示该段起始地址按 N 字节对齐目的是满足代码段某些指令和数据结构的内存对齐要求。尤其 BSS 段如果在 x86 上不对齐后续分页和某些缓冲区操作可能触发未对齐访问异常。C 源码里声明的全局变量会落到.data或.bss段如果链接脚本没安排它们的位置ld 会静默地把它们放到别处调试时你会看到变量地址和预期完全不符。链接时执行命令如下# 用链接脚本把C编译出的目标文件链接成最终内核镜像 ld -m elf_i386 -T link.ld -o kernel.bin head.o start.o main.o其中head.o是汇编入口目标文件start.o和main.o是 C 目标文件。顺序上head.o必须放在第一个因为_start符号定义在其中链接器需要从第一个目标文件里找入口点。如果放置顺序错了ld 可能把_start解析到别的地址导致入口不正确。这类错误通常会在启动阶段表现为跳转到空地址或无限循环极难直接从源码排查。3.3 一份完整的构建Makefile装配、清理与目标依赖当源码里同时出现 .asm、.c、.ld 三类文件时Makefile 的依赖关系就必须仔细处理。一个能直接复制的构建脚本如下# 构建内核镜像的Makefile可作模板 CROSS_COMPILE CC $(CROSS_COMPILE)gcc AS nasm LD ld CFLAGS -m32 -c -fno-builtin -fno-stack-protector -Wall LDFLAGS -m elf_i386 -T link.ld OBJS head.o start.o main.o kernel.bin: $(OBJS) link.ld $(LD) $(LDFLAGS) -o kernel.bin $(OBJS) head.o: head.asm $(AS) -f elf -o head.o head.asm start.o: start.c $(CC) $(CFLAGS) -o start.o start.c main.o: main.c $(CC) $(CFLAGS) -o main.o main.c image: kernel.bin dd ifkernel.bin offloppy.img bs512 seek1 count32 convnotrunc clean: rm -f *.o kernel.bin floppy.img注意 CFLAGS 里没有-O2。-O2会让编译器重新排列代码顺序以优化流水线这在普通程序里没问题但在内核开发中可能改变你预期的指令执行顺序给汇编级调试带来额外困扰。先用默认优化等级保证可调试性等整个机制稳定后再考虑性能。把kernel.bin写入磁盘映像时我没有用count1而是count32因为内核镜像远超512字节。这里的seek1意味着从第1扇区开始写也就是说这个镜像方案仍然依赖前面的 loader 把第1扇区之后的数据搬进内存。理解了 seek 和 count 的配合就理解了磁盘映像布局的本质引导扇区是入口内核数据按顺序摆在后续扇区加载器按同样顺序把它们搬到约定地址。4. 引导扇区与Loader联调用Bochs把开机过程单步走一遍4.1 引导扇区的最小骨架与0x7C00约定掌握了构建流程后下一步应该亲手把引导扇区代码从头写一遍。一个 x86 引导扇区虽然只有512字节但要完成三件事设置栈、通过 BIOS 读取磁盘上的 loader、跳到 loader 执行。最简骨架如下; boot.asm —— 最小引导扇区 org 0x7C00 start: mov ax, cs mov ds, ax mov ss, ax mov sp, 0x7C00 ; 栈指针指向引导代码上方 ; 读取第1扇区开始的loader到内存0x8000 mov ax, 0x0800 mov es, ax mov bx, 0x0000 mov dl, 0x00 ; 软盘驱动器号 mov ah, 0x02 ; BIOS读扇区功能 mov al, 4 ; 连续读4个扇区 mov ch, 0 ; 柱面0 mov cl, 2 ; 起始扇区2第1扇区是引导扇区 mov dh, 0 ; 磁头0 int 0x13 jmp 0x0800:0x0000 ; 远跳转到loader times 510-($-$$) db 0 dw 0xAA55 ; 引导标志org 0x7C00是给汇编器看的告诉 nasm 后续所有标号和变量按这个基址计算CPU 本身不知道这段约定只是 BIOS 跳转到 0x7C00 执行而代码里所有绝对地址必须和实际运行地址一致。栈指针设到 0x7C00 是因为栈向下增长放在引导代码上方不会与代码自身冲突。读取磁盘使用int 0x13的AH0x02功能ES:BX 是内存目标地址DL 是驱动器号CH/CL/DH 分别对应柱面、扇区、磁头AL 是要连续读取的扇区数。读完跳到0x0800:0x0000loader 开始执行。末尾的times指令把文件填充到510字节最后两个字节写0xAA55在二进制里实际是55 AAIntel 小端存储。缺少这两个字节BIOS 会拒绝引导Bochs 的表现是启动后软驱指示灯亮一下就停住屏幕上连光标都没有。4.2 进入保护模式的四个关键步骤Loader 的职责是从实模式过渡到保护模式这一步是全书第一个高门槛。从汇编视角看切换到保护模式需要四个关键动作加载 GDT、关闭中断、设置 CR0 的 PE 位、远跳转刷新流水线。核心代码如下; loader.asm 片段GDT 与切换到保护模式 lgdt [gdt_ptr] ; 加载全局描述符表 cli ; 关中断保护模式下IDTR仍指向实模式向量表 mov eax, cr0 or eax, 0x1 ; 设置PE位置1即进入保护模式 mov cr0, eax jmp dword SELECTOR_CODE:protected_mode ; 远跳转刷新CSGDT 里至少要定义三个描述符空描述符、代码段描述符、数据段描述符。代码段基地址通常设为0段限长设为4GB粒度位打开这样代码段的线性地址就等于物理地址省去分页前的地址换算。书中那张描述符格式表落到代码里是一个由8字节结构组成的数组lgdt指令加载的是这个数组的地址和长度。进入保护模式后实模式中断完全失效所有依赖 BIOS 的功能读磁盘、写屏幕都要自己重新实现。这个“从依赖到独立”的转变恰恰是把设备驱动概念从抽象名词变成实际代码的关键过程。在 Bochs 里调试这一步时最常见的错误是段描述符的 DPL 或粒度位配错导致跳转后立刻触发 GP 异常断点会停在异常处理流程的地址而不是你的代码地址。4.3 Bochs调试器下断点、单步、查看GDT调试这套启动流程掌握 Bochs 调试器的基础指令就够了。启动时不加-q会进入调试状态Bochs 停在第一条指令等待输入。常用调试指令整理如下b 0x7c00 # 在物理地址0x7c00下断点 c # 继续运行直到命中断点 s # 单步执行一条指令 dump_cpu # 打印CPU全部寄存器 xp /16bx 0x7c00 # 查看物理内存0x7c00开始的16字节 info gdt # 显示当前GDT表内容 q # 退出注意 Bochs 调试器默认的地址是物理地址。如果你用org 0x7c00写引导代码那么在0x7c00下断点就能命中入口。但如果代码后来开启分页线性地址和物理地址不再相同Bochs 的xp命令查看的是物理内存而普通调试器里看到的地址往往是线性地址两者要分清。在引导阶段我习惯先b 0x7c00再下一行断点到 loader 入口分别对应引导扇区和加载器。跑到 loader 入口后用dump_cpu看 eax 是否等于预期值再用info gdt确认 GDT 已正确加载。这套固定流程能覆盖百分之九十的启动失败场景。5. 避坑与排障Bochs启动、内核加载与镜像制作的实战故障5.1 现象nasm编译通过Bochs启动后黑屏或反复读软盘先说现象代码在 nasm 下编译零报错Makefile 也都执行成功但一启动 Bochs屏幕黑着软驱指示灯闪几下就停住。最直接的原因是引导标志缺失或位置不对。BIOS 加载引导扇区后会检查最后两个字节是不是55 AA如果不是直接放弃引导。原因基本就三个一是boot.bin大小不等于512字节times填充指令写错导致文件超出或不足二是写入位置错了dd没有写到第0扇区而是写到了别的偏移三是文件本身是 ELF 格式而非裸二进制。解决方法是逐项排查# 三连检查文件大小、文件类型、尾部字节 ls -l boot.bin file boot.bin hexdump -C boot.bin | tail -2如果文件大小不是512字节回到boot.asm检查times 510-($-$$) db 0这行是否拼写正确如果文件类型显示为 ELF说明 nasm 加了-f elf而不是-f bin如果尾部不对检查dw 0xAA55是否存在。最后再确认dd命令里count1、convnotrunc都写全了。我从这套三连检查里捞回过多次本来能救的镜像。5.2 现象C内核编译链接报错找不到 _start 符号编译 C 代码没问题但最后ld时报出undefined reference to _start。这个报错几乎都出在汇编入口文件和链接脚本的配合上。链接脚本里写了ENTRY(_start)可链接器在你给的第一个目标文件里找不到这个符号。原因往往是head.asm里只写了_start:标签但没写global _start。nasm 默认不导出符号ld跨目标文件解析时看不到_start自然报未定义。另一个常见原因是链接顺序里把 C 目标文件放在了汇编文件前面ld从第一个目标文件开始找入口找错了对象。解决分两步。先在汇编入口文件里显式导出符号global _start section .text _start: ; 入口代码再把 Makefile 里链接命令的目标顺序固定为 head.o 在最前ld -m elf_i386 -T link.ld -o kernel.bin head.o start.o main.o如果这样仍报错用nm head.o | grep _start确认符号是否真的存在于目标文件里。没有输出就是导出语句写错有输出但链接报错就是目标文件顺序问题。这个排查顺序能快速定位九成以上的入口符号问题。5.3 现象镜像在虚拟机软件里无法启动Bochs却一切正常Bochs 里跑得好好的软盘镜像拿到另一款虚拟机软件或者实机上表现为“无启动设备”或者黑屏。这不是玄学而是不同引导环境对引导介质格式的宽容度不一样。Bochs 对纯二进制软盘镜像几乎不挑食只要0xAA55在末尾就能启动但某些真实 BIOS 和虚拟化固件会把“软盘”当成必须带有完整 FAT12 文件系统的介质来检查或者干脆不认没有分区表的“软盘镜像”。解决办法是绕过软盘格式的校验改用 ISO 方式引导。ISO 9660 文件系统有标准卷描述符BIOS 和虚拟机固件对它的识别更稳定。把内核放进 ISO 的引导目录再用 GRUB 作为引导器加载就能同时兼容 Bochs、其他虚拟机软件和真实物理机光驱。制作 ISO 时注意两点一是内核镜像要符合 multiboot 规范头部GRUB 才能识别二是 ISO 的引导目录结构要正确不能用普通数据光盘的方式随便丢文件进去。具体流程在第6章展开。5.4 现象Bochs断点命中不了或命中地址和源码对不上调试到分页相关代码时经常遇到b命令下了一个断点c继续运行根本不停或者停下来的位置和预期地址完全不一致。根源在于 Bochs 调试器默认按物理地址断言而你代码里的链接地址是线性地址。开启分页后线性地址经过页表转换才得到物理地址两个值不再相等。例如链接脚本把内核放在线性地址0x304000而页表可能把它映射到物理地址0x100000。你下b 0x304000时Bochs 把它当作物理地址来找CPU 访问的是0x100000的物理页面断点当然不会触发。解决分三种情况。在开启分页之前下断点用逻辑地址直接换算物理地址没有分页时线性地址等于物理地址这样做最简单。开启分页之后先查页表算物理地址或者干脆在 Bochs 里用info gdt查看段基址再手工换算。第三种做法是直接用b对“线性地址”下断点前提是 Bochs 配置里把分页的线性地址断点支持打开具体看版本说明。我自己的习惯是分页还没开就按物理地址断点分页一开就把断点地址换算成物理页宁可多算一步也不瞎试。6. 进阶用GRUB把内核做成可启动ISO并验证启动链6.1 从软盘镜像切换到ISO的制作流程软盘镜像在 Bochs 里足够学习用但想拿到更真实的硬件环境验证ISO 才是更稳的载体。制作流程分两步先把内核打包成 GRUB 能识别的 multiboot 格式再用 mkisofs 生成 ISO。下面是一个可以直接套用的 Makefile 片段# 将kernel.bin制作成可启动ISO kernel.bin: $(OBJS) link.ld $(LD) $(LDFLAGS) -o kernel.bin $(OBJS) iso: kernel.bin mkdir -p iso/boot/grub cp kernel.bin iso/boot/kernel.bin cp grub.cfg iso/boot/grub/grub.cfg grub-mkrescue -o orange.iso isogrub.cfg里最关键的一行是multiboot /boot/kernel.bin它告诉 GRUB 内核入口地址和加载方式。如果内核的汇编入口没有按 multiboot 规范写magic字段和头部GRUB 会直接拒绝加载。最常见的做法是在head.asm里手动构造 multiboot 头部包括 magic 值、标志位和校验和。6.2 启动链的三个验证检查点每次做完 ISO我习惯按三个检查点从头验证启动链缺一个都不算完成。第一道检查看制作工具是否报错特别是grub-mkrescue是否成功生成了引导映像第二道检查用file确认产物格式ISO 和 ELF 的类型都应该符合预期第三道检查直接把 ISO 挂进 Bochs 跑一遍观察是否能进入 GRUB 菜单、是否能加载内核。从做这套实验开始我每次都强制走一遍“先看 Makefile 产物、再查文件格式、最后跑模拟器”的顺序三道检查全过才敢说这次改动是稳的。希望帮到你。本文还有配套的精品资源点击获取