把 C 程序拆到汇编,我才看懂“计算机是怎么工作的“
学号20262826 · 姓名张智泓主题计算机是如何工作的 —— 从反汇编一个 C 程序看函数调用与堆栈原创作品转载请注明出处写在前面第二周的主题是「计算机是如何工作的」。坦白说刚接触时我心里是有点虚的——计算机不就是一个黑盒子吗我写个main.cgcc编译一下跑起来出结果中间到底发生了什么这周的作业逼着我把这个黑盒子拆开看。配合实验楼「反汇编一个 C 程序」的实验我真正动手把一段再简单不过的 C 代码编译成汇编然后逐条去啃pushl、call、ret这些指令才慢慢明白那句计算机科学里流传最广的话是什么意思程序 存储的程序 指令的执行。而指令的执行本质上是 CPU 在「取指—译码—执行」的循环里反复操作寄存器和内存。这篇文章记录我这一周学到的东西重点是我在理解汇编时遇到的困难以及我是怎么一步步把它们啃下来的。一、教材学习《庖丁解牛 Linux 操作系统分析》第 2 章教材第 1 章属于课程导论学习方法、实验环境、Linux 系统概览这些我在第一周的导论作业和实验楼环境搭建中已经接触过这里不再展开。本周重点啃第 2 章它讲两件事2.1 存储程序计算机计算机的地基和2.2 x86 汇编语言基础读懂实验汇编的工具箱章末的2.3 ARM64、2.4 指令乱序和2.5 编写精简内核在我的课程版本里是锁定内容我也按目录框架做了自学整理一并列在下面。学完再回头看实验很多背下来的结论突然就通了。2.1 存储程序计算机一切的地基哈佛结构 vs 冯·诺依曼结构教材先介绍了两种体系结构哈佛结构冯·诺依曼结构指令与数据分开存储、独立总线存在同一个内存里典型代表单片机、DSP 等嵌入式x86 PC、服务器特点取指令和取数据可并行快但复杂结构简单、程序易加载修改但存在取指和取数据抢总线我的实验环境实验楼的 x86 虚拟机就是冯·诺依曼结构——所以我才能干出把 C 代码编译成汇编再存回磁盘这种事指令本身也是数据可以被存储、被查看、被加载。如果是指令数据分开的哈佛结构这套玩法就不一样了。一个有意思的细节现代 CPU 虽然架构上是冯·诺依曼但在CPU 缓存内部又把指令缓存和数据缓存分开了L1 I-Cache / D-Cache算是改良哈佛——两边取长补短。复杂指令集CISC和精简指令集RISCCISC复杂指令集RISC精简指令集思路一条指令干很多事指令多、长短不一指令少而简单、长度固定复杂功能靠组合典型代表x86我们用的 PCARM树莓派、手机、MIPS、RISC-V这正好解释了课程作业里为什么强调「基于树莓派等平台完成 ARM 相关内容」——ARM 是 RISC 的代表和 x86CISC对比着学才能体会两种设计哲学的取舍。回头看实验里我生成的汇编pushl %ebp、movl 8(%ebp), %eax这种允许直接对内存操作数运算的写法就是典型的 CISC 风格RISC如 ARM里通常会先 load 到寄存器再运算内存访问指令单独分开。深入理解冯·诺依曼存储程序思想这一小节是 2.1 的灵魂一句话概括程序指令本身就是数据和普通数据一样存在内存里。由此推出的结论直接支撑了视频里「计算机是如何工作的」的答案CPU 的工作就是一个死循环从内存取指令 → 译码 → 执行 → 取下一条负责下一条在哪的寄存器叫EIP32 位 x86 的程序计数器既然指令是内存里的数据那么修改 EIP 改变执行流程——call、ret、jmp本质上都是在干这件事。这让我彻底想通了一个之前背过但没懂的问题为什么函数调用能跳过去再跳回来因为返回地址被当成数据压进了堆栈返回时再弹出来塞回 EIP。指令和数据在这套体系里本来就是一家人。计算机的存储系统教材用一座金字塔讲存储层次——越往上越快、越贵、越小寄存器 (eax/esp/ebp...) ← 最快CPU 内部 高速缓存 (L1/L2/L3) 内存 (DRAM) ← 程序运行时在这里 磁盘 (SSD/HDD) ← main.c、main.s 在这里对照我的实验就很具体了main.c和main.s躺在磁盘上./main运行时程序被加载进内存而执行过程中的数据参数8、中间值2834在寄存器和堆栈里流动。一条命令的生命周期正好穿过了整座金字塔。计算机的总线结构CPU 和外部打交道靠三组总线分工明确地址总线指定去哪地址总线宽度决定寻址空间32 位机 2³² 4GB数据总线搬运拿什么控制总线指挥干什么读还是写。2.2 x86 汇编语言基础读懂实验汇编的工具箱第 2 章后半部分共六个小节我逐节整理每一节都对应实验里的一个真实发现。2.2.1 x86 CPU 的寄存器对实验最有用的四个寄存器寄存器作用在我实验里的戏份eax通用寄存器习惯上放函数返回值addl $2826, %eax算出结果main里再加 1esp栈顶指针永远指向栈的最上面subl $4, %esp开辟空间ebp栈帧基址进入函数后固定不动8(%ebp)靠它定位参数eip程序计数器指向下一条指令call/ret改的就是它教材还介绍了段寄存器、标志寄存器等本次实验用不上先记住这几个主角。2.2.2 基本汇编语言语法规则我读汇编时踩的第一个坑就是语法方向gcc 生成的是ATT 语法和很多教材里的 Intel 语法操作数顺序相反movl %esp, %ebp ; ATT%esp 是源%ebp 是目的esp → ebp mov esp, ebp ; Intel ebp 是目的esp → ebp顺序看着反了记住 ATT 的口诀源在左目的在右寄存器前加%立即数前加$指令末尾的l后缀表示 32 位长操作long对应w16 位、b8 位。2.2.3 汇编语言代码片段分析实验涉及的指令速查教材这一节教的就是逐条拆解指令一句话解释pushl x把 x 压入堆栈esp 减 4popl x把栈顶弹出存到 xesp 加 4movl a, b把 a 复制到 baddl n, xx x nsubl n, xx x − ncall f压入返回地址跳到函数 fret弹出返回地址跳回去leavemovl %ebp,%esppopl %ebp函数收尾打包动作其中8(%ebp)这种基址加偏移的寻址方式值得单独一说它表示以 ebp 为基准、偏移 8 字节的位置——在我实验里正好就是第一个参数的位置。为什么是 8因为进入函数时call压了 4 字节返回地址、pushl %ebp又压了 4 字节旧 ebp加起来偏移 8 才够到参数。这个8背后就是完整的堆栈故事下面 2.2.5 会彻底讲透。2.2.4 分析完整的 x86 汇编程序这一节解答了我看main.s时的一个疑惑为什么开头结尾一堆以.开头的行原来那些是编译指示assembler directives不是 CPU 指令是给汇编器和链接器看的指示作用.file main.c标注源文件名.globl g声明 g 为全局符号可被其他模块调用.cfi_*调试用的调用帧信息gdb 用分析时可忽略.size g, .-g记录函数大小所以实验里去掉以.开头的行再分析是有依据的——剩下的才是 CPU 真正执行的指令。2.2.5 函数调用堆栈框架本章重点也是我的难点这是教材的重头戏也是我汇编理解困难的病根所在。教材给出的**栈帧stack frame**概念是每调用一个函数就在堆栈里划出一块属于它的空间靠 ebp 划界。我对照自己的实验把main → f → g调用链的堆栈布局画了出来高地址 --------------------------- | (main 的调用者) | --------------------------- | 返回地址 | - call main 压入 --------------------------- | 旧 ebp | - main: pushl %ebp [ebp_main] --------------------------- | 8 (f 的实参) | - main: movl $8,(%esp) [esp_main] --------------------------- | 返回地址(回 main) | - main: call f --------------------------- | 旧 ebp (main 的) | - f: pushl %ebp [ebp_f] --------------------------- | 8 (g 的实参) | - f: movl %eax,(%esp) [esp_f] --------------------------- | 返回地址(回 f) | - f: call g --------------------------- | 旧 ebp (f 的) | - g: pushl %ebp [ebp_g esp_g] --------------------------- 低地址对照这张图函数的标准开场和标准收尾就都能解释了开场序言pushl %ebp把上一层帧的 ebp 存进我的帧movl %esp, %ebp我的帧基址定在这里——本质是存旧链、建新帧收尾leave拆掉我的帧恢复上一层 ebpret弹返回地址回去。而每个函数里8(%ebp)的8在图上一眼就能看出来ebp 往高地址方向跳过旧 ebp4 字节和返回地址4 字节正好落在调用者压入的参数上。2.2.6 C 语言代码中内嵌汇编语言代码教材最后介绍了在 C 代码里直接嵌汇编的写法__asm__ ...。本次实验没用到但它是后续内核学习的常客——Linux 内核源码里有大量内嵌汇编这里先混个脸熟。2.3 ARM64 汇编语言基础换一双 RISC 的眼睛看同一个程序说明教材 2.3 节2.3.1 ARM64 CPU 的寄存器、2.3.2 常用的 ARM64 汇编指令、2.3.3 分析完整的 ARM64 汇编程序在我的课程版本里是锁定内容暂时只能看到目录。不过课程明确要求关注 ARM/树莓派平台所以我按目录框架结合公开资料和实验体会做了自学整理——先把知识框架搭起来细节等能读到教材后再补全、修正。ARM64 CPU 的寄存器对照 x86 学ARM64AArch64是 ARM 的 64 位架构树莓派、主流手机芯片都是它。和 x86 一对比最直观的感受就是——寄存器多到富余用途x8632 位实验里用的ARM64通用寄存器8 个eax/ebx/ecx/edx/esi/edi/ebp/esp31 个x0~x30取低 32 位就是 w0~w30传参惯例参数多靠堆栈实验里8(%ebp)取参前 8 个整数参数直接走 x0~x7返回值eaxx0栈指针espsp帧指针ebpx29FP程序计数器eippc另有x30作链接寄存器 LR存返回地址常用的 ARM64 汇编指令RISC 风格RISC 的设计哲学在指令表上体现得淋漓尽致——内存只能用专门的 load/store 指令访问运算只在寄存器之间进行指令作用对应的 x86 直觉mov x0, x1x0 x1类似movl %esp,%ebpadd x0, x0, #2826x0 x0 2826三操作数类似addl $2826, %eaxldr x0, [sp, #8]从内存读 8 字节到寄存器x86 里访存的部分如movl 8(%ebp),%eaxstr x0, [sp, #8]把寄存器写入内存x86 里写内存的部分stp / ldp一对寄存器成组存/取用它代替 push/pop 保存帧对应pushl %ebp那套开场/收尾bl g调用函数返回地址存入x30LR不压栈对应call但机制不同ret跳回 x30 里的地址对应ret最颠覆我认知的是blx86 的call必然把返回地址压入堆栈而 ARM64 把返回地址放进链接寄存器 x30——只有函数内部还要再调用别的函数时才需要把 x30 压栈保存。同一个函数调用两种架构给出了两种答案正好呼应 2.1 里 CISC 与 RISC 的对比。试读 ARM64 版的 g 函数如果把我实验里的main.c放到 ARM64比如树莓派上编译g函数大致是这个样子自学整理的简化示意真实 gcc 输出还会把参数先暂存到栈上再取回这里省略等我后续做 ARM 实验时再逐条核对g: stp x29, x30, [sp, #-16]! ; 保存帧指针(x29)和返回地址(x30)sp 下移 16 add x0, x0, #2826 ; 参数就在 x0 里直接加 2826 ldp x29, x30, [sp], #16 ; 恢复 x29、x30 ret ; 跳回 x30 里的返回地址结果留在 x0对比 x86 版多出来的那句movl 8(%ebp), %eax从堆栈里取参数ARM64 版参数直接就在 x0 里连取这一步都省了。同一份 C 代码两种架构产出两种风格的汇编——这一对比比背十遍定义都管用。2.4 指令乱序问题CPU 不一定按套路出牌2.4 节2.4.1 可重入函数与线程安全、2.4.2 编译器指令乱序问题同样是锁定章节下面是我按标题框架做的预习式整理。前面我一直说CPU 一条条执行指令严格说不完全对——为了跑得更快编译器可能重排指令顺序只要不改变单线程语义CPU也可能乱序执行、流水线并行。这些优化对单线程程序是透明的但一旦多线程、中断介入就可能踩坑。可重入与线程安全一个函数执行到一半又被再次进入递归、中断、多线程结果依然正确就叫可重入。判断直觉很简单——只用自己的东西参数、栈上局部变量、寄存器的函数天然可重入一旦碰全局变量、静态变量、共享资源就要小心。用这个标准回看实验里的g、f、main它们只用参数和堆栈所以天然是可重入、线程安全的。这也是函数调用堆栈框架的天然礼物——每帧数据私有互不干扰。编译器乱序编译器开优化如-O2后可能调整指令顺序。我们的实验没开优化、逻辑简单暂时感受不到但它埋了个伏笔——后续学内核并发自旋锁、内存屏障、volatile时乱序会变成主角。先记一笔学到再回头补。2.5 编写一个精简的操作系统内核从分析程序到写系统2.5 节2.5.1 虚拟一个 64 位 x86 的硬件平台、2.5.2 精简的操作系统内核范例代码、2.5.3 精简的操作系统内核关键代码分析和「本章实验」同样是锁定内容下面按目录框架做预习式整理。为什么先虚拟一个硬件平台2.5.1 的标题已经把答案写了一半玩内核第一步不是写代码而是用 QEMU 之类的虚拟机虚拟出一个 64 位 x86 硬件平台。原因很实际内核代码一出 bug轻则死机重则破坏数据虚拟机里随便崩重启就行虚拟机可以单步调试、快照回滚真机做不到大家的真机硬件五花八门虚拟出来的平台反而人人一致、可复现。这和我做实验一用的实验楼 x86 虚拟机其实是同一个思路——区别在于实验一我是在操作系统里面反汇编用户程序而 2.5 要从裸机开始让 CPU 直接执行自己写的内核。一个精简内核大概长什么样按 2.5.2 / 2.5.3 的标题推测一个最小内核通常由三块组成预习推测等能读到范例代码后逐条核对引导BIOS/GRUB 把内核从磁盘加载进内存然后跳到内核入口——本质还是改 EIP让 CPU 去执行刚被加载进内存的指令和 2.1 的存储程序思想完全一致汇编入口设好栈指针、关中断等初始化再跳进 C 函数——实验一里pushl %ebp; movl %esp,%ebp练的那套堆栈功夫在这里是给整个内核立地基最小输出内核里没有现成的printf可依赖通常直接往显存地址如0xB8000写字符屏幕上才出现第一个字。等这节解锁后我想把范例内核亲手跑起来再用实验一练出来的汇编分析功夫去读它的关键代码——正好检验这周的 x86 汇编够不够用。本章实验从目录看是 2.5 的配套实践与实验楼实验一反汇编 C 程序已完成互为补充——一个在用户态看懂程序怎么跑一个在裸机态让系统跑起来。等解锁后跟进。教材第 2 章学完的感觉2.1 给了我为什么2.2 给了我怎么读2.3 让我看到另一条路RISC 的取舍2.4 提醒我CPU 不一定按套路出牌2.5 则剧透了这门课的终点——亲手写一个内核实验则是亲手做一遍。正好闭环。二、视频学习计算机是如何工作的第二周的视频回答了一个看起来很笨、其实很根本的问题计算机是怎么工作的视频里最触动我的一个点是——计算机并不聪明它只是执行得特别快、特别死板。CPU 干的事情可以概括成一个无限循环取指(Fetch) → 译码(Decode) → 执行(Execute) → 再取指...配合几个关键概念我画了张图帮自己理解这也是后面实验的理论基础┌───────────────────────────┐ │ 内存程序指令 数据 │ └─────────────┬─────────────┘ 取指 ↓ ┌─────────────────┐ │ CPU 控制器 │ └─────┬───────────┘ 译码 ↓ ┌─────────────────┐ 读写 ┌───────────┐ │ 运算器 ALU │ ←─────→ │ 寄存器组 │ └─────┬───────────┘ └─────┬─────┘ └──────── 结果写回 ──────────┘三个关键点我用自己的话复述一遍存储程序冯·诺依曼结构程序指令和数据都存在同一个内存里CPU 通过地址去取。这就是为什么程序可以被存储、可以被搬运。程序计数器PC / 在 x86 里叫 EIPCPU 靠一个叫 EIP 的寄存器记住下一条指令在哪每执行一条就自动指向下一条。call和ret之所以能实现函数调用和返回本质就是在改这个 EIP。函数调用 堆栈的舞蹈调函数时要把返回地址和局部变量压进堆栈push函数结束再弹出来pop。堆栈由 ESP栈顶和 EBP栈底/帧指针两个寄存器配合管理。说实话第 3 点是我这一周卡得最久的地方。下面这个实验就是专门用来解决函数调用到底怎么靠堆栈实现这个疑问的。三、实验一反汇编一个 C 程序3.1 实验要做什么实验楼的配套实验要求我们写一个简单的 C 程序编译成 32 位汇编然后分析g、f、main三个函数的汇编代码搞清楚函数调用时堆栈是怎么变化的。实验要求特别提醒数字请自行修改以防与他人雷同。所以我用了自己的学号后四位2826作为g函数里的增量参数也用了8和1这样既符合要求又方便我记。3.2 先写代码练习文件怎么来的我用gedit实验楼环境自带新建了一个main.c内容如下// main.cintg(intx){returnx2826;}intf(intx){returng(x);}intmain(void){returnf(8)1;}说明一下为什么这么设计g是最内层函数f中间转调了一次gmain最外层。三个函数层层嵌套正好能完整展示「调用 → 传参 → 返回」的堆栈全过程。g里加2826、main里f(8) 1数字是我自己的避免和别人一样。保存后我习惯性地先cat确认一下内容真的写进去了以前踩过没保存就编译的坑$catmain.c int g(int x){returnx 2826;}...下图中可以看到代码已经在 gedit 里写好并保存到了~/Code/main.c3.3 编译成 32 位汇编用gcc -S生成汇编-m32表示编译成 32 位这样生成的汇编更经典没有 64 位的那些复杂寻址更适合我们理解堆栈$ gcc-S-omain.s main.c-m32下图是我在实验楼环境里实际执行的截图终端里敲下gcc -S -o main.s main.c -m32成功时没有任何输出“没有消息就是好消息”上方文件管理器里可以看到Code目录下已经生成了main.s3.4 完整汇编代码生成的main.s里有很多以.开头的行.file、.cfi_*这些是编译器的调试/辅助信息分析时我们只看真正的指令。我把核心部分整理如下g: pushl %ebp ; 保存旧的帧指针 movl %esp, %ebp ; 让 ebp 指向当前栈底 movl 8(%ebp), %eax ; 把参数 x 取到 eax addl $2826, %eax ; eax x 2826 popl %ebp ; 恢复旧的帧指针 ret ; 返回结果在 eax f: pushl %ebp movl %esp, %ebp subl $4, %esp ; 栈顶下移 4 字节腾出放参数的位置 movl 8(%ebp), %eax ; 取参数 x movl %eax, (%esp) ; 把 x 放到栈顶作为调用 g 的实参 call g ; 调用 g返回地址自动压栈 leave ; 等价于 movl %ebp,%esp; popl %ebp ret main: pushl %ebp movl %esp, %ebp subl $4, %esp movl $8, (%esp) ; 把 8 压到栈顶作为 f 的实参 call f ; 调用 f addl $1, %eax ; f 的返回值 1 leave ret四、重点我遇到的困难 怎么解决的这一节是整个作业的重点也是我花时间最多的地方。老实说第一次看这段汇编我整个人是懵的。下面是我真实卡住的地方以及我最后是怎么想通的。困难 1ebp和esp到底有什么区别问题每个函数开头都是pushl %ebp; movl %esp, %ebp结尾又popl %ebp。我一开始分不清这两个寄存器更不懂为什么要这样绕。解决我把它们想成一个比喻——堆栈是一摞盘子esp是最上面那个盘子的位置栈顶会随着 push/pop 不断变ebp是我进入这个函数时这摞盘子的标记位置栈底/帧指针进入函数后基本不动。为了彻底分清楚我把进入f函数那一刻的堆栈和两个指针画了出来栈向低地址方向生长即往下长看懂这张图两个关键点就通了ebp 是锚esp 是浮标进入函数后ebp被movl %esp,%ebp固定在当前帧的边界之后几乎不动而esp会随着subl开空间、pushl压栈来回浮动永远指向栈顶。为什么8(%ebp)正好是第一个参数从 ebp 往上高地址看紧挨着的是旧 ebp占 4 字节和返回地址占 4 字节跳过这 8 字节就落到了调用者压进来的参数上——所以是8(%ebp)这个8就是旧 ebp(4) 返回地址(4)的总和。为什么需要ebp这个不动的标记因为函数里访问参数和局部变量都是靠ebp的固定偏移来定位的8(%ebp)就是第一个参数。如果只用esp它在函数里会subl $4移动偏移就得算来算去。有了ebp参数位置就稳定了。而开头那句pushl %ebp是为了把上一层函数的 ebp存起来等这个函数结束后popl %ebp恢复——这样层层嵌套的调用才不会乱。困难 2call和ret是怎么跳来跳去的问题call g执行完怎么知道该回到哪里ret又是怎么回来的解决关键在EIP程序计数器和堆栈里存的返回地址。call g做了两件事① 把下一条指令的地址返回地址压入堆栈② 把 EIP 改成g的入口地址让 CPU 跳过去执行。ret做相反的事把堆栈顶的返回地址弹出塞回 EIPCPU 就跳回call的下一句继续执行。所以函数调用本质上就是压返回地址 改 EIP“函数返回就是弹返回地址 改 EIP。绕来绕去都是那个 EIP 在控制下一步执行哪条指令”。困难 3leave是个啥为什么要用它问题f和main结尾有个leave我查了才知道它等价于两条指令但为什么要专门造这么个指令解决leave等价于movl %ebp, %esp ; 把 esp 拉回到 ebp 的位置清掉局部变量/临时空间 popl %ebp ; 恢复上一层函数的 ebp它就是把函数收尾这件固定动作打包成了一条指令。好处是简洁、且不会忘记恢复esp。我一开始以为g没用leave它用的是popl %ebp后来才注意到——g里没有subl开局部空间所以esp本来就没动直接popl %ebp就够了而f和main都subl $4开了 4 字节必须靠leave把esp拉回来。理解了为什么有的函数用 leave、有的不用我才算真正懂了堆栈的平衡。用手推一遍完整的调用过程想通上面三点后我把main→f→g的完整过程在纸上推了一遍这是我觉得最有用的学习方法强烈建议大家都试一次步骤发生了什么esp/ebp 变化1main调f(8)先把8压栈再call f压返回地址栈里多了8和返回地址2进入fpushl %ebp保存 main 的 ebpmovl %esp,%ebp建立 f 的帧ebp 指向新的栈帧3f里subl $4开 4 字节把参数x8放到栈顶call g栈里又多了8和 g 的返回地址4进入g建立帧8(%ebp)取到8addl $2826得到2834eax 28345g的popl %ebpret恢复 ebp跳回 f 的 call 后一句eax 2834 传回 f6f的leaveret返回 maineax 2834 传回 main7main里addl $1eax 2835所以最终main的返回值是2835f(8) g(8) 8 2826 2834再1。把上面的 7 步画成一张调用链时序图方向就一目了然了——从上到下是时间实线箭头是调用往下钻“虚线箭头是返回往上弹”这张图把三件事串在了一起调用是层层往下钻main → f → g每钻一层就把返回地址压进栈好在钻完之后能原路返回返回是原路往上弹g → f → main每弹一层ret就把返回地址塞回 EIP同时 eax 里的结果一路往上带eax 是传家宝g算出2834后f原样接住main再1变成2835。返回值就是靠 eax 这个寄存器一棒一棒传上来的。可以用一条命令验证一下$ gcc-omain main.c-m32./main;echo$?2835echo $?能打印出上一条命令的退出码也就是main的返回值正好是 2835和手推结果一致。五、小结这一周我从计算机是个黑盒子到能亲手反汇编一个程序、看懂函数调用的堆栈舞蹈最大的收获是这几条计算机不神秘就是取指—译码—执行的死循环只是快到我们以为是智能。函数调用 压返回地址 改 EIP返回 弹返回地址 改 EIP。抓住 EIP 这条线整个调用机制就串起来了。ebp是稳定标记esp是浮动栈顶参数和局部变量靠ebp的固定偏移定位。leave是函数收尾的打包动作理解它什么时候用、什么时候不用才算真懂堆栈平衡。CISC 与 RISC 是两种设计哲学x86 的call把返回地址压栈、参数走堆栈ARM64 的bl把返回地址放寄存器、参数直接走 x0~x7——同一份 C 代码两种汇编风格。手推一遍调用过程比只看代码有效得多纸上演算一次胜过读十遍。参考资料《庖丁解牛 Linux 操作系统分析》第 1、2 章蓝墨云班课第二周视频《计算机是如何工作的》实验楼配套实验一反汇编一个 C 程序豆列「Linux内核及安全」深入学习参考原创作品转载请注明出处。如文中有错误欢迎指出一起学习。