【函数本质】五类指令如何模拟出函数和类:计算机底层逻辑完全拆解

发布时间:2026/7/19 9:58:54
【函数本质】五类指令如何模拟出函数和类:计算机底层逻辑完全拆解 原创 · 2026-07-19 · 本文遵循 CC 4.0 BY-SA 版权协议作者简介大家好我是CodeStats。一个在底层技术上“考古”了四年的硬核爱好者也是WWAIC全周项目AI编程范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架从 IoC 容器到嵌入式 Tomcat代码全开源也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。我的技术信条所有高深的技术最后都能用大白话讲清楚。如果讲不清楚说明还没真正理解。 本文收获一以贯之万归于一。对于任何程序运行之谜我的文章以通俗、透彻、全面之姿回归本质的一引领你找到想要的答案。这不是一篇“知识点合集”而是我花了四年时间从“照抄代码”到“追问为什么”一层一层砸穿技术迷雾之后画出来的完整地图。你将会顺着这样的路径层层递进先从冯·诺依曼搞清楚计算机到底长什么样再追问CPU到底能干什么——发现只有五类指令再追问指令怎么跑起来——原来是流水线再追问函数调用怎么实现的——CALL/RET压栈弹栈再追问类又是怎么来的——内存布局this指针再追问多核怎么办——MESI缓存一致性通读筑基、进阶、高阶、论道之层层递进便能从底层运行机制到上层应用架构构建起完整的思考体系。如果你也觉得学技术经常“空中楼阁”这篇文章就是你的地基。 目录提问一电脑组成结构——冯·诺依曼架构的五大部件提问二CPU指令分成哪五大类提问三一条指令的运行流程包含哪些步骤提问四函数的本质是什么如何用指令模拟出来⭐提问五类的本质是什么如何用内存结构和函数模拟出来⭐⭐补充多核时代的挑战——缓存一致性MESI协议提问一电脑组成结构——冯·诺依曼架构的五大部件1.1 核心思想存储程序冯·诺依曼体系结构最核心的思想只有八个字存储程序程序控制。翻译成人话把要执行的指令程序和数据一起放在内存里CPU一条一条取出来执行。今天你电脑里打开的任何软件——浏览器、IDE、游戏——本质上都是一堆提前放在内存里的指令和数据CPU正在逐条取出来执行。一言总结程序和数据都放在内存里CPU逐条取指令执行这就是冯·诺依曼的灵魂。1.2 五大部件冯·诺依曼计算机由五大基本部件组成部件功能现代对应运算器完成算术运算、逻辑运算、数据传送CPU中的ALU算术逻辑单元控制器控制程序执行指挥各部件协调工作CPU中的控制单元存储器存储程序和数据内存RAM输入设备将外部数据送入计算机键盘、鼠标、网卡输出设备将计算结果输出到外部显示器、打印机、网卡运算器和控制器合在一起就是今天所说的CPU中央处理器。一言总结五大部件就是“大脑CPU”“记忆内存”“五官输入输出”。1.3 一个关键瓶颈冯·诺依曼架构有一个著名的“冯·诺依曼瓶颈”指令和数据通过同一条总线传输CPU取指令和取数据不能同时进行。这就好比一条单车道公路车数据和指令另一批车抢着用谁也别想跑太快。现代CPU用多级缓存L1/L2/L3 Cache和指令流水线来缓解这个瓶颈但根本架构没变——你写的每一行代码最终还是要走这条“单车道”进CPU。一言总结指令和数据共用一条总线这是冯·诺依曼架构的最大短板缓存和流水线只是治标不治本。提问二CPU指令分成哪五大类不管是什么架构x86、ARM、RISC-VCPU指令都可以归纳为五大类。这是理解“函数和类如何被模拟”的起点因为所有高级语言特性最终都要坍缩成这几类指令的排列组合。2.1 第一类数据传送指令移动类把数据从一个地方搬到另一个地方。MOV寄存器 ↔ 内存 ↔ 立即数PUSH/POP栈操作LEA取地址你在代码里写的int a 10;或obj.value 100;底层全是MOV指令在搬数据。一言总结数据传送指令就是“搬运工”90%以上的指令都在干这事。2.2 第二类算术运算指令加减乘除类对数据进行数学计算。ADD/SUB加 / 减MUL/DIV乘 / 除INC/DEC自增 / 自减a b、i、x * 2最终都变成这类指令。一言总结算术指令就是“计算器”做数学运算全靠它。2.3 第三类逻辑运算指令与或非类对数据进行位操作。AND/OR/NOT与 / 或 / 非XOR异或SHL/SHR左移 / 右移权限掩码、位图操作、甚至某些加密算法底层全是这类指令在干活。一言总结逻辑指令就是“位操作工”权限、加密、状态判断都靠它。2.4 第四类控制转移指令跳转/调用类改变程序的执行顺序。——这是实现函数和类的核心指令JMP无条件跳转JZ/JNZ/JE/JNE条件跳转if/else、循环CALL函数调用RET函数返回if (x 0)、for (int i0; i10; i)、obj.set(10)底层全是跳转和调用。没有CALL和RET就没有函数更不可能有类。一言总结控制转移指令就是“导航仪”决定程序往哪走、跳去哪、怎么回来。2.5 第五类处理器控制指令中断/特权类控制CPU本身的行为或切换到特权模式。INT软中断系统调用HLT暂停CPUCLI/STI关/开中断你调用read()、write()、malloc()时底层会触发INT 0x80Linux或syscall指令从用户态切到内核态。一言总结处理器控制指令就是“特权开关”系统调用和内核切换全靠它。提问三指令的运行流程包含哪些步骤一条指令从内存到执行完成要经过五个阶段。理解这个流程是为了后面理解“函数调用时CPU到底经历了什么”。3.1 取指IFInstruction FetchCPU根据指令指针寄存器RIP / PC指向的地址从内存的代码段取出指令。这时候指令在CPU眼里还是一堆0和1的二进制串没有任何意义。一言总结取指就是“看一眼图纸”把下一条指令从内存搬到CPU。3.2 译码IDInstruction DecodeCPU把二进制指令翻译成自己能理解的控制信号。比如B8 0A 00 00 00这个二进制串译码器认出来“哦这是MOV EAX, 10把10放进EAX寄存器。”一言总结译码就是“翻译图纸”把二进制翻译成CPU能懂的微操作。3.3 执行EXExecuteALU算术逻辑单元根据译码结果真正干活。算术指令 → 做加法/减法传送指令 → 搬数据跳转指令 → 修改RIP一言总结执行就是“按图纸施工”ALU真正干活的阶段。3.4 访存MEMMemory Access如果指令需要读写内存比如MOV [rax], rbx这个阶段访问内存。一言总结访存就是“进出仓库”需要读写内存的指令才经过这步。3.5 写回WBWrite Back把计算结果写回寄存器或内存。一言总结写回就是“归档成果”把结果存回寄存器或内存。现代CPU为了提速把这五个阶段重叠起来并行执行——这叫指令流水线。理想情况下每个时钟周期都能完成一条指令而不是等一条完全跑完再取下一条。一言总结五级流水线就是“装配线”五个阶段同时干不同的活CPU效率最大化。提问四函数的本质是什么如何用指令模拟出来⭐这是本文的第一个核心重点。函数的本质只有两句话把一段代码单独放在内存里给它一个入口地址。调用时跳过去执行执行完跳回来。就这么简单。CPU根本不认识“函数”它只认CALL和RET两条指令。函数就是用CALL和RET这两条控制转移指令模拟出来的代码复用机制。一言总结函数就是“一段有名字的代码”CPU靠CALL和RET实现去程和回程。4.1 CALL 指令去程假设你写了这样一个函数cint add(int a, int b) { return a b; } int main() { int x add(3, 5); return x; }编译器把add的机器码放在代码段的某个地址比如0x400100。当你执行add(3, 5)时编译器生成的指令是assemblymov edi, 3 ; 第一个参数放入 RDI 寄存器 mov esi, 5 ; 第二个参数放入 RSI 寄存器 call 0x400100 ; 调用 add 函数CALL指令做了三件事计算返回地址CALL指令下一条指令的地址假设是0x400205压栈把这个返回地址0x400205压入栈顶跳转把RIP改成0x400100跳过去执行add一言总结CALL指令就是“买好回程票再出发”——先把返回地址压栈再跳过去执行函数。4.2 函数内部栈帧Stack FrameCPU跳进add函数后需要做三件事保存上一个函数的栈底PUSH RBP建立自己的栈帧MOV RBP, RSP分配局部变量空间SUB RSP, 16这就是所谓的“栈帧”——每个函数在栈上有一块独立的空间用来存局部变量和临时数据。add函数的机器码大概是assemblypush rbp ; 保存调用者的栈底 mov rbp, rsp ; 建立自己的栈帧 mov eax, edi ; 第一个参数(3)放入 EAX add eax, esi ; EAX EAX ESI 3 5 8 pop rbp ; 恢复调用者的栈底 ret ; 返回一言总结栈帧就是“函数的工作台”——每个函数在栈上划一块地放自己的局部变量。4.3 RET 指令回程RET指令只做一件事从栈顶弹出一个地址跳转到那个地址因为调用CALL时把返回地址0x400205压进了栈所以RET弹出0x400205跳回main继续执行。一言总结RET指令就是“掏出回程票回家”——从栈顶弹出返回地址跳回调用者的下一行。4.4 函数调用的完整流程图textmain 执行到 call add ↓ CALL 指令: 把返回地址 0x400205 压栈 → 跳转到 add 的地址 0x400100 ↓ add 执行: push rbp → mov rbp, rsp → sub rsp, 16 → 计算 ab → pop rbp ↓ RET 指令: 从栈顶弹出 0x400205 → 跳转回去 ↓ main 从 0x400205 继续执行一言总结CALL买回程票压栈函数干活RET掏票回家——这就是函数调用的全部。一句话总结函数 一段有入口地址的代码 CALL压栈返回地址 RET弹栈跳回。全部由控制转移指令CALL/RET和数据传送指令PUSH/POP/MOV协同完成。提问五类的本质是什么如何用内存结构和函数模拟出来⭐⭐这是本文的终极重点。CPU完全不认识“类”——类在编译后彻底消失只留下两样东西内存布局一块连续的内存像C语言的struct普通函数函数名被改编多了一个隐藏的this指针参数类就是用“内存布局数据传送指令管理 普通函数CALL/RET调用”模拟出来的数据行为捆绑机制。一言总结类就是“内存布局函数集合”CPU根本不认识“类”这个字。5.1 最简单的类没有虚函数写一个最简单的C类cppclass Simple { int value; public: void set(int v) { value v; } int get() { return value; } }; int main() { Simple obj; obj.set(10); return obj.get(); }编译后去掉优化main函数的汇编大概是assemblysub rsp, 16 ; 减法指令在栈上分配16字节其中4字节给 obj lea rdi, [rsp12] ; 移动指令计算 obj 的地址 → 放入 RDI这就是 this 指针 mov esi, 10 ; 移动指令参数 10 → 放入 RSI call Simple::set ; 跳转指令调用 set 函数 lea rdi, [rsp12] ; 移动指令再次计算 obj 地址 → RDI传 this call Simple::get ; 跳转指令调用 get 函数 mov eax, DWORD PTR [rsp12] ; 移动指令把返回值放入 EAX add rsp, 16 ; 加法指令回收栈空间 ret ; 跳转指令返回关键点来了set函数内部长什么样assembly; Simple::set 的机器码地址假设 0x400100 mov DWORD PTR [rdi], esi ; 移动指令把 esi(10) 写入 rdi 指向的内存地址 ret ; 跳转指令返回看到了吗set函数根本不知道什么“类”、什么“对象”。它只知道rdi里有一个地址esi里有一个值把值写到地址指向的内存里去一言总结成员函数就是普通函数只是第一个参数固定是this指针set内部就是一条MOV指令往this地址写数据。obj.set(10)的底层真相你写的代码编译后的指令涉及的指令类别Simple obj;sub rsp, 16—— 划出4字节内存减法指令obj.set(10);lea rdi, [rsp12]取obj地址mov esi, 10传参call set移动指令 跳转指令set内部value v;mov [rdi], esi—— 往obj地址写数据移动指令一言总结对象就是一段内存属性就是偏移量方法就是带this参数的普通函数。全部由移动指令和跳转指令协同完成。类 一块内存存属性 一堆函数第一个参数固定是this指针。对象里只存属性不存函数代码。函数代码在代码段里只有一份所有对象共用。5.2 有虚函数的类多态的底层实现如果类里有虚函数事情稍微复杂一点cppclass Base { public: virtual void foo() { cout Base; } virtual void bar() { cout Bar; } };编译器会为这个类生成一张虚函数表vtable——本质上就是一个存函数指针的数组。每个Base对象里会多出一个隐藏指针vptr指向这张虚表。对象的内存布局text对象起始地址 → [vptr] → 指向虚函数表 [其他成员变量...] 虚函数表 → [0]: Base::foo → [1]: Base::bar调用obj.foo()时底层指令变成assemblymov rax, [rdi] ; 移动指令从对象地址取 vptr虚表地址 mov rdx, [rax] ; 移动指令从虚表取第一个函数指针foo的地址 call rdx ; 跳转指令跳转到 foo一言总结虚函数就是“函数指针数组”调用时先取vptr再取函数地址运行时才知道调谁。移动指令取地址跳转指令执行。多态的本质子类对象的vptr指向子类的虚表虚表里填的是子类重写后的函数地址。调用时通过vptr间接寻址运行时才知道调的是哪个函数。一言总结多态就是“同一个call指令调用不同的函数地址”——靠vptr在运行时决定跳去哪。5.3 类的本质总结概念编译后变成什么涉及指令类别类定义消失。只留下“大小”和“成员偏移量”的编译器内部记录无编译期概念成员变量对象内存中的一段偏移量如this0、this4移动指令MOV/LEA管理普通成员函数普通函数函数名被改编第一个参数是this指针跳转指令CALL/RET调用虚函数虚函数表中的函数指针通过vptr间接调用移动指令取地址 跳转指令调用对象实例化栈/堆上分配一块内存sub rsp或malloc减法指令 / 系统调用对象调用方法传this指针lea rdi, [obj地址]call 函数地址移动指令 跳转指令一言总结类在编译后彻底消失只剩内存布局和普通函数this指针是连接数据和函数的唯一纽带。整个类机制就是“移动指令管理内存跳转指令调用函数”的组合。补充多核时代的挑战——缓存一致性你可能会问如果两个CPU核心同时操作同一个对象的同一个成员变量会发生什么现代CPU为了提速每个核心都有自己的L1/L2 缓存。核心0改了obj.value但新值还在核心0的缓存里没写回内存。核心1去读obj.value读到的可能是旧值。这就是缓存一致性问题。一言总结多核各有各的缓存一个改了另一个不知道——这就是缓存一致性问题。硬件用MESI协议来解决。每个缓存行Cache Line有四种状态状态含义MModified已修改只在本缓存中有内存中是旧的EExclusive独占干净只有本缓存有SShared共享多个缓存都有都是干净的IInvalid无效不能使用当核心0修改一个缓存行时会通过总线广播让其他核心把对应的缓存行标记为I无效。这样核心1再去读的时候发现缓存行无效就只能从内存重新加载——拿到最新值。一言总结MESI就是“缓存状态机”——M独占修改、E独享、S共享、I失效改了就广播通知别人失效。这也是为什么多线程编程需要volatile、atomic、锁这些东西——硬件层面的MESI协议保证的是缓存一致性但内存可见性和原子性还需要软件层面的配合。一言总结MESI保证硬件缓存一致但多线程编程还需要volatile/atomic/锁来保证软件层面的可见性和原子性。 终极总结从底层到上层的完整认知地图把整篇文章串起来你脑子里应该有这张图text┌─────────────────────────────────────────────────────────────┐ │ 你写的代码class Simple { int v; void set(int x); } │ ← 应用层 ├─────────────────────────────────────────────────────────────┤ │ 编译器类→内存布局 函数→普通函数(带this参数) │ ← 编译层 ├─────────────────────────────────────────────────────────────┤ │ 操作系统进程/线程管理、虚拟内存、系统调用 │ ← OS层 ├─────────────────────────────────────────────────────────────┤ │ CPU指令MOV(移动) ADD/SUB(加减) CALL/RET(跳转) INT │ ← 指令层 ├─────────────────────────────────────────────────────────────┤ │ 指令流水线取指→译码→执行→访存→写回 │ ← 微架构层 ├─────────────────────────────────────────────────────────────┤ │ 硬件运算器 控制器 存储器 缓存(MESI协议) │ ← 硬件层 └─────────────────────────────────────────────────────────────┘每一层都在做同一件事把上一层的东西翻译成下一层能理解的“移动和跳转”。你写的obj.set(10)最终在硬件层就是移动指令搬运数据 跳转指令切换执行位置 返回地址压栈保证回得来。没有魔法没有黑盒只有MOV移动、ADD/SUB加减、CALL/RET跳转、INT中断这五类指令的排列组合。函数 CALL压栈去程 RET弹栈回程类 内存布局移动指令管理 成员函数跳转指令调用本文终极一言应用层的所有魔法——函数、类、多态——到底层都是“移动数据 跳转地址”五个字。 写在最后能坚持看到这里的说明你跟我一样是个不喜欢“空中楼阁”的人。别人学框架背API、记配置你学框架脑子里自动浮现“哦这里不过是在堆上分配了一块内存然后存了几个函数指针”。这种从底层往上长的认知方式才是真正的地基。有了这个地基你看任何源码都不会慌——因为你永远知道再复杂的框架底层也不过是五类指令 一块内存的排列组合。如果这篇文章帮你看清了计算机的“全链路”欢迎点赞、收藏、关注我们下期继续深挖互动话题你觉得还有哪个“高级特性”比如lambda、协程、反射的底层原理想被扒干净评论区告诉我下期安排 本文核心思维导图一句话串联全部冯·诺依曼五大部件→五类指令移动/加减/跳转/中断→流水线取指译码执行访存写回→函数CALL压栈/RET弹栈→类内存布局this指针→虚函数vptr虚表→缓存一致性MESI广播失效→结论函数和类都是移动指令跳转指令的组合产物