3、汇编语言入门(上):x86汇编基础、通用寄存器、常用指令(mov, add, sub, push, pop, call, ret)

好,咱们今天开始碰汇编。说实话,很多新手一听“汇编”两个字就头大,觉得那是大神才玩的东西。其实真不是。你想想看,我们平时写的C、Python,最终跑到CPU里都是变成一堆0和1。而汇编,就是把这些0和1翻译成了人能勉强看懂的“单词”。说白了,它就是机器码的文本版。

我刚开始学逆向的时候,也对着汇编发怵。后来发现,你不需要背下所有指令,只需要掌握最常用的那二三十个,就能看懂大部分代码了。今天这一节,咱们就把最核心的x86汇编基础、寄存器,以及几条出现频率最高的指令给啃下来。

3.1 x86汇编是什么?

x86汇编,指的是Intel和AMD的32位/64位CPU使用的汇编语言。我们常说的“32位程序”就是跑在x86指令集上。它的特点是:指令长度不固定,功能强大,但历史包袱也重。

我个人习惯把汇编看成“CPU的遥控器”。你按下一个键(指令),CPU就执行一个动作。比如你按“mov”,它就把数据从一个地方搬到另一个地方。就这么简单。

核心概念:汇编语言 = 助记符 + 操作数。助记符就是指令的名字(如mov),操作数就是指令要操作的对象(如寄存器、内存地址、立即数)。

3.2 通用寄存器:CPU的临时工作台

寄存器是CPU内部的小块存储,速度极快,但数量有限。你可以把它想象成你桌面上的几个便签纸——随手拿来记东西,但写满了就得擦掉重来。

在32位x86架构下,有8个通用寄存器。它们的名字和用途我列在下面:

寄存器 32位名 16位名 8位名(高/低) 常见用途
累加器 EAX AX AH / AL 算术运算、函数返回值
基址寄存器 EBX BX BH / BL 常作为内存基址指针
计数寄存器 ECX CX CH / CL 循环计数器、移位次数
数据寄存器 EDX DX DH / DL I/O操作、乘除法辅助
源变址 ESI SI - 字符串操作源指针
目的变址 EDI DI - 字符串操作目的指针
栈基址 EBP BP - 函数栈帧基址
栈指针 ESP SP - 指向栈顶,不要乱改

小技巧:你在逆向分析时,看到EAX通常就是函数的返回值。如果函数返回一个整数,十有八九在EAX里。我曾经调试一个崩溃的驱动,就是靠检查EAX的值定位到错误码的。

嗯,这里要注意:ESP这个寄存器很特殊。它指向当前栈的顶部。你如果随便改ESP,程序立马崩溃给你看。我刚开始学的时候手痒,写了个mov esp, 0,结果整个调试器都卡死了……别学我。

3.3 常用指令:mov, add, sub

这三条指令是汇编里的“三原色”。几乎所有程序都离不开它们。

3.3.1 mov —— 数据搬运工

mov 就是把数据从一个地方复制到另一个地方。注意是“复制”,不是“移动”。源操作数不变。

mov eax, 1234h      ; 把立即数0x1234存入EAX
mov ebx, eax        ; 把EAX的值复制到EBX
mov ecx, [eax]      ; 把EAX指向的内存地址中的值读入ECX

格式:mov 目标操作数, 源操作数。目标不能是立即数,源和目标不能同时是内存地址(x86不支持内存到内存的直接mov)。

避坑指南:我曾经在分析一个恶意软件时,看到mov [eax], eax这种写法。它把EAX的值写入EAX指向的内存地址。如果你没搞清楚哪个是地址、哪个是值,很容易看晕。记住:带方括号的表示“内存地址里的内容”。

3.3.2 add / sub —— 加减法

这两个就是算术加减。没什么玄乎的。

add eax, 10         ; EAX = EAX + 10
sub ebx, ecx        ; EBX = EBX - ECX
add [esp], 4        ; 栈顶的值加4

它们会影响标志寄存器(EFLAGS)里的ZF(零标志)、SF(符号标志)、CF(进位标志)等。比如sub eax, eax,结果为零,ZF会被置1。这个在条件跳转里非常关键。

3.4 栈操作:push 和 pop

栈是一种后进先出的数据结构。在x86里,栈从高地址向低地址生长。也就是说,你每push一次,ESP就减小4(32位下),然后把数据放进去。

push eax            ; ESP = ESP - 4, 然后把EAX的值写入[ESP]
pop ebx             ; 把[ESP]的值读入EBX, 然后ESP = ESP + 4

为什么需要栈?因为寄存器不够用啊。你想想看,一个函数里可能要调用子函数,子函数也会用寄存器。如果不把原来的值保存起来,回来就丢了。所以函数开头通常会把要用到的寄存器push进去,返回前再pop出来。

实战经验:我在逆向一个加壳程序时,发现它大量使用push/pop来混淆代码。比如push eax; pop eax,看似什么都没做,其实是为了消耗时间或者干扰静态分析。遇到这种“花指令”,直接忽略就好。

3.5 函数调用:call 和 ret

这两个指令是程序流程控制的核心。没有它们,代码就只能顺序执行,没法实现函数调用。

3.5.1 call —— 调用子程序

call 做了两件事:

  1. 把下一条指令的地址(返回地址)压入栈。
  2. 跳转到目标地址执行。
call 0x401000       ; 调用地址0x401000处的函数
call eax            ; 调用EAX中保存的地址

3.5.2 ret —— 返回

ret 从栈顶弹出返回地址,然后跳转过去。它和call正好配对。

ret                 ; 从栈顶弹出地址,跳转
ret 8               ; 弹出地址后,ESP再加8(用于清理参数)

你看,一个函数调用的生命周期就是:call压入返回地址 -> 执行子函数 -> ret弹出地址返回。就这么简单。

个人习惯:我在分析一个函数时,会先看它的开头有没有push ebp; mov ebp, esp。如果有,这就是一个标准的函数帧。然后看结尾的pop ebp; ret。中间的就是函数体。这个模式在逆向里太常见了,闭着眼睛都能认出来。

3.6 知识体系总览

下面这张图把本章的核心知识点串在了一起。你可以把它当成一张地图,随时回来对照。

x86汇编基础 通用寄存器 EAX, EBX, ECX, EDX ESI, EDI, EBP, ESP 每个都有16位/8位子寄存器 数据指令 mov: 数据复制 add: 加法 sub: 减法 栈操作指令 push: 压栈 pop: 出栈 栈从高地址向低地址生长 控制流指令 call: 调用子程序 ret: 返回 call压入返回地址,ret弹出

这张图里,寄存器是基础,数据指令负责运算,栈指令管理临时数据,控制流指令决定程序走向。四者配合,就能构成完整的程序逻辑。

3.7 小结

今天咱们把x86汇编最核心的几条指令过了一遍。你不需要一次全记住,但一定要理解它们背后的逻辑:mov是搬数据,add/sub是算数,push/pop是管理栈,call/ret是函数调用。这些指令在逆向分析里出现的频率极高,几乎每段代码里都有它们的身影。

我建议你找个调试器(比如x64dbg或OllyDbg),随便打开一个简单的exe,单步执行几行,亲眼看看寄存器怎么变、栈怎么动。看十遍不如动手一遍。嗯,下一节我们会继续深入,看看更多实用的指令和寻址方式。


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