x86汇编


滴水逆向课程的学习笔记

汇编基础

数据宽度

数据长度也叫数据宽度,超出宽度的数据会被丢弃

数据宽度单位:

名称 大小
位(Bit) █(1位)
字节(Byte) █|█|█|█|█|█|█|█(8位)
字(Word) █|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█(16位、2个字节)
双字(Double word) █|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█(32位、2个字、4个字节)

无符号数与有符号数

无符号数、有符号数、原码、反码、补码、左移、右移、位运算、高位、低位

简单理解:无符号数存储在计算机内就是其本身的值,1就是1。而有符号数比如-1有一个符号位。个人理解。

通用寄存器

通用寄存器可以往里面存储任意数据和值。寄存器分两种一种就是存值的,如下前四个。另一种用来存放内存地址的,也就是指针寄存器。此处重点掌握

通用寄存器
32位 16位 8位 作用
EAX AX AL、AH 存值,如变量、返回值、计算结果等
ECX CX CL、CH 存值
EDX DX DL、DH 存值
EBX BX BL、BH 存值
ESP SP 栈顶指针,当前正在使用的堆栈地址
EBP BP 栈底指针,当前正在使用的堆栈地址
ESI SI 源变址寄存器,放的是内存地址
EDI DI 目的变址寄存器,放的是内存地址

除了通用寄存器还有一个特殊的指针寄存器EIP,它里面存放的值是CPU下次要执行的指令地址。汇编是从上到下,依次执行。

内存地址的5种形式

mov

向内存中添加数据或从内存中获取数据

  • 格式:MOV 目标操作数,源操作数
  • 含义:将源操作数传送到目标操作数
1
2
3
MOV DOWRD PTR DS:[内存地址], 立即数
MOV DOWRD PTR DS:[内存地址], 32位通用寄存器
MOV 32位通用寄存器, DOWRD PTR DS:[内存地址]

DOWRD为数据宽度,存储的数据需要与DOWRD数据宽度一致.还可以用上面提到的BYTE、WORD

1
2
3
4
// 读取内存的值
mov eax, dword ptr ds:[0x13FFC4]
// 向内存中写入数据
mov dword ptr ds:[0X13FFC4], eax
1
2
3
4
5
6
7
// 读取内存的值
move ecx, 0x13FFD0
mov eax, dword ptr ds:[ecx]

// 向内存中写入数据
mov edx, 0x13FFD8
mov dword ptr ds:[edx], 0x87654321
1
2
3
4
5
6
7
// 读取内存的值
mov ecx, 0x13FFD0
mov eax, dword ptr ds:[ecx+4]

// 向内存中写入数据
mov edx, 0x13FFD8
mov dword ptr ds:[edx+0xC], 0x87654321
1
2
3
4
5
6
7
8
9
// 读取内存的值
mov eax, 0x13FFC4
mov ecx, 0x2
mov edx, dword ptr ds:[eax+eax*4]

// 向内存中写入数据
mov eax, 0x13FFC4
mov ecx, 0x2
mov dword ptr ds:[eax+eax*4], 0x87654321
1
2
3
4
5
6
7
8
// 读取内存的值
mov eax, 0x13FFC4 mov ecx, 0x2
mov edx, dword ptr ds:[eax+eax*4+4]

// 向内存中写入数据
mov eax, 0x13FFC4
mov ecx, 0x2
mov dword ptr ds:[eax+eax*4+4], 0x87654321

常用汇编指令

我们常用的汇编指令有:MOV、ADD、SUB、AND、OR、XOR、NOT

如下格式举例中表示含义:

含义

  • r 代表通用寄存器

  • m 代表内存

  • imm 代表立即数

  • r8 代表8位通用寄存器

  • m8 代表8位内存

  • imm8 代表8位立即数

数据传输与访问

MOV指令

表示数据传送,其格式为:

1
2
3
4
5
6
7
8
9
10
11
// MOV 目标操作数,源操作数
// 含义:将源操作数传送到目标操作数
MOV r/m8,r8
MOV r/m16,r16
MOV r/m32,r32
MOV r8,r/m
MOV r16,r/m16
MOV r32,r/m32
MOV r8, imm8
MOV r16, imm16
MOV r32, imm32

MOVS指令

表示数据传送,它与MOV的不同处在于,它可以将内存的数据传送到内存,但也仅仅能如此,其格式为:

1
2
3
4
5
// MOVS EDI指定的内存地址,ESI指定的内存地址
// 含义:将ESI指定的内存地址的数据传送到EDI指定的内存地址(使用MOVS指令时,默认使用的就是ESI和EDI寄存器),MOVS指令执行完成后ESI、EDI寄存器的值会自增或自减,自增或自减多少取决于传送数据的数据宽度
MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI] //简写为:MOVSB
MOVS WORD PTR ES:[EDI], WORD PTR DS:[ESI] //简写为:MOVSW
MOVS DWORD PTR ES:[EDI], DWORD PTR DS:[ESI] //简写为:MOVSD

MOVS指令举例说明

  1. 先将ESI、EDI的值修改为对应内存地址
1
2
MOV ESI, 0x12FFC4
MOV EDI, 0x12FFD0
  1. 将0x11223344存入EDI指定的内存地址中
1
MOV DWORD PTR DS:[ESI], 0x11223344

STOS指令

表示将AL/AX/EAX的值储存到EDI指定的内存地址,其格式为:

1
2
3
4
5
// STOS EDI指定的内存地址
// 含义:将AL/AX/EAX的值储存到EDI指定的内存地址,STOS指令执行完成后EDI寄存器的值会自增或自减,自增或自减多少取决于传送数据的数据宽度,与MOVS指令一样自增或自减取决于DF位
STOS BYTE PTR ES:[EDI] ``//简写为:STOSB
STOS WORD PTR ES:[EDI] ``//简写为:STOSW
STOS DWORD PTR ES:[EDI] ``//简写为:STOSD

REP指令

表示循环,其格式为:

1
2
3
4
5
6
7
8
// REP MOVS指令/STOS指令
// 含义:循环执行MOVS指令或STOS指令,循环次数取决于ECX寄存器中的值,每执行一次,ECX寄存器中的值就会减一,直至为零,REP指令执行完成
REP MOVSB
REP MOVSW
REP MOVSD
REP STOSB
REP STOSW
REP STOSD

算术运算

ADD指令

表示数据相加,其格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ADD 目标操作数,源操作数
// 含义:将源操作数与目标操作数相加,最后结果给到目标操作数
ADD r/m8, imm8
ADD r/m16,imm16
ADD r/m32,imm32
ADD r/m16, imm8
ADD r/m32, imm8
ADD r/m8, r8
ADD r/m16, r16
ADD r/m32, r32
ADD r8, r/m8
ADD r16, r/m16
ADD r32, r/m32

SUB指令

表示数据相减,其格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
// SUB 目标操作数,源操作数
// 含义:将源操作数与目标操作数相减,最后结果给到目标操作数
SUB r/m8, imm8
SUB r/m16,imm16
SUB r/m32,imm32
SUB r/m16, imm8
SUB r/m32, imm8
SUB r/m8, r8
SUB r/m16, r16
SUB r/m32, r32
SUB r8, r/m8
SUB r16, r/m16
SUB r32, r/m32

MUL

无符号数乘法,默认操作数是AX寄存器。

1
2
3
MOV AX,5
MOV BX,10
MUL BX

IMUL

DIV

IDIV

INC

DEC

逻辑运算

AND指令

表示数据相与(位运算知识),其格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
// AND 目标操作数,源操作数
// 含义:将源操作数与目标操作数进行与运算,最后结果给到目标操作数
AND r/m8, imm8
AND r/m16,imm16
AND r/m32,imm32
AND r/m16, imm8
AND r/m32, imm8
AND r/m8, r8
AND r/m16, r16
AND r/m32, r32
AND r8, r/m8
AND r16, r/m16
AND r32, r/m32

OR指令

表示数据相或(位运算知识),其格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
// AND 目标操作数,源操作数
// 含义:将源操作数与目标操作数进行或运算,最后结果给到目标操作数
OR r/m8, imm8
OR r/m16,imm16
OR r/m32,imm32
OR r/m16, imm8
OR r/m32, imm8
OR r/m8, r8
OR r/m16, r16
OR r/m32, r32
OR r8, r/m8
OR r16, r/m16
OR r32, r/m32

XOR指令

表示数据相异或(位运算知识),其格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
// XOR 目标操作数,源操作数
// 含义:将源操作数与目标操作数进行异或运算,最后结果给到目标操作数
XOR r/m8, imm8
XOR r/m16,imm16
XOR r/m32,imm32
XOR r/m16, imm8
XOR r/m32, imm8
XOR r/m8, r8
XOR r/m16, r16
XOR r/m32, r32
XOR r8, r/m8
XOR r16, r/m16
XOR r32, r/m32

NOT指令

表示非(位运算知识),其格式为:

1
2
3
4
5
// NOT 目标操作数
// 含义:将源操作数进行非运算,最后结果给到目标操作数
NOT r/m8
NOT r/m16
NOT r/m32

栈与函数调用

SHR

SHL

ROL

ROR

PUSH指令

表示压入数据,其格式为:

1
2
3
4
5
// PUSH 通用寄存器/内存地址/立即数
// 含义:向堆栈中压入数据,压入数据后会提升(sub)栈顶指针(ESP),提升多少取决于压入数据的数据宽度
PUSH r16/r32
PUSH m16/m32
PUSH imm8/imm16/imm32

POP指令

表示释放数据,其格式为:

1
2
3
4
// POP 通用寄存器/内存地址
// 含义:释放压入堆栈中的数据,释放数据后会下降(add)栈顶指针(ESP),下降多少取决于释放数据的数据宽度
POP r16/r32
POP m16/m32

JMP指令

表示跳转,其格式为:

1
2
// JMP 寄存器/内存/立即数
// 含义:JMP指令会修改EIP的值为指定的指令地址,也就修改了程序下一次执行的指令地址,我们也可以称之为跳转到某条指令地址。

CALL指令

表示调用函数可理解为push+jmp,其格式为:

1
2
// CALL 寄存器/内存/立即数
// 含义:跟JMP指令的功能一样也可以修改EIP的值,不同的点说,CALL指令执行后会将其下一条指令地址压入堆栈,ESP栈顶指针的值减4

RET指令

表示返回,配合call使用,让其执行完当前call之后回到之前调用位置继续执行,其格式为:

1
2
RET
// 含义:将当前栈顶指针的值赋给EIP,然后让栈顶指针加4</p>

PUSHAD

POPAD

INT80

HIT

NOP

IRET

CMP

JCC指令

JCC指令 含义 英文 检查符号位 C语句
JZ/JE 若为0则跳转;若相等则跳转 jump if zero;jump if equal ZF=1 if (i == j);if (i == 0);
JNZ/JNE 若不为0则跳转;若不相等则跳转 jump if not zero;jump if not equal ZF=0 if (i != j);if (i != 0);
JS 若为负则跳转 jump if sign SF=1 if (i < 0);
JNS 若为正则跳转 jump if not sign SF=0 if (i > 0);
JP/JPE 若1出现次数为偶数则跳转 jump if Parity (Even) PF=1 /
JNP/JPO 若1出现次数为奇数则跳转 jump if not parity (odd) PF=0 /
JO 若溢出则跳转 jump if overflow OF=1 /
JNO 若无溢出则跳转 jump if not overflow OF=0 /
JC/JB/JNAE 若进位则跳转;若低于则跳转;若不高于等于则跳转 jump if carry;jump if below;jump if not above equal CF=1 if (i < j);
JNC/JNB/JAE 若无进位则跳转;若不低于则跳转;若高于等于则跳转; jump if not carry;jump if not below;jump if above equal CF=0 if (i >= j);
JBE/JNA 若低于等于则跳转;若不高于则跳转 jump if below equal;jump if not above ZF=1或CF=1 if (i <= j);
JNBE/JA 若不低于等于则跳转;若高于则跳转 jump if not below equal;jump if above ZF=0或CF=0 if (i > j);
JL/JNGE 若小于则跳转;若不大于等于则跳转 jump if less;jump if not greater equal jump SF != OF if (si < sj);
JNL/JGE 若不小于则跳转;若大于等于则跳转; jump if not less;jump if greater equal SF = OF if (si >= sj);
JLE/JNG 若小于等于则跳转;若不大于则跳转 jump if less equal;jump if not greater ZF != OF 或 ZF=1 if (si <= sj);
JNLE/JG 若不小于等于则跳转;若大于则跳转 jump if not less equal;jump if greater SF=0F 且 ZF=0 if(si>sj)

标志寄存器与跳转指令

EFLAGS

  1. 进位标志位(CF)

  2. 奇偶标志(PF)

  3. 辅助进位标志(AF)

调用约定与传参

函数的调用约定决定了函数的参数入栈顺序、用什么传参和堆栈由谁来清理。

​不同的编译器默认使用不同的调用约定。比如:gcc默认使用cdecl,vs默认使用stdcall

​学过汇编都知道,函数可以不止可以通过堆栈传参,还可以通过寄存器传参。

调用约定 参数压栈顺序 平衡堆栈
__cdecl 从右至左入栈 调用者清理栈
__stdcall 从右至左入栈 自身清理堆栈
__fastcall ECX/EDX传送前两个参数,剩下的从右至左入栈 自身清理堆栈

清理堆栈就是将堆栈恢复到函数调用之前的样子。

接下来我们来看一下一个函数的调用流程,由汇编源码和操作一一对应。

调用一个函数的调用流程

参数入栈
call地址 调用一个函数,call指令会将下一条指令地址入栈,并提升栈顶
保存栈底 将ebp入栈
提升栈顶创建缓冲区 将esp值复制到esp,将esp的值减去缓冲区分配空间值
保存现场 将ebx、esi、edi入栈,并提升栈顶
填充缓冲区 将缓冲区中的数据全部填充为断点数据
函数功能 函数功能代码
还原现场 将ebx、esi、edi弹出栈
栈顶调制栈底 将ebp的值复制到esp
弹出ebp 将栈中保存的ebp弹出栈
ret 弹出call函数时保存的地址到eip寄存器

一个函数调用的汇编代码

1
2
3
4
5
6
	add(1, 2);
009D4601 push 2
009D4603 push 1
009D4605 call add (09D1406h)
009D460A add esp,8
return 0;

add函数内部的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	int add(int a, int b) {
009D4AA0 push ebp
009D4AA1 mov ebp,esp
009D4AA3 sub esp,0CCh
009D4AA9 push ebx
009D4AAA push esi
009D4AAB push edi
009D4AAC lea edi,[ebp-0Ch]
009D4AAF mov ecx,3
009D4AB4 mov eax,0CCCCCCCCh
009D4AB9 rep stos dword ptr es:[edi]
009D4ABB mov ecx,offset _63DF38DB_1@cpp (09DC066h)
009D4AC0 call @__CheckForDebuggerJustMyCode@4 (09D1325h)
int c = a + b;
009D4AC5 mov eax,dword ptr [a]
009D4AC8 add eax,dword ptr [b]
009D4ACB mov dword ptr [c],eax
return c;
009D4ACE mov eax,dword ptr [c]
}

可以看到函数调用完执行ret指令后,有一个add指令。这就是cdecl调用约定的外平栈指令。

内嵌汇编

MSVC编译器下的裸函数

1
2
3
4
5
6
7
void declspec(naked)fun(){

​ __asm{
​ ret
​ }

}

ESP寻址

借助ESP去获取对应参数的地址,这种行为我们称之为ESP寻址。

因为ESP在汇编指令作为栈顶指针,所以它经常需要移动,所以编译器编译程序后的代码中很少会看到esp寻址的情况。

不过如果软件开始者想要提升逆向难度,可能会使用汇编指令来编写某个函数,即可在其中发现这样的汇编指令寻址方式。

1
mov eax, dword ptr ss:[esp+4]

esp-4就是有值推到堆栈里面,也就是往上抬。esp+4与之相反就是往下找具体地址,也就是esp寻址。

一个int占四个字节,所以就是esp的值减少了四。

如下在执行push 0x5,push 0xc和push0x9的操作的时候,esp就会相应的减少12,对应的十六进制就是10,所以ESP就从0019FEE4 – 10 = 0019FED8,也就是说如果要往栈里面存入数据,esp的值就是栈顶的地址值

堆栈平衡

简单知道概览即可,这里暂不深入了解

需要注意的是这段代码只有Debug版本才会有,而在Release版本中堆栈的布局与这是不一样的。

1
2
3
00401094   add         esp,44h
00401097 cmp ebp,esp
00401099 call __chkesp (004010b0)

这段代码的意思就是对比esp和ebp是否一样,而我们知道堆栈在使用完成之后要恢复成员来的样子(堆栈平衡),所以在add指令之后ebp与esp应该是一样的,而后的call指令实际上就是调用了一个函数(**__chkesp),这个函数就是用来检查你的堆栈是否平衡**的。