首页 / 资讯中心 / 文章详情

从a+b到CPU:程序员必知的底层硬核知识入门

从a+b到CPU:程序员必知的底层硬核知识入门 ★ FEATURED ARTICLE
简介面向希望补齐计算机底层认知的程序员与计算机专业学习者这份PDF围绕CPU结构、RAM分类、进制转换、操作系统与API、BIOS及引导流程、汇编语言、调用约定、DMA、RLE与DLL等主题展开把散落在日常开发中的硬件与系统概念串成一条可自学的知识线适合用来查漏补缺或准备面试。整份资料为单文件PDF压缩包约19.81MB内容按主题分段组织便于检索和跳读。已有1281人学习下载说明其在基础认知类资料中具备一定参考价值。读者可从中获得CPU控制器、运算器与寄存器分工DRAM与SRAM差异二进制与十六进制换算MBR与GRUB引导链路以及应用通过系统调用和I/O指令与硬件交互等具体知识点。并据此理解程序从代码到硬件执行的整体路径为系统级编程、性能优化和复杂问题排查打下概念基础。1. 先破除一个误解程序员必知的硬核知识都是你天天在用却从没看见的东西有新手问过我“CPU、内存、汇编、进制转换这些东西是不是得先学完才能写代码”我通常反问一句你现在写的每一行代码都已经在这套东西上跑过几十遍了只是没人指给你看过位置。就像天天坐电梯却从没推开过机房那扇门。程序员必知的硬核知识说的就是门后面那点事——CPU 怎么把一条指令执行完、变量到底住在内存的哪个格子、函数被调用时发生了什么。它不难但确实是个黑匣子不打开它调试就只能靠玄学。这篇就带你从零把这扇门推开一条缝先看懂再跑通第一个例子。2. 为什么要学一行普通代码背后站着四层世界2.1 从 a b 说起高级语言的三行代码CPU 只认一条加法指令先看一句最不起眼的 C 代码c a b; /* 新手眼里这是一步操作 */编译器把它变成机器码之后真正被 CPU 执行的大概是这么三条汇编指令mov eax, dword ptr [ebp-8] ; 把内存里 a 的值搬进 eax 寄存器 add eax, dword ptr [ebp-0Ch] ; 把内存里 b 的值加到 eax 上 mov dword ptr [ebp-4], eax ; 把 eax 的结果写回内存里 c 的位置mov是 move 的缩写意思是搬运add是加法。dword ptr [ebp-8]这一串看着吓人其实只表达一件事去内存里某个地址取 4 个字节。CPU 本身不会“懂”加法以外的东西它手里只有一小把这样的小动作。为什么要知道这个因为从这一刻起你就明白了一行代码不等于一步操作。同样是加法写成哪种形式、变量放寄存器还是放内存执行速度能差好几倍。以后你看到“性能优化”四个字脑子里就有画面了而不是跟着别人瞎调参数。再往上还有一层C 语言编译成汇编汇编再翻译成 0 和 1 的机器码。你在编辑器里敲的字符最终会变成 CPU 引脚上高低电平的组合。这条链子很长但每一环都有名字都能查都不是玄学。2.2 变量住在哪儿内存、栈和那摞盘子接着说[ebp-8]里的地址。内存可以想成一排带门牌号的柜子每个柜子存 1 个字节1 byte 8 bits。门牌号就是地址CPU 拿着地址去敲门取东西或者放东西。那局部变量为什么总在ebp附近打转因为函数调用时会开一块叫“栈”的区域。栈的特点是后进先出像一摞盘子——你只能往最上面放也只能从最上面拿。函数进去时把盘子摞高分配局部变量函数返回时把盘子拿走释放。这也解释了一个常见报错递归写太深会“栈溢出”不是内存不够是盘子摞太高撞到上限了。新手常有的另一个困惑是既然有内存为什么还要寄存器因为寄存器就在 CPU 内部读写几乎不花时间而内存要走一趟“高速公路”。所以真实程序里编译器会拼命把数据往寄存器里塞实在塞不下才写回内存。你以后学性能分析看到“缓存命中率”这个词说的也是同一件事的放大版。CPU 内部大致分成三块控制器负责调度指挥运算器ALU负责算术和逻辑运算寄存器负责临时存放数据。这三块配合的节奏就是下面要讲的取指、译码、执行。2.3 call 和 return函数调用不是魔法写惯了函数的人会觉得“调用一下”是天经地义的。但从 CPU 视角看函数调用只做了两件事跳过去再跳回来。跳过去靠call指令跳回来靠return指令。问题在于CPU 怎么知道该跳回哪里答案是call执行时会把“下一条指令的地址”压进栈里存好return执行时再把它弹出来塞回程序计数器PC。程序计数器就是个指针永远指向“下一条要执行的指令”。这里有个关键细节值得记住PC 一般是自动 1 往下走的只有遇到跳转类指令call、return、jump才会被改写。你写的if、while、for翻译到底层全是跳转。所以“程序崩溃跳到乱七八糟的地方”这种诡异现象八成是栈上的返回地址被写坏了——这就是血泪经验里那句“栈被踩了”。理解了这一层你看递归、看异常栈、看崩溃转储就不会觉得是一堆天书了。2.4 新手该怎么看这些概念先画地图再补细节最后说说方法。我最不建议的做法是一上来买本汇编教材从头背指令表。背到第三天基本就放弃了因为不知道背它干嘛。更靠谱的顺序是先搞定进制和字节第 4 章的内容再搞懂变量在内存里长什么样然后看函数调用怎么用栈最后才碰汇编指令和系统调用。每一步都有能跑起来的代码验证看得见结果就不会虚。3. 第一个例子用 Python 亲手模拟一次 CPU 执行加法3.1 环境准备装一个不折腾的解释器这一段不需要装编译器不需要配环境变量。去装一个 Python 3安装时记得勾上“Add to PATH”那个选项然后打开终端敲python --version看到版本号输出就算成了。Windows 上如果提示找不到命令试试py --version。这一步卡住的人不少多半是安装时没勾 PATH重装一遍勾上就好不用怀疑人生。为什么第一个例子选 Python因为它不用编译、不用管内存布局能把注意力全放在“CPU 到底在干嘛”这件事上。等你看懂了流程再去碰 C 语言那种要自己管内存的语言心里就有底了。3.2 第一段代码把取指、译码、执行写成循环真实的 CPU 每秒跑几十亿条指令但它的核心动作只有三步取指令、译码看懂是什么指令、执行。我们把这三步写成 20 行 Python你就全看明白了。# 一个极简 CPU 模拟器只认 4 条指令够看懂取指—译码—执行 memory [0] * 16 # 假装这是内存16 个格子 registers {A: 0, B: 0} # 假装这是寄存器只有两个 pc 0 # 程序计数器指向下一条要执行的指令 program [ (LOAD, A, 3), # 把数字 3 装进寄存器 A (LOAD, B, 5), # 把数字 5 装进寄存器 B (ADD, A, B), # A A B (STORE, A, 0), # 把 A 的值写进内存 0 号格子 (HALT, None, None), # 停机 ] while True: op, x, y program[pc] # 取指按 PC 拿到当前指令 print(fPC{pc} 执行 {op} {x} {y}) # 让你看见每一步 if op LOAD: registers[x] y # 译码 执行写寄存器 elif op ADD: registers[x] registers[x] registers[y] elif op STORE: memory[y] registers[x] # 写回内存 elif op HALT: break pc 1 # 关键PC 自动 1 print(内存 0 号格子的值是:, memory[0])跑起来你会看到五行输出最后一行是8。就这二十来行把 CPU 的核心循环演完了。几个参数说明一下registers是寄存器只有 A 和 B 两个真实 CPU 里有几十个memory是内存只有 16 个格子真实机器上是几十亿个pc是程序计数器pc 1这一句就是“程序往下走”的全部真相。HALT那一支之所以要break是因为它不产生结果只是告诉循环“别转了”。3.3 改两个参数让程序“拐弯”只会一路往下走的程序是没用的所以下一步加一条跳转指令JMP让它学会往回跳program [ (LOAD, A, 0), # A 0当计数器 (LOAD, B, 1), # B 1每次加一 (ADD, A, B), # A A 1 (STORE, A, 0), # 结果写回内存 (JMP, 2, None), # 跳回第 2 条指令形成循环 ] steps 0 while steps 5: # 防止死循环只看 5 步 op, x, y program[pc] print(fPC{pc} {op} {x} {y} A{registers[A]}) if op LOAD: registers[x] y elif op ADD: registers[x] registers[x] registers[y] elif op STORE: memory[y] registers[x] elif op JMP: pc x - 1 # 先减 1下面统一 1 正好落在目标上 pc 1 steps 1这里pc x - 1是个小技巧因为循环末尾统一要pc 1所以先减掉 1加完正好停在目标指令上。跑起来你会看到 A 从 1 涨到 5。到这里你写的while循环在底层长什么样应该不用别人解释了——就是“条件判断 跳回去”。所有的高级语言结构拆到底都是这套东西。3.4 进阶半步换成 C 语言看看真实地址Python 把地址藏起来了想看真东西得换 C。装个gcc或者用任意在线编译环境保存成demo.c#include stdio.h int main(void) { int a 123; int b 456; int c a b; // %p 打印地址 是取地址%zu 打印字节数 printf(a 的地址 %p值 %d\n, (void*)a, a); printf(b 的地址 %p值 %d\n, (void*)b, b); printf(c 的地址 %p值 %d\n, (void*)c, c); printf(int 占 %zu 字节\n, sizeof(int)); return 0; }编译时一定要加-O0gcc -O0 -o demo demo.c ./demo原因很实在-O1以上的优化会把这些变量优化进寄存器甚至直接算完你可能连地址都打印不出来。-O0是“关掉优化”专门给看底层用的。三个变量的地址通常是连着的差 4 个字节因为int占 4 字节而且是从高往低排。这就把第 2 章讲的“栈”坐实了局部变量确实是一摞盘子一个挨一个放。提示这里打印出来的是虚拟地址不是物理内存条上的真实位置。现代操作系统都会给每个程序一套“假门牌号”这个概念知道就行先不用深究。4. 进制、字节与数据类型新手第一道容易翻车的坎4.1 二进制和十六进制记住 8421 就够用了计算机内部只有开和关所以一切都存成 0 和 1。但二进制写起来太长人看着累于是把 4 位二进制合成 1 位十六进制——这就是十六进制存在的全部理由。速算法只有一个8421。从右往左四位分别代表 8、4、2、1。十进制二进制十六进制101010A151111F12311110117B25511111111FF练两遍就熟了1010 8 2 10 A1111 8 4 2 1 15 F。看到0xFF就想到 255看到0x10就想到 16这就够用了不用再证明一遍原理。4.2 1 byte 8 bitschar、short、long 到底占多大字节是所有类型的基本单位。一个字节 8 位能表示 0 到 255或者 -128 到 127。多个字节拼起来才能装下更大的数所以有了下面这张表类型常见字节数说明char1存单个字符或小整数short2短整数int4最常用的整数类型long4 或 8跟平台有关最容易翻车的一个long long8大整数float4单精度小数double8双精度小数指针4 或 832 位系统 4 字节64 位系统 8 字节这张表里最需要标记的是long它在 32 位环境下是 4 字节64 位环境下常常是 8 字节。同一个数字123装进char和装进long里占的格子数完全不同。顺带回答一个常见疑惑为什么小数字也能放进大类型不直接全用long因为内存是有限的占的字节越多同样大的内存能装的东西越少读写也越慢。类型的意义就是“按需要多大就占多大”。4.3 动手把进制、字节、类型一次看清Python 里转换进制只要三个函数n 123 print(bin(n)) # 0b1111011 二进制 print(oct(n)) # 0o173 八进制 print(hex(n)) # 0x7b 十六进制 # 反过来把字符串按指定进制解析成整数 print(int(1111011, 2)) # 123 print(int(7b, 16)) # 123 print(int(173, 8)) # 123bin、oct、hex的参数都是整数返回带前缀的字符串int的第二个参数指定按几进制解析不写默认按十进制。这一组四个函数基本覆盖日常所有进制转换场景。C 语言里看字节数直接sizeof#include stdio.h int main(void) { printf(char %zu\n, sizeof(char)); printf(short %zu\n, sizeof(short)); printf(int %zu\n, sizeof(int)); printf(long %zu\n, sizeof(long)); printf(long long %zu\n, sizeof(long long)); printf(double %zu\n, sizeof(double)); printf(void* %zu\n, sizeof(void*)); return 0; }把这段跑一遍你机器上的真实答案就有了。别背表背表会记错——尤其是long和指针。4.4 高位补 0 与字节顺序一个真实的踩坑现场最后一个坑新手几乎必踩把一个小整数塞进大类型然后从二进制角度看发现高位全是 0。比如123存成short是00000000 01111011存成long是00000000 00000000 00000000 00000000 00000000 00000000 00000000 01111011。值没变但字节数变了。这本身没问题问题出在“你以为它是 1 字节实际占了 8 字节”。更麻烦的是跨字节读数据时的顺序问题import struct x 0x12345678 print(struct.pack(I, x).hex()) # 78563412 小端低位字节在前 print(struct.pack(I, x).hex()) # 12345678 大端高位字节在前表示小端表示大端I表示 4 字节无符号整数。同一个数两种读法得到的字节序列完全相反。你以后解析文件格式、读网络数据包如果发现“数字莫名变成几亿”十有八九就是这个没对上。注意这类问题不会报错程序照常运行就是结果不对。所以第一次接触二进制数据时先把原始字节打印出来看一眼别急着算。5. 新手最容易踩的五个坑常见问题排查清单5.1 一上来就啃汇编教材三天就放弃现象书买回来从第 1 页看到第 40 页指令表越背越多然后就没有然后了。原因没有具体问题驱动纯记忆型学习留不住。指令表离开场景就是一堆缩写。解决先带着一个具体问题看比如“我这个函数调用到底怎么传参的”。看第 2 章那种三行汇编片段就够了不需要覆盖全部指令集。用得上的查用不上的放着。5.2 以为 1 KB 就是 1000 字节现象算内存大小算出来的数和实际打印的字节数差一截。原因日常口语的 KB 和底层说的 KB 不是一回事。按 1024 进制换算或者按 1000 进制换算结果能差 2% 以上。解决动手算的时候一律以字节数为准sizeof打印出来是多少就是多少。需要换算时明确写清用哪种进制别靠心算。5.3 用 Python 打印“地址”以为看到了真实内存现象照着网上代码写id(x)打印出一串数字以为那就是内存地址。原因Python 的对象地址是解释器内部管理的而且对象可能被搬来搬去id只是个标识不等于可直接寻址的物理位置。解决想看真实地址就用 C配合-O0编译。Python 更适合验证逻辑不适合观察内存布局。5.4 以为 int 在哪台机器上都是 4 字节现象代码在自己电脑上跑得好好的换台机器或者换个平台结果莫名其妙。原因部分类型的字节数跟平台和编译器有关long尤其明显指针在 32 位和 64 位下也不一样。解决凡是依赖字节数的地方一律用sizeof拿不要写死 4 或 8。跨平台代码里这条是铁律。5.5 看到 call / return 就跳过结果看不懂崩溃栈现象程序报错抛出一大段函数调用栈你只认识最后一行前面全是系统函数名。原因不知道函数调用是在栈上摞盘子就看不懂“栈帧”这个词自然读不下来。解决回头把第 2.3 节再看一遍然后自己写个三层嵌套调用在每层打印变量地址看着地址一层层变小就有感觉了。以后看到崩溃栈至少知道要从下往上看。6. 下一步学什么一份能走完的三个月入门路径看完上面的例子最容易犯的错是马上冲去啃操作系统教材。我建议按下面的节奏走每一步都要有能跑出来的东西别只看不练。阶段学什么时间怎么验证自己学会了第 1 到 2 周进制转换、字节与类型、sizeof每天 30 分钟能手算十进制转十六进制能说出自己机器上各类型占几字节第 3 到 4 周内存与栈、变量地址布局每天 30 分钟能写 C 程序打印三个局部变量地址解释为什么连续排列第 5 到 6 周取指、译码、执行、PC 与跳转每周两次能把 Python 模拟器改成支持 5 条以上指令第 7 到 8 周函数调用、参数传递、调用约定每周两次能解释递归太深为什么会爆栈第 9 到 10 周程序怎么启动、引导与固件周末看能复述从通电到程序跑起来的大致顺序第 11 到 12 周挑一个方向深入压缩、链接库或设备交互自主安排能对着一个真实小工具说出它用到了哪些底层知识几个执行上的提醒。第一每学一个概念一定要落到一段能跑的代码上哪怕只有 10 行跑通的记忆比看十页书牢。第二别追求“全部搞懂”入门阶段允许有 20% 模糊地带先把主干打通。第三遇到看不懂的汇编片段先查指令含义再看地址别被dword ptr那串吓退它只是在说“这里取 4 个字节”。我第一次接触这些内容时最大的教训是贪多。当时一口气下载了十几份资料从指令集手册到内核源码结果一个月过去连一个变量在内存里长什么样都没亲手验证过。从那以后我每次学新东西都强制走一遍“先跑一个最小例子再回头补理论”的流程哪怕那个例子只有三行。底层知识这件事慢就是快。今天你先跑通模拟器那 20 行代码可能只花二十分钟但它会在你脑子里留下一张地图。以后无论碰到性能问题、崩溃栈还是各种诡异的数据错位你都知道该往地图的哪个角落去找。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站