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 |
|
DOWRD为数据宽度,存储的数据需要与DOWRD数据宽度一致.还可以用上面提到的BYTE、WORD
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
常用汇编指令
我们常用的汇编指令有:MOV、ADD、SUB、AND、OR、XOR、NOT
如下格式举例中表示含义:
含义
r 代表通用寄存器
m 代表内存
imm 代表立即数
r8 代表8位通用寄存器
m8 代表8位内存
imm8 代表8位立即数
数据传输与访问
MOV指令
表示数据传送,其格式为:
1 |
|
MOVS指令
表示数据传送,它与MOV的不同处在于,它可以将内存的数据传送到内存,但也仅仅能如此,其格式为:
1 |
|
MOVS指令举例说明:
- 先将ESI、EDI的值修改为对应内存地址
1 |
|
- 将0x11223344存入EDI指定的内存地址中
1 |
|
STOS指令
表示将AL/AX/EAX的值储存到EDI指定的内存地址,其格式为:
1 |
|
REP指令
表示循环,其格式为:
1 |
|
算术运算
ADD指令
表示数据相加,其格式为:
1 |
|
SUB指令
表示数据相减,其格式为:
1 |
|
MUL
无符号数乘法,默认操作数是AX寄存器。
1 |
|
IMUL
DIV
IDIV
INC
DEC
逻辑运算
AND指令
表示数据相与(位运算知识),其格式为:
1 |
|
OR指令
表示数据相或(位运算知识),其格式为:
1 |
|
XOR指令
表示数据相异或(位运算知识),其格式为:
1 |
|
NOT指令
表示非(位运算知识),其格式为:
1 |
|
栈与函数调用
SHR
SHL
ROL
ROR
PUSH指令
表示压入数据,其格式为:
1 |
|
POP指令
表示释放数据,其格式为:
1 |
|
JMP指令
表示跳转,其格式为:
1 |
|
CALL指令
表示调用函数可理解为push+jmp,其格式为:
1 |
|
RET指令
表示返回,配合call使用,让其执行完当前call之后回到之前调用位置继续执行,其格式为:
1 |
|
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
进位标志位(CF)
奇偶标志(PF)
辅助进位标志(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 |
|
add函数内部的代码
1 |
|
可以看到函数调用完执行ret指令后,有一个add指令。这就是cdecl调用约定的外平栈指令。
内嵌汇编
MSVC编译器下的裸函数
1 |
|
ESP寻址
借助ESP去获取对应参数的地址,这种行为我们称之为ESP寻址。
因为ESP在汇编指令作为栈顶指针,所以它经常需要移动,所以编译器编译程序后的代码中很少会看到esp寻址的情况。
不过如果软件开始者想要提升逆向难度,可能会使用汇编指令来编写某个函数,即可在其中发现这样的汇编指令寻址方式。
1 |
|
esp-4
就是有值推到堆栈里面,也就是往上抬。esp+4
与之相反就是往下找具体地址,也就是esp寻址。
一个int占四个字节,所以就是esp的值减少了四。
如下在执行push 0x5,push 0xc和push0x9的操作的时候,esp就会相应的减少12,对应的十六进制就是10,所以ESP就从0019FEE4 – 10 = 0019FED8,也就是说如果要往栈里面存入数据,esp的值就是栈顶的地址值
堆栈平衡
简单知道概览即可,这里暂不深入了解
需要注意的是这段代码只有Debug版本才会有,而在Release版本中堆栈的布局与这是不一样的。
1 |
|
这段代码的意思就是对比esp和ebp是否一样,而我们知道堆栈在使用完成之后要恢复成员来的样子(堆栈平衡),所以在add指令之后ebp与esp应该是一样的,而后的call指令实际上就是调用了一个函数(**__chkesp),这个函数就是用来检查你的堆栈是否平衡**的。