如果把 CPU 比作一家高速运转的快递公司,那么 CPU 内部的小推车就是各种暂存位,公司的大仓库就是内存。程序运行的本质,就是不停地把数据从小推车搬进仓库、又从仓库搬回小推车。x86 提供的传送指令,就是一组分工不同的搬运工具:有的负责普通搬运,有的只量货架位置不动货,有的把文件压进托盘,有的在誊写时补齐尺寸,还有的能在一声令下完成无差错交换。听懂了这些工具,你就掌握了程序底层最频繁的动作。

岗位关联

数据传送是每条程序的肌肉记忆;理解它,是阅读反汇编、优化热点代码、构建无锁并发系统的地基能力。

目录
  1. 可视化演示
  2. MOV指令基础
  3. 寻址方式
  4. LEA地址计算
  5. PUSH/POP栈操作
  6. 数据扩展传送
  7. XCHG原子交换
  8. 工程实践优化
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示MOV/LEA寄存器内存数据流、PUSH/POP栈操作ESP变化及MOVSX/MOVZX扩展机制,支持步骤切换与播放控制。

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

MOV与LEA数据流 MOV在寄存器与内存间复制数据,LEA计算地址不访存 MOV dst,src / LEA r,[addr] MOV需访存,LEA仅算地址

MOV指令基础

公司里最基础的搬运工叫 MOV。他的口诀很简单:把右边的东西搬到左边。比如从小推车上取一件货放到仓库货架,或者反过来从仓库取件放到小推车上。对于刚入行的程序员,先记住这条铁律:MOV DEST, SRC,源在右,目标在左,一切传送都以这个方向为基准。

这个搬运工并不是真的“移动”数据,而是“复制”数据。就像用复印机把文件复印一份送到另一个工位,原件还留在原地。所以执行 MOV eax, ebx 后,ebx 里的值不会消失,eax 和 ebx 会同时拥有相同的值。除了寄存器、内存之外,MOV 还能搬运“立即数”——也就是直接写在指令里的常数,相当于搬运工手里拿着一张写好的便签,不用去仓库取货。

MOV 也有自己的规矩:一次只能搬一个目标,不能在两块内存之间直接搬运,通常也不能把一个段寄存器直接搬到另一个段寄存器。如果非要在内存之间移动,得先搬到寄存器这个“中转小推车”,再搬到目标内存。

MOV 就像公司里最忙碌也最普通的搬运工,几乎所有程序的第一滴“汗水”都是他流下的。没有他,寄存器里的临时数据和仓库里的长期数据就永远无法汇合,程序也就无法运行。

MOV DEST, SRC:DEST←SRC\displaystyle \text{MOV}\ \text{DEST},\ \text{SRC}:\quad \text{DEST} \leftarrow \text{SRC}
这条公式表达的是:把源操作数 SRC 的值复制给目标操作数 DEST,方向从右到左,源数据不变。
  1. 目标操作数 DEST数据最终要送达的位置,可以是寄存器或内存地址。
    DEST\text{DEST}
  2. 左箭头符号表示“赋值为”或“复制为”,不是数学上的等于。
    ←\leftarrow
  3. 源操作数 SRC数据的来源,可以是寄存器、内存位置或立即数。
    SRC\text{SRC}

寻址方式

搬运工搬货之前,必须先知道货在哪。寻址方式就是告诉 CPU“货架在哪一格”的指路方式。如果在编程时直接说“把 42 放进 eax”,这就是立即数寻址,因为 42 已经写在指令里,不用去仓库找。如果搬运工直接从自己的小推车取货,就是寄存器寻址,速度最快。如果必须去仓库,就涉及存储器寻址。

仓库里往往不是只有一个格子,而是有成千上万个格子。为了精准定位,x86 提供了一套组合地址公式:把一个基址、一个变址、一个比例和一个位移拼起来。类似物流系统里写“3 号库、第 5 排、往前第 2 个货架、第 4 层”。在 Intel 语法中写作 [base + index * scale + disp],在 AT&T 语法中写作 disp(base,index,scale),只是零件顺序不同,含义相同。

对 CPU 来说,地址不是魔法,而是一道算术题。它把各个部分加起来,得到有效地址,然后这个地址才实际决定读取哪个内存单元。

寻址方式就是仓库里的“导航系统”。搬货工不用记住每一个货架,只需要会计算“在哪个入口基础上走几步”,就能高效地找到目标。理解这一点,后面读 LEA 指令时就会觉得格外轻巧。

EA=Base+Index×Scale+Displacement\displaystyle \text{EA} = \text{Base} + \text{Index} \times \text{Scale} + \text{Displacement}
有效地址 EA 就是把基址、变址乘以比例后再加上位移所得的结果,地址计算是所有访存操作的前置步骤。
  1. 基址 Base通常是一个寄存器,用来保存一段连续空间的起点。
    Base\text{Base}
  2. 变址 Index 与比例 ScaleIndex 是偏移的索引,Scale 是每个元素的字节长度,两者相乘得到元素偏移量。
    Index×Scale\text{Index} \times \text{Scale}
  3. 位移 Displacement在基址和变址基础上再追加的固定偏移量。
    Displacement\text{Displacement}

LEA地址计算

经验丰富的仓库主管知道,有时不需要真的搬货,只需要把货架的位置记录下来。LEA 就是这位只带卷尺、不搬货物的测量员。它的全称是 Load Effective Address,也就是“把有效地址装进寄存器”。

LEA eax, [ebx + 4] 做的事情是:先算出 ebx+4 这个地址,然后把“这个地址编号本身”写进 eax,而不是去读取 ebx+4 那个内存位置里的数据。对比一下:MOV eax, [ebx+4] 是“去那个仓库格子取货”,而 LEA eax, [ebx+4] 是“把仓库格子的编号抄到纸上”。

正因为不访存,LEA 常被用来做指针运算,也可以顺便做简单的整数乘法。例如 LEA eax, [eax*4 + eax] 可以快速算出 eax×5。它还不修改标志位,所以不会干扰后面的条件判断,这一点让 LEA 在编译器优化中非常受欢迎。

LEA 让“量位置”和“搬货物”分离。程序里如果要频繁使用某个结构的地址,派一名测量员把地址算好放在寄存器里,后续搬运工就能直接出发,省下不少时间。

r←EA(Base+Index×Scale+Displacement)\displaystyle r \leftarrow \text{EA}\left(\text{Base} + \text{Index} \times \text{Scale} + \text{Displacement}\right)
LEA 指令把括号内的地址计算结果直接写入目标寄存器 r,整个过程不访问内存,所以不会产生访存延迟。
  1. 目标寄存器 r接收地址编号的位置,通常是一个通用寄存器。
    rr
  2. EA 函数表示计算括号内表达式的有效地址。
    EA(⋯ )\text{EA}(\cdots)
  3. 地址表达式与寻址方式共用同一套公式,LEA 只是快速算出结果。
    Base+Index×Scale+Displacement\text{Base} + \text{Index} \times \text{Scale} + \text{Displacement}

PUSH/POP栈操作

公司接待台前有一个文件托盘,客户只能把文件放在最上面,取文件时也只能取最上面一份。这就是栈的“后进先出”规则。PUSH 是往托盘上压一份文件,POP 是从托盘上取走最上面那份。x86 的栈还有一个特点:托盘不是向上长,而是向下长,每次压栈,栈指针 ESP 会先减 4,再把数据写入新栈顶。

PUSH 和 POP 是程序世界的“存取款机”。函数调用时,返回地址、局部变量、保护现场都靠它们存进去;函数返回时,又按后进先出的顺序取出来。如果压了两次栈却只弹一次,程序就会像从托盘下面取文件一样,拿到错误的数据,最终导致崩溃。

栈指针 ESP 就是那根“当前放在哪一层”的手指。PUSH 让手指往下移一格,POP 让手指往上移一格。这个动作非常规律,所以 CPU 可以高效处理。

PUSH/POP 就是接待员手里的托盘:放文件和取文件都只动最上面一层。函数嵌套调用得再深,只要每层都严格压栈和出栈,数据就不会乱套。

PUSH r: ESP←ESP−4, [ESP]←r;POP r: r←[ESP], ESP←ESP+4\displaystyle \text{PUSH}\ r:\ ESP \leftarrow ESP - 4,\ [ESP] \leftarrow r;\qquad \text{POP}\ r:\ r \leftarrow [ESP],\ ESP \leftarrow ESP + 4
PUSH 先让栈指针减 4,再把数据放到新栈顶;POP 先把栈顶数据取走,再让栈指针加 4。
  1. 栈指针变化PUSH 时 ESP 减 4,POP 时 ESP 加 4,这就是栈向下生长的体现。
    ESP←ESP±4ESP \leftarrow ESP \pm 4
  2. 写入栈顶PUSH 把源数据写入新的栈顶地址 [ESP]。
    [ESP]←r[ESP] \leftarrow r
  3. 读取栈顶POP 从当前栈顶 [ESP] 读出数据,再送回寄存器。
    r←[ESP]r \leftarrow [ESP]

数据扩展传送

有时候,仓库里的纸条只有 8 位数字,但目标表格却有 32 格。直接把小纸条贴上去,剩余的空格怎么办?MOVZX 和 MOVSX 是两位誊写员。MOVZX 负责“无符号誊写”,把所有空位都补 0;MOVSX 负责“有符号誊写”,如果原数是正数就补 0,如果是负数就在高位补 1,让数值的真值不变化。

为什么不能都补 0?假设一个 8 位的数 0xFF 当作有符号数时表示 -1,如果高位全补 0,变成 0x000000FF,数值变成了 255,符号就丢了。MOVSX 会把符号位复制到高位,得到 0xFFFFFFFF,这样它仍然表示 -1。MOVZX 则把 0xFF 看作无符号 255,所以补 0 完全正确。

实际编程中,从内存读一个字节或两个字节到 32 位寄存器时,如果不做扩展,寄存器高位会有历史残留的“垃圾”。使用 MOVZX/MOVSX 后,数据大小匹配,后续计算才安全。

MOVZX 和 MOVSX 就像两套抄写规范:面对同样的纸条,一个按无符号处理,一个按有符号处理。抄写员先问清楚数据“身份”,再决定补 0 还是补符号,这样才能保证复印件和原件数值一致。

MOVZX: D←0⋯0⏟高位补零+S;MOVSX: D←s⋯s⏟符号位复制+S\displaystyle \text{MOVZX}:\ D \leftarrow \underbrace{0\cdots 0}_{\text{高位补零}} + S;\qquad \text{MOVSX}:\ D \leftarrow \underbrace{s\cdots s}_{\text{符号位复制}} + S
MOVZX 在高位填 0,适合无符号数;MOVSX 把最高位符号填充到高位,适合有符号数。
  1. 源数据 S被扩展的小尺寸数据,例如 8 位或 16 位数。
    SS
  2. 高位补零MOVZX 把高位全部写成 0,适合无符号数。
    0⋯0⏟高位补零\underbrace{0\cdots 0}_{\text{高位补零}}
  3. 符号位复制MOVSX 把 S 的最高位视为符号,并复制到所有高位。
    s⋯s⏟符号位复制\underbrace{s\cdots s}_{\text{符号位复制}}

XCHG原子交换

如果仓库和小推车要交换货物,普通流程是先把货物放到地上,再从对方位置拿走,这中间万一有人插一脚,就全乱了。XCHG 是一位裁判,他喊“三、二、一,换!”两边的货物在同一个瞬间交换,没人能看到中间状态。

XCHG 指令可以在寄存器和寄存器之间交换,也可以让寄存器和内存交换值。只要其中一个操作数是内存,x86 会自动给这次交换加锁,让操作对其他处理器呈现原子性,不需要再写额外的前缀。这正是“读-改-写”这种复杂动作能被安全执行的关键。

在无锁编程中,XCHG 被广泛用于实现自旋锁、无锁栈、无锁队列。因为它能作为一个不可分割的动作,完成“取出旧值并写入新值”,所以多线程之间不需要加锁也能同步状态。

XCHG 就是物流部的“双人同时交换”裁判。它不需要快递员把货先放到地上再拿,而是同一时刻完成两个方向的动作。这个看似简单的保证,是整个无锁并发世界的基石之一。

temp←A;A←B;B←temp(原子地)\displaystyle \text{temp} \leftarrow A;\quad A \leftarrow B;\quad B \leftarrow \text{temp}\quad(\text{原子地})
公式描述了一次交换的语义:借助临时变量 temp 把 A 和 B 的值互换,但硬件层面是原子完成的,不需要程序员真正写 temp。
  1. 暂存旧值temp 保存 A 的原值,这是用人类思维描述的辅助步骤。
    temp←A\text{temp} \leftarrow A
  2. 写入新值把 B 的值赋给 A,让 A 拿到 B 的货物。
    A←BA \leftarrow B
  3. 完成回填把 temp 中的 A 旧值赋给 B,交换完成。
    B←tempB \leftarrow \text{temp}

工程实践优化

物流部每天最忙的永远是普通搬运工。数据传送指令在程序里同样是最频繁出现的指令类型,所以工程优化的核心,不是发明更花哨的指令,而是减少不必要的搬运。

第一原则是“少访存”。寄存器的存取速度远高于内存,因此经常使用的值应尽量放在寄存器里,不要每次都去仓库搬。第二原则是“选对工具”:要算地址就用 LEA,不要用 MOV 先去取内存再加减;要把 8 位数据扩展到 32 位就用 MOVZX/MOVSX,不要额外写一条清零指令。第三原则是“栈操作要成对”:PUSH 和 POP 的次数要保持平衡,否则程序执行流会像托盘堆歪一样崩塌。

最后还要懂得取舍。寄存器总共就那么几个,不能把所有数据都塞进小推车;可读性和性能之间要找到平衡点。写出别人能看懂的代码,往往比盲目追求少一条指令更值钱。

物流调度的智慧是“能用小推车就不出仓库”。当搬运工减少无效往返、选择最快路线时,整条流水线自然就跑得更快。数据传送指令的工程优化,说到底就是让数据在最短的路径上抵达最需要它的地方。

T搬运=Nreg×1+Nmem×Caccess\displaystyle T_{\text{搬运}} = N_{\text{reg}} \times 1 + N_{\text{mem}} \times C_{\text{access}}
总搬运成本约等于寄存器搬运次数乘以 1 个周期,加上内存访问次数乘以每次访存开销;访存开销通常远大于寄存器访问。
  1. 寄存器搬运次数程序中使用寄存器到寄存器传送的次数,单次开销约为 1 个周期。
    Nreg×1N_{\text{reg}} \times 1
  2. 内存访问次数访问内存的次数,通常来自 MOV 从内存取数或写数。
    NmemN_{\text{mem}}
  3. 访存成本系数每一次内存访问对应的周期数,比寄存器慢几十到上百倍。
    CaccessC_{\text{access}}

AI 应用场景

自动化逆向与指令特征提取

阶段:训练数据准备与特征工程

解决的问题:安全分析里,AI 需要从海量二进制代码中识别函数边界和调用习惯。传统手工反汇编耗时巨大,且容易漏掉指令特征。使用 MOV、PUSH/POP 等数据传送指令的规律,可以自动提取栈帧结构和函数调用关系。

具体案例:恶意软件检测系统解析函数入口的 push ebp; mov ebp, esp 序列,把“数据传送指令频率”作为特征向量的一部分,再交给分类模型判断样本是否为恶意程序。数据传送指令是程序最底层的肌肉记忆,因此特征非常稳定。

推理引擎中的无锁队列

阶段:在线推理服务部署与性能优化

解决的问题:AI 推理服务高并发请求下,请求队列是多线程竞争的焦点。传统互斥锁会造成线程阻塞和上下文切换开销。利用 XCHG 的原子交换特性,可以实现无锁队列,让请求入队和出队几乎不阻塞。

具体案例:一个实时语音识别服务用 XCHG 实现自旋锁保护环形缓冲区。每个请求只需一次原子交换即可抢到处理权,吞吐量比普通互斥锁提升数倍,推理延迟也更稳定。

JIT 算子生成中的类型扩展

阶段:模型算子编译与代码生成

解决的问题:AI 框架把张量计算编译成机器码时,不同精度数据需要统一到寄存器位宽。如果直接拷贝小尺寸数据,高位残留会导致计算错误。编译器的代码生成模块会插入 MOVZX 或 MOVSX,按张量元素类型做正确扩展。

具体案例:某深度学习编译器在生成 int8 算子时,用 MOVZX 把 8 位无符号激活值扩展到 32 位寄存器;对带符号权重则用 MOVSX。这样既保证了数组元素正确加载,又避免了多余指令带来的性能损失。

Python 演示

python84 行
class ToyCPU:    def __init__(self):        self.regs = {'eax': 0, 'ebx': 0, 'ecx': 0, 'edx': 0, 'esi': 0}        self.mem = {}        self.stack = []        self.sp = 0     def store(self, addr, value):        self.mem[addr] = value & 0xFFFFFFFF     def load(self, addr):        return self.mem.get(addr, 0)     def _read(self, operand):        if isinstance(operand, int):            return operand        if isinstance(operand, tuple) and operand[0] == 'mem':            return self.load(operand[1])        return self.regs[operand]     def mov(self, dst, src):        val = self._read(src) & 0xFFFFFFFF        if isinstance(dst, tuple) and dst[0] == 'mem':            self.store(dst[1], val)        else:            self.regs[dst] = val     def lea(self, dst, spec):        _, reg, disp = spec        self.regs[dst] = (self.regs[reg] + disp) & 0xFFFFFFFF     def push(self, src):        val = self._read(src)        self.sp -= 4        self.store(self.sp, val)     def pop(self, dst):        val = self.load(self.sp)        self.sp += 4        self.regs[dst] = val     def movzx(self, dst, src, bits):        val = self._read(src) & ((1 << bits) - 1)        self.regs[dst] = val     def movsx(self, dst, src, bits):        val = self._read(src) & ((1 << bits) - 1)        sign = 1 << (bits - 1)        if val & sign:            val -= 1 << bits        self.regs[dst] = val & 0xFFFFFFFF     def xchg(self, a, b):        va = self._read(a)        vb = self._read(b)        if isinstance(a, tuple) and a[0] == 'mem':            self.store(a[1], vb)        else:            self.regs[a] = vb        if isinstance(b, tuple) and b[0] == 'mem':            self.store(b[1], va)        else:            self.regs[b] = va def main():    cpu = ToyCPU()    cpu.store(100, 0x12)    cpu.mov('eax', ('mem', 100))    cpu.lea('ebx', ('addr', 'eax', 4))    cpu.push('eax')    cpu.pop('ecx')    cpu.store(104, 0x80)    cpu.movzx('edx', ('mem', 104), 8)    cpu.movsx('esi', ('mem', 104), 8)    cpu.xchg('eax', ('mem', 100))    print('eax =', hex(cpu.regs['eax']))    print('ebx =', hex(cpu.regs['ebx']))    print('ecx =', hex(cpu.regs['ecx']))    print('edx(movzx) =', hex(cpu.regs['edx']))    print('esi(movsx) =', hex(cpu.regs['esi']))    print('mem[100] =', hex(cpu.load(100))) if __name__ == '__main__':    main()
目的:用 Python 模拟了一个迷你 x86 CPU,展示 MOV、LEA、PUSH/POP、MOVZX/MOVSX 和 XCHG 的基本行为,帮助理解每条指令对寄存器和内存的影响。
  1. val = self._read(src) & 0xFFFFFFFF统一读出源操作数,并截断到 32 位,模拟寄存器宽度。
  2. self.regs[dst] = val把读取到的值写到目标寄存器,源寄存器不会被清空。
  3. self.regs[dst] = (self.regs[reg] + disp) & 0xFFFFFFFFLEA 只算地址,不访问内存。
  4. self.sp -= 4PUSH 让栈指针向下移动,模拟 x86 栈向下生长。
  5. val -= 1 << bitsMOVSX 的灵魂:负数的 8 位值先减掉 2^bits,再截断成 32 位有符号表达。
  6. cpu.xchg('eax', ('mem', 100))演示 XCHG 交换寄存器与内存中同一个数据单元。

常见误区

误区我觉得 MOV 就是把数据从一个地方搬到另一个地方,搬完以后原来那个地方就空了,像剪切粘贴一样。

正解MOV 是复制操作,源操作数的值保持不变。它更像复印机,而不是剪刀。执行 MOV eax, ebx 后,ebx 依然保留原值。这个误解会让新手在调试时错误判断寄存器内容,甚至以为某个值“丢了”。

为何常见:因为“传送”这个词容易让人联想到移动,很多教材又强调“把数据送到目标”,没有强调源数据仍保留,所以初学者很容易先入为主。

误区LEA 的括号里写的是内存地址,它应该和 MOV 一样会去读那个地址里的数据。

正解LEA 只负责计算括号里的地址表达式,并把结果写入寄存器,全程不访问内存。它像测量员只画图纸,不搬货。LEA eax, [ebx+4] 得到的是 ebx+4 这个数值,而不是 [ebx+4] 指向的内存内容。

为何常见:因为 [ ] 在 MOV 中表示解引用,新手很自然地把同样语法带到 LEA 中,忽略了 LEA 的语义重点在“有效地址”而非“地址处的数据”。

误区栈应该像摞盘子一样向上长,PUSH 让栈指针变大,POP 让栈指针变小。

正解x86 的栈地址是向下增长的。PUSH 先把 ESP 减 4,再写入数据;POP 先读数据,再把 ESP 加 4。托盘确实是“一层层往上摞”,但地址编号的方向是向下递减的。用反方向理解栈帧,调试时会完全颠倒。

为何常见:日常生活中的堆叠都是向上堆,所以“栈向上增长”更符合直觉;而 x86 为了历史兼容性选择了向下增长,这是反直觉的。

误区反正都是把小尺寸数据扩展到大尺寸,MOVZX 和 MOVSX 应该没什么区别。

正解两个指令的高位填充规则完全不同。MOVZX 无条件补 0,适合无符号数;MOVSX 复制符号位,适合有符号数。把 0xFF 看作有符号数 -1 时,MOVZX 得到 255,MOVSX 得到 -1,数值含义完全不同。

为何常见:在只处理正数的场景中,两者结果确实一样,初学者经常只见过正数例子,就误以为它们永远等价。

误区只有加上 `LOCK` 前缀,XCHG 才是原子的,不加就有风险。

正解当 XCHG 的操作数包含内存时,x86 架构会自动锁定总线或缓存行,保证交换操作原子完成,不需要额外写 LOCK。相反,如果操作数只涉及寄存器,就没有跨处理器同步的需求。无锁编程使用 XCHG 正是因为它的内存交换天然原子。

为何常见:很多资料重点讲 LOCK 前缀用于其他读改写指令,新手容易把所有原子操作都归因于 LOCK,忽略了 XCHG 自带的原子语义。

面试题

Q1. 在 Intel 语法的 MOV 指令中,源操作数一般写在哪个位置?

单选题 · easy

  • A.等号左边
  • B.等号右边
  • C.逗号右边← 正确答案
  • D.括号里面

答案:C。Intel 语法写作 MOV DEST, SRC,源操作数在逗号右边,目标操作数在逗号左边。AT&T 语法则相反。就像“快递单”上收件人和寄件人顺序不同,要看清楚平台约定。

Q2. LEA 指令的主要作用是什么?

单选题 · easy

  • A.读取内存数据
  • B.计算有效地址← 正确答案
  • C.修改栈指针
  • D.交换两个寄存器

答案:B。LEA 只把地址表达式的计算结果放入寄存器,不访问内存。读取内存数据应该用 MOV,修改栈指针用 PUSH/POP 或 ADD/SUB,交换寄存器用 XCHG。LEA 是“只量位置不搬货”的测量员。

Q3. 在 x86 中执行 PUSH 指令时,栈指针 ESP 通常如何变化?

单选题 · easy

  • A.ESP 增加 4
  • B.ESP 减少 4← 正确答案
  • C.ESP 保持不变
  • D.ESP 归零

答案:B。x86 栈向下增长,PUSH 先把 ESP 减 4,再把数据写到新栈顶。如果 ESP 增加,就变成向上增长,不是 x86 的行为。可以把 ESP 想象成“托盘上的手指”,压栈时手指向下移动一格。

Q4. 执行 MOVZX eax, al 后,eax 的高 24 位是什么?

单选题 · mid

  • A.全部补零← 正确答案
  • B.复制 al 的符号位
  • C.保持原寄存器值
  • D.随机填充

答案:A。MOVZX 是零扩展,把所有高位填 0。如果要复制符号位,应该用 MOVSX。MOVZX 适合无符号数,就像把无符号纸条抄到大海报时空白位置全填 0。

Q5. 下面哪种寻址方式需要真正访问内存?

单选题 · mid

  • A.立即数寻址
  • B.寄存器寻址
  • C.直接寻址← 正确答案
  • D.LEA 寻址

答案:C。立即数寻址把常数直接放在指令中,寄存器寻址从寄存器取数,LEA 只计算地址不访存。直接寻址使用内存地址取数据,所以会访问内存。它相当于直接报出仓库货架编号并进去取货。

Q6. XCHG 指令在操作内存时具有什么特性?

单选题 · mid

  • A.可能被中断打断
  • B.自动原子执行← 正确答案
  • C.必须手动加 LOCK
  • D.只能交换寄存器

答案:B。当 XCHG 的操作数包含内存时,x86 会自动锁定总线或缓存行,保证交换原子完成,不需要额外 LOCK。它像裁判保证双方在同一时刻完成交换,中间状态不可见。

Q7. 执行 PUSH eax 后,栈顶指针和栈顶内容分别发生了什么?

单选题 · mid

  • A.ESP 减 4,新栈顶写入 eax← 正确答案
  • B.ESP 加 4,旧栈顶写入 eax
  • C.ESP 减 4,旧栈顶写入 eax
  • D.ESP 加 4,新栈顶写入 eax

答案:A。PUSH 总是先让 ESP 向下移动 4 字节,然后把数据写入这个新的栈顶位置。POP 则相反。这和接待员把文件先放到托盘更上面一层是一个道理。

Q8. 下列哪些指令属于 x86 数据传送类指令?

多选题 · mid

  • A.MOVZX← 正确答案
  • B.LEA← 正确答案
  • C.ADD
  • D.PUSH← 正确答案

答案:ABD。MOVZX 做零扩展传送,LEA 算有效地址并送入寄存器,PUSH 把数据压入栈,它们都属于数据传送范畴。ADD 是算术运算指令,负责加法,不算数据传送。

Q9. 关于 MOVSX 与 MOVZX,下列说法正确的有哪些?

多选题 · hard

  • A.MOVSX 按符号位扩展← 正确答案
  • B.MOVZX 高位补零← 正确答案
  • C.两者结果总是相同
  • D.MOVSX 适合有符号数← 正确答案

答案:ABD。MOVSX 是把源操作数的符号位复制到高位,适合有符号数;MOVZX 高位全部补零,适合无符号数。当数据为正数时两者结果相同,但遇到负数时结果完全不同,所以不能说“总是相同”。

Q10. 关于 XCHG 指令,下列哪些说法是正确的?

多选题 · hard

  • A.能交换寄存器与内存← 正确答案
  • B.内存操作时自动锁定← 正确答案
  • C.可用于无锁同步← 正确答案
  • D.能直接交换两个内存地址

答案:ABC。XCHG 允许寄存器与内存交换,且内存参与时会自动锁定总线或缓存行,因此常用于无锁同步。但 x86 规定两个操作数不能都是内存地址,所以 D 错误。它就像“同时换货”的裁判,不能同时指挥两个仓库自己换。