汇编指令精讲(一):数据传送与算术运算

各位同学,今天我们正式进入汇编指令的实战环节。说实话,很多初学者一看到汇编指令就头大,觉得这玩意儿又枯燥又难记。我当年刚入行时也这么想,直到有一次在逆向一个恶意软件时,发现对方用了几条巧妙的LEA指令就把整个控制流给藏起来了——那一刻我才真正意识到,指令集就是二进制世界的语法,不懂它,你连代码在干什么都看不懂。

今天我们先啃两块硬骨头:数据传送指令算术运算指令。这两类指令是汇编代码里出现频率最高的,你随便打开一个二进制文件,里面至少一半都是它们。

核心要点:数据传送指令负责搬数据,算术运算指令负责算数据。两者配合,构成了程序的基本计算单元。

本章指令体系总览 数据传送指令 MOV PUSH / POP LEA XCHG 算术运算指令 ADD / SUB MUL / DIV INC / DEC NEG 指令 = 操作码 + 操作数 操作数可以是:寄存器、内存地址、立即数

一、数据传送指令:搬砖的艺术

1.1 MOV:最基础的搬运工

MOV指令,说白了就是把数据从一个地方复制到另一个地方。它的格式很简单:MOV 目标操作数, 源操作数。注意顺序——目标在前,源在后,跟Intel语法保持一致。

; 基本用法
MOV EAX, 0x12345678    ; 将立即数送入EAX
MOV EBX, EAX           ; 将EAX的值复制到EBX
MOV ECX, [0x400000]    ; 将内存0x400000处的值送入ECX
MOV [EBP-4], EDX       ; 将EDX的值存入栈中局部变量位置

注意:MOV指令不能直接从内存搬到内存!比如 MOV [addr1], [addr2] 是非法的。你必须通过寄存器中转。我见过不少新手在这里栽跟头,编译直接报错。

我个人习惯在逆向分析时,先快速扫一遍代码里的MOV指令,看看哪些常量被频繁加载。这往往能帮你快速定位关键数据。比如一个程序里反复出现MOV EAX, 0x401000,那这个地址八成是个关键函数入口。

1.2 PUSH 和 POP:栈操作双雄

栈是程序运行时的临时存储区,遵循后进先出(LIFO)原则。PUSH把数据压入栈顶,POP从栈顶弹出数据。这两个指令在函数调用、上下文切换时用得特别多。

; 栈操作示例
PUSH EAX               ; 将EAX压栈,ESP -= 4
PUSH 0x100             ; 将立即数压栈
POP EBX                ; 弹出栈顶到EBX,ESP += 4
POP ECX                ; 继续弹出

嗯,这里要注意:PUSHPOP操作的是ESP寄存器。每次PUSHESP减4(32位下);每次POPESP加4。我曾经在调试一个栈溢出漏洞时,发现攻击者就是通过精心构造的PUSH序列覆盖了返回地址——所以理解栈的变化对安全分析至关重要。

小技巧:在逆向分析时,看到连续的PUSH后面跟着CALL,这通常是在传递函数参数。x86下参数是通过栈传递的,顺序是从右向左压栈。

1.3 LEA:取地址的利器

LEA(Load Effective Address)是我个人非常喜欢的一条指令。它不访问内存,只计算有效地址并存入寄存器。很多人把它跟MOV搞混,其实区别很明显:MOVLEA地址

; LEA vs MOV 对比
MOV EAX, [EBP+8]       ; 取EBP+8处的值
LEA EAX, [EBP+8]       ; 取EBP+8这个地址本身

; LEA的妙用:快速算术
LEA EAX, [ECX+ECX*4]   ; EAX = ECX * 5
LEA EDX, [EAX+EAX*8]   ; EDX = EAX * 9

你看,LEA还能用来做乘法运算,而且比MUL快得多。我在优化一个图像处理算法时,就用LEA替代了多次乘法,性能提升了将近20%。

1.4 XCHG:交换数据

XCHG指令交换两个操作数的值。虽然用得不多,但在某些同步机制里能看到它的身影。

XCHG EAX, EBX          ; 交换EAX和EBX的值
XCHG EAX, [MEM_ADDR]   ; 交换寄存器和内存的值

实战经验:XCHG指令在原子操作中很常见。比如实现自旋锁时,会用XCHG配合LOCK前缀来保证操作的原子性。不过日常逆向中遇到它的频率不高,了解即可。

二、算术运算指令:让数据动起来

2.1 ADD 和 SUB:加减法

ADDSUB是最基本的算术指令。它们会影响标志寄存器(EFLAGS)中的ZF(零标志)、SF(符号标志)、CF(进位标志)和OF(溢出标志)。

; 加法示例
ADD EAX, 10            ; EAX += 10
ADD EBX, ECX           ; EBX += ECX
ADD EAX, [0x400000]    ; EAX += 内存中的值

; 减法示例
SUB EAX, 5             ; EAX -= 5
SUB ECX, EDX           ; ECX -= EDX

为什么我要强调标志位?因为在逆向分析条件跳转时,你经常需要根据ADD/SUB之后的标志位来判断程序走向。比如SUB之后紧跟着JZ(Jump if Zero),那八成是在判断两个数是否相等。

避坑指南:我曾经在分析一个加密算法时,看到ADD之后没有检查溢出,结果算出来的值完全不对。后来才发现是整数溢出导致的问题。所以在做安全分析时,一定要留意算术运算是否可能产生溢出。

2.2 MUL 和 DIV:乘除法

乘除法比加减法复杂一些,因为它们涉及的操作数长度和结果存放位置有特殊规定。

MUL(无符号乘法):

  • 单操作数:MUL EBXEAX * EBX,结果存于EDX:EAX(高32位在EDX,低32位在EAX)
  • 双操作数:IMUL EAX, EBXEAX *= EBX(有符号乘法)

DIV(无符号除法):

  • DIV EBXEDX:EAX / EBX,商存于EAX,余数存于EDX
  • 如果除数为0,会触发#DE异常(除零异常)
; 乘法示例
MOV EAX, 100
MOV EBX, 200
MUL EBX                ; EDX:EAX = 100 * 200 = 20000

; 除法示例
MOV EAX, 1000
XOR EDX, EDX           ; 清空EDX,因为被除数是EDX:EAX
MOV ECX, 7
DIV ECX                ; EAX = 142 (商), EDX = 6 (余数)

个人经验:做除法前一定要先清空EDX!我见过太多人忘记这一步,结果EDX里残留了之前的值,导致计算结果完全错误。用XOR EDX, EDX或者MOV EDX, 0都行。

2.3 INC 和 DEC:自增自减

INCDEC分别对操作数加1和减1。它们比ADD/SUB效率更高,而且不影响CF标志位(只影响ZF、SF、OF)。

INC EAX                ; EAX++
DEC ECX                ; ECX--

在循环中你经常能看到INCDEC配合JNZ(Jump if Not Zero)使用,实现循环计数。比如:

MOV ECX, 10
loop_start:
  ; 循环体代码
  DEC ECX
  JNZ loop_start       ; ECX != 0 时继续循环

2.4 NEG:取补码

NEG指令对操作数取补码,相当于0 - 操作数。它会影响CF、ZF、SF、OF标志位。

MOV EAX, 5
NEG EAX                ; EAX = -5 (0xFFFFFFFB)

你想想看,NEG在逆向分析中有什么用?我遇到过一些混淆代码,它们用NEG配合ADD来实现减法,故意绕开SUB指令,增加分析难度。这时候如果你不了解NEG的行为,很容易被绕晕。

三、综合实战:一段简单的汇编代码分析

好了,理论讲完了,我们来看一段真实的汇编代码。假设你在逆向一个计算器程序,遇到了下面这段:

; 计算 (a + b) * c - d / e
MOV EAX, [a]           ; 加载a
ADD EAX, [b]           ; EAX = a + b
MOV ECX, [c]
MUL ECX                ; EDX:EAX = (a+b) * c
PUSH EAX               ; 暂存结果到栈

MOV EAX, [d]
XOR EDX, EDX           ; 清空EDX
MOV ECX, [e]
DIV ECX                ; EAX = d / e, EDX = 余数

POP EBX                ; 恢复之前的乘积
SUB EBX, EAX           ; EBX = (a+b)*c - d/e
MOV [result], EBX      ; 保存结果

这段代码虽然简单,但包含了我们今天讲的大部分指令:MOVADDMULDIVPUSHPOPSUB。你能看出它计算的是什么吗?

分析思路:先看数据流向,再看运算顺序。注意PUSHPOP的配对使用,以及除法前对EDX的清零操作——这些都是实战中必须注意的细节。

四、总结与避坑清单

今天的内容就到这里。我整理了一份避坑清单,都是我在项目中踩过的坑:

指令 常见错误 正确做法
MOV 试图内存到内存直接传送 用寄存器中转
PUSH/POP 忘记栈平衡(PUSH和POP数量不匹配) 确保成对出现
LEA 误以为LEA会访问内存 LEA只计算地址,不取值
MUL 忽略EDX:EAX的高32位 注意结果可能溢出到EDX
DIV 忘记清空EDX 用XOR EDX, EDX清零
NEG 混淆NEG和NOT NEG取补码,NOT取反码

记住,汇编指令是二进制世界的基石。你越熟悉它们,逆向分析时就越得心应手。下次当你面对一堆十六进制字节时,能一眼认出这些指令,那才叫真正的入门了。

最后一个小建议:找几个简单的C程序,编译成汇编代码,对照着看。这是最快的学习方式。我当年就是这么过来的——每天看几十页汇编输出,慢慢就练出了「汇编眼」。


公众号:蓝海资料掘金营,微信deep3321