想象有个快递员叫小E,他的工作是在一座巨大城市里按门牌号送货。这里的门牌号就是内存地址,每个门牌里放着的“货物”就是一条指令。小E随身带着一本线路本,上面写着“下一站去几号门牌”,这个线路本就是 CPU 的指令指针 EIP/RIP。平时他从一条街走到下一条街,这叫顺序执行;遇到路口时,红绿灯决定他是直行还是转弯,这就是条件跳转 JCC;当他需要去仓库取货时,会在便利贴堆上写一张“办完事回这里”的便签,然后跑出去,这就是 CALL;办完事回来,撕下最上面那张便利贴继续走,这就是 RET。这篇文章就是跟着小E跑一趟,把 x86 控制流指令彻底看懂。

岗位关联

学懂控制流指令,你就能读懂编译器生成的跳转、函数调用和反汇编代码;这对 AI 算子的性能优化、二进制安全分析和推理引擎故障排查都至关重要。

目录
  1. 可视化演示
  2. 指令指针之谜
  3. 标志位侦探
  4. JCC十字路口
  5. CALL出差流程
  6. RET回家之路
  7. 循环与实战
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示二叉搜索树的逐元素插入过程,演示比较路径和节点放置规则,支持步进控制和自动播放。

本节配有交互动效(进入视口后自动加载,约 3KB)。

二叉搜索树构建 紫色节点为已插入的树节点 粉色高亮为新插入节点 left < node < right 每个节点左子树值小于父节点。

指令指针之谜

快递员小E每天沿着街道送快递,他手里那本“线路本”上写着的不是快递内容,而是“下一站去哪条街”的门牌号。在 CPU 里,这个线路本就是指令指针寄存器:32 位模式下叫 EIP,64 位模式下叫 RIP。CPU 每执行完一条指令,线路本就会自动翻到下一页,于是程序按顺序一条条执行下去。这就是控制流最朴素的形式:顺序执行。

但是真实程序不可能永远直线走下去。if、switch、循环、函数调用,这些高级语言结构在机器码层面都会变成“修改指令指针”的操作。x86 的跳转指令本质上就是给 EIP/RIP 写一个新值。需要注意的是,绝大多数跳转并不直接写一个绝对地址,而是写一个“相对偏移量”,意思是“从下一条指令开始,再向前或向后走多少步”。

回到快递员的比喻:EIP 不是“当前站在哪栋楼”的绝对坐标,而是“我接下来要去哪栋楼”的导航。跳转指令不是在门上写死一个路名,而是告诉他“从下下个路口再右转三米”。这种相对寻址让代码可以整体搬动位置而不必修改跳转目标,这正是动态链接库和地址随机化能工作的基础。

target=next_ip+offset,next_ip=cur_ip+insn_len\text{target} = \text{next\_ip} + \text{offset},\quad \text{next\_ip} = \text{cur\_ip} + \text{insn\_len}
这条公式是理解所有跳转指令的钥匙:CPU 先算好下一条指令的位置,再加上一个有符号的偏移量,得到真正要跳去的地方。
  1. 取完当前指令CPU 先根据指令指针取出当前指令,然后自动把指针指向紧挨着的下一条指令。
    next_ip=cur_ip+insn_len\text{next\_ip} = \text{cur\_ip} + \text{insn\_len}
  2. 读偏移量跳转指令自带 8 位或 32 位有符号位移,表示“从下一站再走几步”。
    offset=imm8 或 imm32\text{offset} = \text{imm8} \text{ 或 } \text{imm32}
  3. 计算目的地把下一条指令的地址和偏移量相加,得到真正的跳转目标。
    target=next_ip+offset\text{target} = \text{next\_ip} + \text{offset}

标志位侦探

每个路口都有红绿灯,但这些灯不是随便亮的,而是由快递员刚刚做过的那件事决定。在 CPU 里,这些灯就是标志位,保存在 EFLAGS/RFLAGS 寄存器中。最常用的四个灯是:ZF(零标志)、SF(符号标志)、CF(进位/借位标志)、OF(溢出标志)。CMP 这类指令就像两台秤比重量:它做一次减法,但结果不写进寄存器,只把红绿灯的状态留下。

具体来说,CMP EAX, EBX 会把 EAX 减去 EBX,然后根据结果设置标志位。如果两者相等,差为 0,ZF 就亮起;如果结果为负数,SF 就亮起;如果无符号数发生了借位,CF 亮起;如果有符号数发生了溢出,OF 亮起。后面的条件跳转指令并不关心被测数据本身,只关心这些灯是红是绿。

快递员小E的比喻在这里很贴切:他不用记住刚才那趟到底运了多少东西,只需要看路口上方哪盏灯亮着,就能决定转弯还是直行。条件跳转指令永远不问“刚才发生了什么”,只问“现在哪盏灯亮着”。

ZF=1  ⟺  结果=0,SF=结果的最高位,CF=1  ⟺  无符号借位/进位,OF=1  ⟺  有符号溢出\text{ZF}=1 \iff \text{结果}=0,\quad \text{SF}=\text{结果的最高位},\quad \text{CF}=1 \iff \text{无符号借位/进位},\quad \text{OF}=1 \iff \text{有符号溢出}
标志位是算术指令和环境跳转指令之间的“暗号”:算术指令负责把灯点亮,JCC 负责看灯决定走向。
  1. ZF 零标志如果运算结果为 0,ZF 置 1;只要有一个比特不为 0,ZF 就清 0。
    ZF=1  ⟺  (result==0)\text{ZF} = 1 \iff (\text{result} == 0)
  2. SF 符号标志把结果的最高位(符号位)拷贝到 SF,结果为负时 SF 为 1。
    SF=result 的符号位\text{SF} = \text{result 的符号位}
  3. CF 进位/借位标志无符号加法产生进位,或无符号减法产生借位时,CF 置 1。
    CF=1  ⟺  无符号进位或借位发生\text{CF} = 1 \iff \text{无符号进位或借位发生}
  4. OF 溢出标志两个同符号有符号数相加减,得到相反符号结果时,OF 置 1。
    OF=1  ⟺  有符号运算结果溢出\text{OF} = 1 \iff \text{有符号运算结果溢出}

JCC十字路口

当快递员走到一个路口,到底该不该转弯?答案是看红绿灯。JCC 就是一组“看灯转弯”的指令,CC 是 Condition Code(条件码)。最常用的是 JZ/JE(ZF=1 时跳转)、JNZ/JNE(ZF=0 时跳转)、JB/JAE(CF 为 1/0 时跳转)、JL/JGE(有符号比较,用 SF 和 OF 的组合判断)。JMP 则是无条件的,相当于红绿灯永远绿灯,不管什么情况都直接转弯。

编码上,短跳转的 JCC 通常是一个字节的操作码加上一个有符号的 8 位偏移。比如 JZ rel8 的机器码可能是 74 02,地址范围很有限;如果需要跳得更远,就要用近跳转,形如 0F 84 rel32,偏移量是 32 位。无论哪种,跳转目标计算都遵循“下一条指令地址 + 偏移量”的公式。

在路口比喻里,JCC 是写在指示牌上的规则,标志位是红绿灯。JMP 是“不管灯直接走”,JZ 是“零灯亮了才走”,JNZ 是“零灯没亮才走”。读汇编时看到一串 JCC,其实就是在还原代码里的 if、for、while 这些分支结构。

JCC 跳转  ⟺  条件为真,target=next_ip+imm8/imm32\text{JCC 跳转} \iff \text{条件为真},\quad \text{target}=\text{next\_ip}+\text{imm8/imm32}
JCC 本身不负责判断数据,它只根据标志位做出“跳”或“不跳”的决定,所以理解标志位是看懂 JCC 的前提。
  1. 匹配条件码JZ、JNZ、JB、JL 等助记符对应一组标志位状态,条件满足才跳转。
    condition(JCC)=f(ZF,SF,CF,OF)\text{condition}(\text{JCC}) = f(\text{ZF}, \text{SF}, \text{CF}, \text{OF})
  2. 读取偏移量指令中紧跟着操作码的 imm8 或 imm32 是有符号数,表示相对下一条指令的位移。
    offset=imm8 或 imm32\text{offset} = \text{imm8} \text{ 或 } \text{imm32}
  3. 更新线路本条件成立时,把 target 写入 EIP/RIP;条件不成立则继续顺序执行。
    EIP←target若不成立,则 EIP 不变\text{EIP} \leftarrow \text{target} \quad \text{若不成立,则 EIP 不变}

CALL出差流程

小E不是只会一条街送到黑,偶尔他需要去一个专门的仓库领特殊包裹。出发前,他会把一张便利贴写上“这边办完还要回 xx 号门牌”,然后压在便利贴堆的最上面,再跑去仓库。这张便利贴就是返回地址。在 x86 中,这条指令叫 CALL。

CALL 做了两件事:先把下一条指令的地址压入栈中,再把 EIP 改成目标子程序的地址。所谓“压入栈”,就是让栈顶指针 ESP 先向下移动一个单位,然后把返回地址写到新栈顶。因为栈向低地址方向增长,所以 ESP 是减小的。

调用约定(Calling Convention)在此基础上决定了参数怎么传、由谁清理栈。比如 cdecl 由调用者清理参数,stdcall 由被调用者清理。无论哪种约定,CALL 自己只负责压返回地址,剩下的规则是程序员或编译器共同遵守的“快递公司章程”。

ESP←ESP−k,[ESP]←next_ip,EIP←target, k=4 或 8\text{ESP}\leftarrow\text{ESP}-k,\quad [\text{ESP}]\leftarrow\text{next\_ip},\quad \text{EIP}\leftarrow\text{target},\ k=4\text{ 或 }8
CALL 的本质是“先留后路,再出发”:返回地址被安全地放在栈顶,子程序跑完后再靠 RET 找到回家的路。
  1. 缩小栈顶栈向低地址生长,ESP 减小 k 个字节(32 位为 4,64 位为 8)。
    ESP←ESP−k\text{ESP} \leftarrow \text{ESP} - k
  2. 写入返回地址把 CALL 的下一条指令地址写到新栈顶的位置。
    [ESP]←next_ip[\text{ESP}] \leftarrow \text{next\_ip}
  3. 跳转子程序把目标地址写入 EIP,CPU 开始执行子程序。
    EIP←target\text{EIP} \leftarrow \text{target}

RET回家之路

小E在仓库办完事,要怎么回到原来的路线?他不需要重新规划导航,只需要撕下便利贴堆最上面那张,上面写着“回 xx 号门牌”。这就是 RET 指令。它从栈顶弹出返回地址,写回 EIP,同时让 ESP 向上移动,栈空间被释放。

RET 还有一种形式是 RET n,意思是返回后额外从栈顶弹出 n 个字节,常用于调用者把参数也放在栈上的场景。不过要记住,RET 只负责弹出返回地址,不会奇迹般地清理所有局部变量。如果函数开头压了太多东西,却没有在返回前恢复栈指针,RET 弹出来的可能不是返回地址,而是某个局部变量,程序就会“迷路”。

如果 CALL 是“写好便利贴再出门”,RET 就是“撕下便利贴回家”。栈上所有便利贴遵循后进先出规则:最后写下的那张永远在最上面,所以函数嵌套调用可以一层一层正确返回。一旦中途有一张便利贴被意外撕掉或没写,整个路线就会乱套。

EIP←[ESP],ESP←ESP+k\text{EIP}\leftarrow[\text{ESP}],\quad \text{ESP}\leftarrow\text{ESP}+k
RET 是 CALL 的逆操作:一个把返回地址压栈,一个把返回地址弹出来;两者必须成对出现,栈才能保持平衡。
  1. 读取栈顶把内存中 ESP 指向的地址里保存的数据取出,作为返回地址。
    EIP←[ESP]\text{EIP} \leftarrow [\text{ESP}]
  2. 扩大栈顶ESP 加上 k,把已经弹出的空间回收。
    ESP←ESP+k\text{ESP} \leftarrow \text{ESP} + k
  3. 继续执行CPU 从新 EIP 开始取指令,快递员回到原来的街道继续送件。
    继续执行地址为 EIP\text{继续执行地址为 EIP}

循环与实战

小E有一种常见任务:把同一批货物挨家挨户送完,从第一家跑到最后一家,然后回过头来再跑一遍,直到某个条件满足。这就是循环。在汇编层面,循环通常不是一个专门的语句,而是由“计数器 + 条件跳转”拼出来的。

最简单的方式是用 ECX/RCX 做计数器:先设一个初值,每次循环减一,然后用 JNZ 跳回循环开头。也可以用 CMP 比较一个值,再用合适的 JCC 判断是否继续。编译器有时会把高级语言的 for (;;) 转换成 DEC + JNZ,有时会为了性能展开循环,所以反汇编看到的循环并不总是长得一模一样。

实战中阅读反汇编,建议先找 JCC 的跳转目标,把它看成画流程图的箭头。向下跳转通常是 if 分支,向上跳转通常是循环。只要把这些箭头还原成流程图,再配合 CALL/RET 圈出函数边界,一段陌生汇编代码的骨架很快就能浮出水面。

ECX←ECX−1,若 ECX≠0 则 EIP←loop_start\text{ECX}\leftarrow\text{ECX}-1,\quad \text{若}\ \text{ECX}\neq 0\ \text{则}\ \text{EIP}\leftarrow\text{loop\_start}
循环的本质是“往回跳”:只要条件不满足,EIP 就一直被拉回循环起点,直到计数器归零。
  1. 计数器减一每次循环都将 ECX 减 1,同时影响 ZF。
    ECX←ECX−1\text{ECX} \leftarrow \text{ECX} - 1
  2. 判断条件如果 ECX 不为 0,ZF 为 0,于是 JNZ 条件成立。
    ZF=1  ⟺  ECX=0\text{ZF} = 1 \iff \text{ECX} = 0
  3. 跳回起点条件成立时把 EIP 改成循环开始处的地址,形成回路。
    EIP←loop_start\text{EIP} \leftarrow \text{loop\_start}

AI 应用场景

恶意代码逆向分析

阶段:样本分析与特征提取

解决的问题:传统方式靠人肉阅读大量机器码,效率低;AI 模型可以从控制流图中提取行为特征,但前提是分析者必须先看懂 JCC/CALL/RET 还原出的流程图。

具体案例:安全研究员拿到一个混淆过的木马样本,发现它用大量 JCC 拼接成虚假分支,真实逻辑藏在 CALL 和 RET 的栈匹配关系中。通过给 CALL/RET 下断点、跟踪返回地址,最终定位到一个解密循环。

AI编译器指令级优化

阶段:高性能算子编译与优化

解决的问题:在 GPU/NPU 时代,CPU 上的 AI 算子仍需高效执行;编译器开发者需要理解 JCC 的预测代价,把热点循环改造成分支更少的版本。

具体案例:优化一个矩阵归一化算子时,发现循环内频繁出现 CMP 和 JCC,分支预测错误率很高。编译器工程师改用无分支指令,吞吐量提升了约 18%。

推理引擎崩溃栈回溯

阶段:线上服务故障排查

解决的问题:大模型推理服务崩溃时,经常只剩一份 core dump;如果不懂 CALL/RET 与栈结构,就无法从回溯信息中定位非法访问发生在哪一层函数。

具体案例:一个推理引擎在并发请求下崩溃,gdb 显示 RET 返回地址被破坏。顺着栈帧逐层检查,发现某个自定义算子多压了一个局部变量,破坏了返回地址。

Python 演示

python88 行
# -*- coding: utf-8 -*-class CPU:    def __init__(self, program):        self.program = program        self.eip = 0        self.regs = {'eax': 0, 'ebx': 0, 'ecx': 0, 'esp': 0x7ff0}        self.flags = {'ZF': False, 'SF': False, 'CF': False, 'OF': False}        self.stack = []        self.labels = {}        for i, item in enumerate(program):            if item[0] == 'label':                self.labels[item[1]] = i     def val(self, x):        return self.regs[x] if isinstance(x, str) else x     def exec_one(self, op, *args):        if op == 'mov':            self.regs[args[0]] = self.val(args[1])        elif op == 'add':            self.regs[args[0]] += self.val(args[1])        elif op == 'sub':            self.regs[args[0]] -= self.val(args[1])        elif op == 'mul':            self.regs[args[0]] *= self.val(args[1])        elif op == 'cmp':            r = self.regs[args[0]] - self.val(args[1])            self.flags['ZF'] = (r == 0)            self.flags['SF'] = (r < 0)        elif op == 'jmp':            self.eip = self.labels[args[0]]            return False        elif op == 'jz':            if self.flags['ZF']:                self.eip = self.labels[args[0]]                return False        elif op == 'jnz':            if not self.flags['ZF']:                self.eip = self.labels[args[0]]                return False        elif op == 'call':            self.stack.append(self.eip + 1)            self.eip = self.labels[args[0]]            return False        elif op == 'ret':            self.eip = self.stack.pop()            return False        elif op == 'hlt':            self.eip = len(self.program)            return False        return True     def run(self, trace=False, max_steps=100):        steps = 0        while self.eip < len(self.program) and steps < max_steps:            op, *args = self.program[self.eip]            if op == 'label':                self.eip += 1                continue            if trace:                print('eip=%d op=%s args=%s regs=%s flags=%s' %                      (self.eip, op, args, self.regs, self.flags))            if self.exec_one(op, *args):                self.eip += 1            steps += 1        return self.regs['eax'] def main():    program = [        ('mov', 'ecx', 5),        ('mov', 'eax', 0),        ('label', 'sum_loop'),        ('add', 'eax', 'ecx'),        ('sub', 'ecx', 1),        ('jnz', 'sum_loop'),        ('call', 'triple'),        ('hlt',),        ('label', 'triple'),        ('mov', 'ebx', 3),        ('mul', 'eax', 'ebx'),        ('ret',)    ]    cpu = CPU(program)    result = cpu.run(trace=True)    print('result =', result) if __name__ == '__main__':    main()
目的:用 Python 模拟一个极简 x86 控制流机,展示 JNZ 实现循环、CALL 压返回地址、RET 弹返回地址的完整过程。
  1. self.labels[item[1]] = i预扫描所有 label,相当于给每个分店门牌号建立索引,跳转指令才能找到目标。
  2. self.flags['ZF'] = (r == 0)CMP 执行减法后设置零标志,这就是 JZ/JNZ 判断的依据。
  3. elif op == 'jnz':条件跳转只有在 ZF 为 0 时才执行,否则顺序继续走。
  4. self.stack.append(self.eip + 1)CALL 把下一条指令地址压入栈,这就是快递员写下“回这里”的便利贴。
  5. self.eip = self.stack.pop()RET 弹出栈顶地址并写回 EIP,快递员撕下便利贴回家。
  6. ('jnz', 'sum_loop')计数器减一后向上跳回循环起点,这是 x86 循环最常见的机器码形态。

常见误区

误区我读机器码时看到 JCC 后面跟了一个数字,以为这个数字就是 CPU 要跳去的绝对地址,手动算半天都对不上。

正解x86 的短跳转和近跳转大多数是相对跳转,指令里的数字是“相对下一条指令的偏移量”。目标地址必须用 next_ip + offset 计算,而不是直接把数字当地址。就像快递员看到的指示牌写的是“再走三栋楼”,而不是直接写“去 10086 号楼”。

为何常见:许多反汇编器默认显示最终目标地址,让人误以为机器码里也存着绝对地址。

误区我以为 CALL 一执行,CPU 就会把所有寄存器都保存好,子程序随便用,返回时寄存器自动还原。

正解CALL 只做两件事:压返回地址、跳转目标地址。寄存器保护不是硬件保证的,而是靠调用约定和函数开头结尾的 push/pop 实现的。如果被调函数不遵守约定,调用者的寄存器就会丢。

为何常见:高级语言封装了函数调用细节,程序员很少看到真实的寄存器保护过程。

误区函数返回时,RET 会把栈上所有局部变量都弹出去,所以栈水平不水平不需要我操心。

正解RET 只弹出栈顶的返回地址,局部变量必须由函数自己的收尾代码清理,比如先用 LEAVE 恢复 EBP/ESP,再执行 RET。只靠 RET 清除局部变量是不可能的。

为何常见:看到 RET 在函数末尾,就误以为它是万能清理器,忘记还有栈帧这一层结构。

误区条件跳转判断的永远是上一次 CMP 的结果,所以我只会在 CMP 后面看 JCC。

正解几乎所有算术和逻辑指令都会更新标志位,比如 ADD、SUB、DEC、INC、TEST。只要前一条指令改了 ZF,后面的 JZ/JNZ 就能根据它跳转。CMP 只是专门为比较设计的典型例子。

为何常见:教材为了讲清楚 JCC,总是拿 CMP 搭配举例,导致初学者误以为只有 CMP 能改变标志位。

误区我以为 x86 里循环就应该用 LOOP 指令,汇编里看到 JNZ 往上跳就是非标准写法。

正解LOOP 指令虽然存在,但现代编译器更常用 DEC ECX + JNZ 组合,因为 LOOP 在部分微架构上性能较慢。判断循环应该看跳转方向,而不是看是否用了 LOOP 助记符。

为何常见:汇编教科书常把 LOOP 当作循环入门指令,但实际编译器生成的代码往往不会这么死板。

面试题

Q1. 在 x86 中,哪个寄存器保存下一条要执行的指令地址?

单选题 · easy

  • A.EAX 通用寄存器
  • B.EIP/RIP 指令指针← 正确答案
  • C.ESP 栈顶指针
  • D.EBP 栈基指针

答案:B。EIP/RIP 就是快递员的线路本,专门记录下一站门牌号。EAX 是干活用的通用寄存器,ESP/EBP 管栈,不负责指令寻址。

Q2. 执行 CMP EAX, EBX 后,JE target 指令何时跳转?

单选题 · easy

  • A.EAX 大于 EBX 时
  • B.EAX 小于 EBX 时
  • C.EAX 等于 EBX 时← 正确答案
  • D.EAX 与 EBX 不等时

答案:C。CMP 做 EAX 减 EBX,相等时结果为 0,ZF 置 1。JE/JZ 看到 ZF=1 就跳转,所以只有两数相等时才会跳。

Q3. 短跳转 JCC 的偏移量是有符号 8 位数,计算目标地址的基址是?

单选题 · mid

  • A.当前指令地址
  • B.下一条指令地址← 正确答案
  • C.栈顶指针地址
  • D.标签绝对地址

答案:B。CPU 先取完当前指令,EIP 已经指向下一条指令,然后再加上偏移量。这就像快递员站在“下一栋楼门口”再数步数,而不是从当前门口数。

Q4. CALL 指令执行时,CPU 做了哪些事?

多选题 · mid

  • A.返回地址压入栈← 正确答案
  • B.ESP 自动减小← 正确答案
  • C.EIP 跳到子程序← 正确答案
  • D.所有寄存器清零

答案:ABC。CALL 把返回地址写到栈顶,栈向低地址生长所以 ESP 减小,然后 EIP 改到子程序入口。它不会清空寄存器,寄存器保护靠调用约定。

Q5. RET 指令从哪里取得返回地址?

单选题 · easy

  • A.从栈顶 [ESP] 取← 正确答案
  • B.从 EAX 寄存器取
  • C.从指令立即数取
  • D.从数据段全局变量取

答案:A。RET 就是撕便利贴,便利贴放在栈顶,也就是 ESP 指向的位置。它把栈顶内容弹出写回 EIP,不读 EAX,也不读立即数。

Q6. 以下哪些情况会破坏调用栈的平衡?

多选题 · hard

  • A.CALL 后缺少对应 RET← 正确答案
  • B.返回前多弹一次栈← 正确答案
  • C.ret n 与调用者清理不一致← 正确答案
  • D.JMP 跳到某个标签

答案:ABC。CALL/RET 必须成对出现,RET 之前多 pop 会弹错返回地址,ret n 额外弹出字节数若和参数大小不一致也会破坏栈。JMP 只修改 EIP,不碰栈,所以不会破坏栈平衡。

Q7. 两个 8 位无符号数相加产生进位时,哪个标志位会变为 1?

单选题 · mid

  • A.ZF 零标志位
  • B.SF 符号标志位
  • C.CF 进位标志位← 正确答案
  • D.OF 溢出标志位

答案:C。无符号数只看范围,进位意味着结果装不进目标宽度,CF 会亮起。OF 管的是有符号溢出,SF 管符号,ZF 管结果是否为零。

Q8. 用 ECX 做计数器实现循环时,通常会配合哪一组指令?

单选题 · mid

  • A.EAX 加一
  • B.ECX 减一← 正确答案
  • C.EBP 加一
  • D.ESP 减一

答案:B。循环计数器常用 ECX/RCX,每轮执行 DEC 或 SUB 让 ECX 减一,再用 JNZ 判断是否回到循环头。EBP/ESP 管栈,不承担计数器角色。

Q9. 条件跳转 JCC 的判断条件可能受到哪些标志位影响?

多选题 · hard

  • A.ZF 判断结果相等← 正确答案
  • B.SF 判断结果为负← 正确答案
  • C.OF 判断有符号溢出← 正确答案
  • D.标志位不参与判断

答案:ABC。JZ/JE 看 ZF,JS 看 SF,JO 看 OF,有符号比较指令还会组合 SF 和 OF。JCC 本身就是“看灯转弯”,标志位是它唯一的判断依据。

Q10. 32 位模式下,无限递归函数调用最终通常导致什么结果?

单选题 · extreme

  • A.所有寄存器被清零
  • B.EAX 寄存器溢出
  • C.栈溢出覆盖返回地址← 正确答案
  • D.条件标志位丢失

答案:C。每次 CALL 都会把返回地址压栈,ESP 不断减小,最终栈空间耗尽,返回地址被破坏,程序崩溃。这就是便利贴堆得太高倒下来,把原来写好的门牌号全盖住了。