学计算机系统基础,绕不开的一段就是常用机器指令。很多人第一次看汇编清单时,脑子里全是问号:这一串mov、add、jmp到底在干什么,为什么一条指令能决定程序跑得快不快,为什么指针越界会直接让进程崩掉。我在带新人和自己做底层调试的过程中发现,绝大多数“看不懂机器指令”的问题,不是记性不好,而是没有建立起“指令是硬件和软件之间的合同”这个认知。这篇文章面向三类人:正在学计算机系统基础的学生、刚转底层开发需要看反汇编的程序员、以及想搞明白程序在机器层面到底怎么跑的技术爱好者。我会把常用机器指令拆成可操作的模块,从指令格式、寻址方式到编码验证、排错实录,尽量做到看完就能上手对着反汇编窗口一条条读。
1. 机器指令到底在解决什么问题:从一行 C 代码说起
1.1 指令是软硬件之间的“合同条款”
先看一行再普通不过的代码:int a = b + 1;。在高级语言里这是一句话,在机器层面它会被拆成若干条常用机器指令。比如变量b存在内存某个位置,处理器需要先把这块内存的数据搬到寄存器,再让寄存器和一个立即数相加,最后把结果写回内存。每一步对应一条指令,每条指令又对应一串二进制编码。所谓指令集架构,本质上就是一份合同:软件按约定把操作写成指令编码,硬件按约定识别这些编码并执行。
这份合同规定了几个核心内容。第一,有哪些指令可用,这叫指令集。第二,指令长什么样,这叫指令格式。第三,操作数从哪里来,这叫寻址方式。第四,执行结果放到哪里、状态怎么记录,这涉及寄存器和标志位。理解这四件事,比死记助记符有用得多。我在实际排查问题时,经常遇到有人能背出mov是传送,却说不清mov eax, [ebx]和mov eax, ebx的差别,这种“背名词”式的学习在真实调试里几乎帮不上忙。
从从业者角度看,机器指令的学习价值不在于手写汇编,而在于建立一种“翻译能力”:看到高级语言能大致猜到会生成什么指令,看到反汇编能还原出程序在做什么。这种能力在性能优化、崩溃分析、安全审计里都是硬通货。你不需要成为汇编高手,但必须能读懂常见指令的意图。
1.2 为什么不能只背助记符:一次真实的误判
我踩过一个很典型的坑。早期分析一段崩溃现场时,看到反汇编里有一条mov把某个地址的值读进寄存器,我下意识判断是空指针,结果查了半天发现指针本身没问题,问题出在寻址方式上:指令用的是基址加变址的复合寻址,变址寄存器里的值是一个未初始化的临时变量,导致实际访问地址偏出去很远。指令没写错,是我把“读内存”这件事想得太简单了。
这件事让我明白一个道理:常用机器指令的难点从来不是助记符本身,而是助记符背后的“地址计算”和“数据宽度”。同一条mov,操作数宽度不同、寻址方式不同,生成的机器码长度可能差好几倍,执行开销也完全不同。所以后面我会把重点放在三件事上:指令格式怎么拆、地址怎么算、标志位怎么变。把这三件事吃透,再看任何一份反汇编清单都不会发怵。
另外提醒一句,学习阶段不要一上来就啃指令手册。手册是查阅用的,不是教材。正确的路径是先掌握十几条高频指令,理解它们的典型用法和副作用,再逐步扩展到边界情况。高频指令覆盖了绝大多数真实代码,剩下的冷门指令遇到再查完全来得及。
2. 指令格式与寻址方式:一条指令里的信息该怎么摆
2.1 操作码、操作数、地址码的分工
一条机器指令在二进制层面通常由若干字段组成:操作码字段说明“做什么”,操作数字段说明“对谁做”,地址码字段说明“去哪里找操作数”。不同架构对这些字段的划分差别很大。定长指令把每个字段的位置和宽度固定下来,译码器读起来很快;变长指令则通过前缀、扩展字节来灵活表达,代码密度高但译码复杂。
拿一个常见的变长架构举例,一条简单的寄存器传送指令可能只有一个字节的操作码,后面跟着由“模字段”和“寄存器字段”组成的寻址字节,再后面才是可选的位移量或立即数。这意味着同样一条mov,操作寄存器时可能只有两个字节,操作内存加偏移时可能变成六七个字节。你在反汇编窗口里看到指令长度参差不齐,根源就在这里。
理解字段划分的直接好处是能手动拆机器码。比如看到字节序列B8 78 56 34 12,如果知道B8是“把立即数装入某寄存器”的操作码,后面四个字节就是立即数本身,而且因为小端序,实际数值是0x12345678。这种“看字节还原指令”的能力,在分析没有符号表的二进制文件时特别有用。新手常犯的错误是忽略端序,把78 56 34 12读成0x78563412,结论就全错了。
注意:端序不是“大小端哪个更先进”的问题,而是硬件约定。分析机器码前先确认目标架构的端序,否则后面所有地址计算都会错。
2.2 常见寻址方式与选择逻辑
寻址方式决定了操作数从哪里取。最基础的是立即数寻址,操作数直接写在指令里,速度快但值固定;寄存器寻址从寄存器取值,速度最快,是所有计算的落脚点;直接寻址在指令里写一个内存地址;间接寻址先把地址存到寄存器或内存,再去取;基址加偏移寻址用一个基址寄存器加一个固定偏移,是访问结构体成员和数组元素的常用手段;变址寻址则是基址加变址寄存器再乘一个比例因子,专门服务数组遍历。
为什么要有这么多寻址方式?核心原因是代码里大量访问模式是重复的。访问局部变量是“栈指针加固定偏移”,访问结构体字段是“对象指针加固定偏移”,遍历数组是“基址加索引乘元素大小”。如果只有直接寻址,每次访问都要先用指令算出完整地址,指令条数会翻好几倍。硬件把这些高频模式固化成寻址方式,一条指令就能完成“算地址加取数据”,这是性能优化里非常经典的做法。
选择寻址方式时有几个实际考量。第一,比例因子的取值通常有限制,常见是 1、2、4、8,刚好对应字节、半字、字、双字这些常见元素宽度,超出范围就得先用指令把索引放大。第二,基址加偏移里的偏移量位宽有限,偏移太大时编译器会拆成多条指令或者借助临时寄存器。第三,复杂寻址不一定更快,某些架构上复合寻址会多一个周期的地址计算开销,编译器在优化等级较高时会做权衡。我个人的经验是,读反汇编时先看寻址方式,往往能立刻判断出这段代码在访问数组、结构体还是普通局部变量,比逐条猜语义快得多。
| 寻址方式 | 典型写法 | 常见场景 | 需要留意的点 |
|---|---|---|---|
| 立即数 | op reg, 0x10 | 常量运算、赋值 | 立即数位宽有限 |
| 寄存器 | op reg1, reg2 | 算术逻辑运算 | 无额外访存开销 |
| 直接 | op reg, [0x1000] | 全局变量 | 地址固定,重定位需处理 |
| 基址加偏移 | op reg, [base+8] | 结构体成员、局部变量 | 偏移位宽限制 |
| 变址 | op reg, [base+index*4] | 数组遍历 | 比例因子取值受限 |
| 相对 | jmp label | 条件跳转、位置无关代码 | 偏移相对当前指令 |
3. 六类常用机器指令逐个拆解
3.1 数据传送类:mov 家族的真实开销
数据传送是出现频率最高的一类指令,核心动作是把数据从一个位置搬到另一个位置。听起来简单,但它的变体极多,开销差异也大。寄存器到寄存器的传送通常在一个周期内完成,甚至在某些架构上会被寄存器重命名直接消除;寄存器到内存或内存到寄存器涉及访存,开销取决于缓存层级;内存到内存的传送在很多架构上不允许单条指令完成,需要拆成两条。
这里有个容易被忽略的细节:传送指令往往不影响标志位。也就是说mov执行完,之前的比较结果还在,这给编译器安排指令顺序留出了空间。但有些特殊传送指令会隐含影响状态,比如涉及段寄存器或特殊控制寄存器的操作,这类指令在系统级代码里才常见,普通应用代码基本遇不到。
实际写代码时,传送指令的优化空间主要在两处。一是减少不必要的往返,比如先把值存回内存再立刻读出来,编译器一般会优化掉,但在调试构建里经常能看到这种冗余。二是注意数据宽度匹配,把一个 32 位值写进 64 位寄存器时,不同架构对高位是清零还是保留有不同约定,搞错了会出现“高 32 位残留脏数据”的诡异问题。我在排查一个数值比较异常的问题时,最后定位到的就是某条传送指令的高位处理不符合预期,导致比较结果和想象中不一样。
实操心得:读反汇编时看到连续的
mov,先判断是参数准备、返回值搬运还是临时变量赋值。参数准备通常集中在调用指令之前,返回值搬运紧跟调用之后,临时变量赋值则零散分布。按这个规律切分,能快速理清一段代码的骨架。
3.2 算术与逻辑类:加减乘除和位运算的副作用
算术指令里最常用的是加法、减法和比较。加法会影响进位标志、零标志、符号标志和溢出标志,减法在硬件上通常复用加法器,把减数取补后相加,所以标志位的语义要按“减法”来理解,不能按加法套。这一点在分析条件跳转时特别关键:比较指令本质是做一次减法但不写回结果,只更新标志位,后面的条件跳转根据标志位决定走向。
逻辑指令包括与、或、异或、取反和移位。它们的典型用途是掩码提取、位标志设置和快速乘除。左移一位等价于乘二,右移一位等价于除二,这个结论只在无符号数或者确定为正数时成立,负数右移涉及符号扩展,结果依赖架构约定。我见过有人在性能敏感的代码里用手写移位代替乘除,结果在负数场景下算错了,原因就是忽略了算术右移和逻辑右移的区别。
乘除法指令要单独说。整数乘法通常有单操作数和双操作数两种形式,双操作数形式只保留低位结果,高位溢出直接丢掉;需要完整结果时必须用单操作数形式,结果会拆到两个寄存器里。整数除法更麻烦,除数为零会触发异常,而且除法指令延迟普遍较高,编译器会把常量除法优化成乘法和移位组合。如果你在反汇编里看到一堆乘法和移位却没有除法,别奇怪,那是编译器在做常量除法优化。
| 指令类别 | 典型指令 | 主要副作用 | 常见用途 |
|---|---|---|---|
| 加法 | add | 更新进位、零、符号、溢出标志 | 计数、地址计算 |
| 减法 | sub | 同上,按减法语义解释 | 差值计算、循环条件 |
| 比较 | cmp | 只更新标志位,不写结果 | 条件判断前置 |
| 逻辑 | and/or/xor | 更新零、符号标志,进位清零 | 掩码、标志位操作 |
| 移位 | shl/shr/sar | 更新进位、零、符号标志 | 快速乘除、位提取 |
| 乘法 | mul/imul | 可能更新进位和溢出标志 | 数值计算、地址缩放 |
| 除法 | div/idiv | 除零异常,结果商余分离 | 数值计算 |
3.3 比较与跳转类:标志位才是真正的开关
条件跳转是控制流的骨架,而它依赖的核心是标志位寄存器。常见的标志位有零标志、符号标志、进位标志和溢出标志。零标志表示上一次运算结果是否为零,符号标志表示结果最高位,进位标志表示无符号运算是否溢出,溢出标志表示有符号运算是否溢出。条件跳转指令就是这些标志位的组合查询:等于跳转看零标志,大于跳转同时看符号标志和溢出标志,无符号比较则看进位标志。
这里最容易出错的地方是有符号和无符号的条件跳转不能混用。同样是比较两个数,有符号场景下-1小于1,无符号场景下-1对应的全一模式是一个极大的正数。编译器会根据变量类型选择不同的跳转指令,如果你手工改汇编或者分析混淆过的代码,把这两类跳转搞混,逻辑就会完全反过来。我在分析一段校验逻辑时遇到过这种情况:源码里用的是无符号类型,反汇编里对应的是“高于则跳转”,我按有符号的“大于”去理解,结论完全相反,折腾了很久才反应过来。
无条件跳转则简单得多,直接改指令指针。它有两种常见形式:相对跳转和绝对跳转。相对跳转的偏移是相对于当前指令的下一条指令计算的,这种设计让位置无关代码成为可能,代码被加载到任意地址都能正确跳转。绝对跳转直接给出目标地址,在需要跳到固定入口或者跳转表时使用。
循环结构在机器层面通常是这样组织的:初始化计数器,循环体开头做一次比较,根据结果条件跳转到循环结束,循环体末尾做计数器增减,然后无条件跳回开头。理解这个固定套路后,看反汇编里的循环会非常快,基本扫一眼就能定位循环边界和退出条件。
3.4 栈操作与函数调用:call 和 ret 背后的完整动作
函数调用是机器指令里协作性最强的一组。调用指令做两件事:把返回地址压入栈,然后跳转到目标地址。返回指令从栈顶弹出返回地址并跳转回去。看似只有两步,但围绕栈的约定非常多,包括参数怎么传、返回值放哪、哪些寄存器由调用方保存、哪些由被调用方保存、栈帧怎么建立和销毁。
常见的调用约定里,前几个参数通过寄存器传递,多出来的参数通过栈传递,返回值放在固定寄存器。被调用方进入函数后通常先保存旧的栈基址,把当前栈指针赋给栈基址,再调整栈指针为局部变量腾空间。函数结束时恢复栈指针和栈基址,然后返回。这一套动作在反汇编里非常规整,我通常把它称为“函数序言和尾声”,看到这两段就能准确切分函数边界。
栈的生长方向也是个容易混淆的点。多数主流架构的栈向低地址方向生长,所以“压栈”是栈指针减小,“出栈”是栈指针增大。局部变量的地址通常低于栈基址,参数地址通常高于栈基址。分析栈溢出或者缓冲区越界时,这个方向感必须清楚,否则会把覆盖方向搞反。
注意:不要假设所有架构的调用约定一致。参数寄存器编号、栈对齐要求、返回值位置都可能不同。跨架构分析二进制时,先确认调用约定再下结论。
栈还有一个常被忽略的作用:它是调试信息的来源。调用栈回溯就是沿着栈帧链一路往上找返回地址,每一层帧指针保存了上一层帧指针的位置,形成一条链。理解这条链的构造方式,你就能明白为什么有些崩溃现场能打出完整调用栈,有些只能打出一层。编译优化开启后帧指针可能被省略,回溯就会变得困难,这是优化构建更难调试的原因之一。
3.5 加载有效地址、空操作与系统调用
有一类指令不访问内存,只做地址计算,典型代表是加载有效地址指令。它把基址加偏移的计算结果直接写入寄存器,不产生访存。这条指令常被编译器用来做乘加运算,因为它的地址计算单元支持比例因子,一条指令就能完成“寄存器乘比例加偏移”的效果,比先用乘法再用加法快。你在反汇编里看到它出现在明显不是取地址的场景,不要疑惑,那多半是编译器的算术优化。
空操作指令看起来没用,实际用途不少。它可以用于指令对齐,让关键循环入口落在缓存行边界上;可以用于占位,方便后续打补丁;在流水线架构上还可以用于延迟槽填充。某些架构的跳转指令后面有一条固定的延迟槽指令,无论跳转是否发生都会执行,编译器会把一条有用的指令塞进去,塞不进去就放空操作。
系统调用指令是用户态进入内核态的唯一正规入口。它把调用号放进约定寄存器,参数按约定摆放,然后触发陷入。内核根据调用号分发处理,完成后把返回值放回约定寄存器。这里的关键是调用号和参数寄存器在不同架构上完全不同,而且系统调用约定和普通函数调用约定往往不一样。分析程序行为时,如果看到系统调用指令,基本可以判断这里在做文件操作、内存映射、进程控制或者网络收发。
| 指令 | 是否访存 | 典型用途 | 易错点 |
|---|---|---|---|
| 加载有效地址 | 否 | 地址计算、乘加优化 | 误判为普通取数 |
| 空操作 | 否 | 对齐、占位、延迟槽 | 误判为无用代码删除 |
| 系统调用 | 是(内核态) | 文件、内存、进程、网络 | 调用号和参数约定不同 |
| 陷入指令 | 是(内核态) | 断点、异常触发 | 与系统调用混淆 |
4. 从汇编到机器码:手算编码与反汇编验证
4.1 一段真实指令序列的字节级拆解
光看助记符不够,动手拆一次机器码,理解会深很多。假设有一段典型的函数开头,做了保存栈基址、建立新栈帧、分配局部空间这几件事。对应的指令序列大致是:压栈保存旧基址,把栈指针赋给基址寄存器,栈指针减去一个固定值。逐条看编码,压栈指令通常只有一个字节的操作码;传送指令是两个字节,一个操作码加一个寻址字节;减法立即数指令可能是三个字节,操作码、寻址字节加一个单字节立即数。
把这些字节按顺序排好,你就得到了一小段机器码。再往后是函数体,最后是恢复栈帧和返回。恢复动作通常有一条专门的离开指令,等价于“栈指针回到基址,然后弹出旧基址”,最后一条返回指令只有一个字节。整段函数从入口到返回,机器码可能只有十几个字节到几十个字节,但完成的工作非常完整。
这里要强调小端序的影响。多字节立即数和地址在内存里是低位字节在前。手算时如果不做转换,读出来的数值会是反的。我的习惯是先在纸上标好每个字段的宽度,再按小端序重组,最后和反汇编工具的输出对照。对不上就逐字节检查,通常错误出在字段宽度判断或者前缀字节被漏掉。
4.2 用反汇编工具做交叉验证
手算之后一定要用工具验证。把机器码写进一个二进制文件,用反汇编工具按目标架构反汇编,看输出是否和预期一致。这个动作看起来笨,但它是建立“字节到指令”直觉的最快方式。我建议新手至少完整做五次:一次寄存器传送,一次立即数传送,一次内存访问,一次条件跳转,一次函数调用。做完这五次,再看反汇编窗口里的十六进制列就不会发懵。
验证过程中有几个高频陷阱。第一,架构模式选错。同一个架构可能有多种位宽模式和指令模式,选错了反汇编出来全是乱码。第二,起始偏移没设对。反汇编工具默认从文件开头按指令边界解析,如果文件前面有头部信息,需要手动指定代码段偏移,否则整段都会错位。第三,数据段被误当代码。反汇编器不会自动区分代码和数据,遇到跳转表或者字符串常量,会强行解析成指令,输出一堆莫名其妙的内容。看到不合常理的指令序列,先怀疑这里其实是数据。
实操心得:反汇编输出里的地址列、字节列、助记符列要结合起来看。字节列能告诉你指令实际占多长,地址列能告诉你下一条指令从哪里开始。只看助记符列容易忽略指令长度,遇到变长指令架构就会算错跳转目标。
交叉验证还有一个进阶用法:修改机器码里的某个立即数或者寄存器字段,观察程序行为变化。这是理解指令字段最直接的方式。比如把条件跳转的條件码改掉,程序逻辑立刻改变;把传送指令的目标寄存器改掉,后续计算全部受影响。这种实验比看书印象深得多,但一定要在隔离环境里做,别拿正式代码练手。
5. 常见问题与排查实录
5.1 非法指令、对齐异常与段错误的分辨
调试底层问题时,最先要分清三类错误。第一类是非法指令异常,处理器遇到无法识别的操作码或者当前特权级不允许的指令时触发。典型原因包括跳转到了数据区、函数指针被破坏、指令集特性未启用。第二类是对齐异常,某些架构要求访存地址按数据宽度对齐,不对齐就触发异常。第三类是段错误,访问了未映射或者无权限的地址。
这三类错误的现象很像,都是进程突然终止,但排查路径完全不同。非法指令要看指令指针指向哪里,那个位置原本应该是什么;对齐异常要看访存地址的低位,判断是哪个操作数越界;段错误要看地址是否落在合法映射区间内。我在实际排查中总结了一个顺序:先看错误类型,再看出错地址,最后看指令和操作数。顺序反了容易在错误的方向上浪费大量时间。
还有一个隐蔽情况是指令缓存和自修改代码。程序在运行时修改自己的指令,修改完成后必须刷新指令缓存,否则处理器可能执行旧指令。这类问题在高性能场景和某些保护机制里会出现,现象是“代码明明改了但行为没变”,非常难查。遇到这种症状,先确认是否有代码修改动作,再确认是否做了缓存同步。
5.2 常见问题速查表与独家避坑技巧
下面这张表是我自己排查时常用的速查清单,按现象归类,方便快速定位方向。
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 进程收到非法指令信号 | 跳转到数据区、函数指针被覆盖 | 检查指令指针附近内存内容 |
| 进程收到对齐异常信号 | 访存地址未按宽度对齐 | 打印访存地址低位,检查操作数 |
| 进程收到段错误信号 | 空指针、越界、权限不足 | 核对地址映射与访问权限 |
| 计算结果与预期不符 | 有符号无符号跳转混用、高位残留 | 检查条件跳转类型和传送宽度 |
| 函数返回后崩溃 | 栈被破坏、返回地址被覆盖 | 检查栈帧完整性和缓冲区边界 |
| 循环提前退出或死循环 | 标志位被中间指令改写 | 检查比较与跳转之间是否有干扰指令 |
| 反汇编输出乱码 | 架构模式错误、代码数据未区分 | 确认架构模式,标记数据区 |
几条独家避坑技巧。第一,遇到标志位相关的问题,把比较指令和跳转指令之间的所有指令列出来,逐条看是否改写了标志位。有些算术指令会意外清除进位标志,夹在中间就会让跳转条件失效。第二,分析栈问题时,先在纸上画出栈的生长方向和当前栈指针位置,再标出每个局部变量和参数的偏移,比在脑子里空想可靠得多。第三,怀疑指令编码问题时,不要只看反汇编助记符,一定要看原始字节,助记符是工具翻译的,字节才是硬件真正执行的东西。
还有一条经验:不要迷信优化后的反汇编。优化等级高时,编译器会重排指令、复用寄存器、内联函数、消除冗余,反汇编和源码的对应关系会变得很弱。调试优化构建时,优先看关键变量的值和控制流走向,不要试图逐行对应源码。真需要逐行对应,就用调试构建复现问题,定位到大致范围后再回到优化构建验证。
6. 不同指令集的设计取舍:为什么指令长得不一样
6.1 定长与变长:译码复杂度换代码密度
指令集设计里最基础的取舍是定长还是变长。定长指令每条宽度相同,译码器可以在固定位置切分指令边界,硬件实现简单,流水线效率高,代价是代码密度低,简单指令也要占满整个宽度。变长指令按需分配字节,常用指令可以很短,代码密度高,代价是译码复杂,需要先解析前缀和长度才能确定指令边界。
这个取舍直接影响你读反汇编的体验。定长架构的指令地址总是按固定步长递增,跳转目标好算;变长架构的指令地址参差不齐,算跳转偏移时必须知道每条指令的实际长度。我在两种架构之间切换分析时,最容易犯的错误就是按定长思维去数指令条数,结果偏移算错。后来养成习惯,先确认指令集是定长还是变长,再决定怎么估算地址。
代码密度在实际场景里是有意义的。同样一段逻辑,变长编码可能只占定长编码六七成的空间,对指令缓存有限的嵌入式场景影响明显。但定长编码的译码功耗和流水线效率优势也实实在在。所以你会看到桌面和服务器架构倾向变长以兼容历史积累,移动和嵌入式架构倾向定长以控制功耗,新兴的开源指令集则在这两者之间做新的平衡。
6.2 寄存器数量与寻址复杂度:编译器友好度之争
寄存器数量直接决定编译器有多大的调度空间。寄存器多的架构可以把更多中间结果留在寄存器里,减少访存;寄存器少的架构则需要更频繁地读写栈,指令条数多但每条指令短。这也是为什么同一段代码在不同架构上编译出来,指令条数可能差很多,但实际性能未必差那么多,因为瓶颈可能在访存或者分支预测上。
寻址复杂度也是类似道理。复杂寻址让一条指令完成更多工作,代码紧凑,但硬件地址计算单元更复杂,高频率复合寻址可能成为瓶颈。简单寻址把地址计算拆成多条指令,代码变长,但每条指令的时序更可预测。实际选型时没有绝对优劣,要看目标场景是追求峰值性能、功耗控制还是代码体积。
从学习角度看,我建议先精通一种架构的常用指令,再横向对比另一种。对比时重点看三处:函数调用约定、条件跳转的条件码设计、立即数的位宽和编码方式。这三处最能体现设计哲学差异。比如条件执行能力,有的架构支持几乎每条指令带条件前缀,能减少大量短分支;有的架构只支持独立的条件跳转,短分支靠预测器兜底。理解这些差异,你再看跨平台代码的性能表现就会更有判断力。
| 设计维度 | 一种取向 | 另一种取向 | 影响 |
|---|---|---|---|
| 指令长度 | 定长 | 变长 | 译码复杂度与代码密度 |
| 寄存器数量 | 较多 | 较少 | 寄存器压力与访存频率 |
| 寻址方式 | 简单 | 复杂 | 指令条数与地址计算开销 |
| 条件执行 | 独立跳转 | 指令级条件 | 短分支效率 |
| 调用约定 | 寄存器传参为主 | 栈传参为主 | 调用开销与栈使用量 |
最后分享一个我自己的学习习惯:每学一条新指令,就在纸上写三行——它做什么、它改哪些标志位、它的典型编码长度。坚持一段时间后,你会发现自己看反汇编的速度明显变快,遇到没见过的指令也能根据操作码位置和操作数形式猜出大概用途。机器指令这部分内容,看十遍不如动手拆一遍,找一段自己写的小程序,编译后反汇编,逐条对照源码看,收获会比啃手册大得多。