文章目录
汇编
cpu整体的工作分为三类:
- 读取指令
- 指令译码
- 执行单元(计算、读写内存、设置寄存器、跳转)
读取指令、内存读写都需要CPU控制其他硬件,比如:内存、显卡
寄存器
- 4个数据寄存器(EAX、EBX、ECX和EDX)
- 2个变址和指针寄存器(ESI和EDI)
- 2个指针寄存器(ESP和EBP)
- 6个段寄存器(ES、CS、SS、DS、FS和GS)
- 1个指令指针寄存器(EIP)
- 1个标志寄存器(EFlag)
内存大小:BYTE(1个字节)、WORD(2个字节)、DWORD(4个字节)、QWORD(8个字节)
32位通用寄存器
寄存器 | 序号 | 存储数据范围 | 描述 |
---|---|---|---|
EAX | 0 | 0-0xFFFFFFFF | 数据寄存器(累加器) |
ECX | 1 | 0-0xFFFFFFFF | 数据寄存器(计数寄存器) |
EDX | 2 | 0-0xFFFFFFFF | 数据寄存器(数据寄存器) |
EBX | 3 | 0-0xFFFFFFFF | 数据寄存器(基地址寄存器) |
ESP | 4 | 0-0xFFFFFFFF | 指针寄存器(栈顶指针) |
EBP | 5 | 0-0xFFFFFFFF | 指针寄存器(栈底指针) |
ESI | 6 | 0-0xFFFFFFFF | 变址寄存器 (用于存放存储单元在段内的偏移量) |
EDI | 7 | 0-0xFFFFFFFF | 变址寄存器(用于存放存储单元在段内的偏移量) |
32位 | 16位 | 8位 |
---|---|---|
EAX | AX(EAX的低16位) | AH、AL |
ECX | CX(ECX的低16位) | CH、CL |
EDX | DX(EDX的低16位) | DH、DL |
EBX | BX(EBX的低16位) | BH、BL |
ESP | SP(ESP的低16位) | - |
EBP | BP(EBP的低16位) | - |
ESI | SI(ESI的低16位) | - |
EDI | BI(EBI的低16位) | - |
段寄存器
段寄存器 | 描述 |
---|---|
CS | 代码段 |
DS | 数据段 |
SS | 堆栈段 |
ES | 辅助段寄存器 |
FS | 辅助段寄存器 |
GS | 辅助段寄存器 |
指令指针寄存器(EIP)
EIP:保存程序当前即将要执行的指令的地址
- 不可随意修改
- 如果要修改的话,要使用jcc指令
标志寄存器(EFLAG)
EFLAG:
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
OF | DF | IF | TF | SF | ZF | AF | PF | CF | |||||||
溢出 | 方向 | 中断 | 陷阱 | 符号 | 零 | 辅助 | 奇偶 | 进位 |
注意只有运算指令(算术运算,逻辑运算)才能改变下面标志位,mov指令不算运算指令
- CF(Carry Flag):进位标志,当运算结果的最高有效位有进位(借位)时,进位标志为1,否则为0
mov al, 0x80; add al, 0x80; 此时CF会置为1
mov al, 0x80; add ax, 0x80; CF则不会置为1
- OF(OverFlow Flag):溢出标志,若运算结果有溢出则OF=1否则=0。注意:溢出是相对于有符号数的。例如:
Mov al,0x80 ;SUB AL,0X1 就会产生溢出,因为AL是8位寄存器 对于有符号数也就是-128-127
0x80就是-128 再减1就会产生溢出
判断方法:按无符号数计算,看最高位是否变化,如果变了则置1,否则置0。
- ZF(Zero Flag):零标志,若运算结果为0,则ZF=1否则ZF=0
- SF(Sgin Flag):符号标志,运算结果符号位为1,则SF=1,否则SF=0;(有符号数二进制位最高有效位表示符号)
汇编指令
汇编指令集
- X64指令集
- X64程序使用的是X64指令集,64位指令集有AMD64、EM64T、IA-64三种。
- AMD64是最早推出的,Intel与惠普联合推出了IA-64,但是没用起来。
- Intel后来直接拷贝AMD的指令集推出了IA-32E后改名为EM64T,也就是Intel64。
- 这两套指令集其实是完全一样的,我们统称X64指令集。
- X64与X86的区别:
- X86中原有的寄存器在X64中均扩展为64位,且名称的第一个字母E改为R。但是我们依然可以使用低位的寄存器 RAX、EAX、AX、AL、AH
- EIP 变成 RIP
- EFLAG 变成 RFLAG
- X64多了8个64位通用寄存器:R8-R15。每个寄存器也都可以拆分为 32位16位8位例如:R8D R8W R8B
- X64程序变化:
- 32位程序中的函数调用约定被废除了
- x64应用程序只有1种调用约定 类似于fastcall。前4个参数使用寄存器传递,如果参数超过4个,多余的参数就放在栈里,人栈顺序为从右到左,由函数调用方平衡栈空间。前4个参数存放的寄存器是固定的,分别是第1个参数RCX、第2个参数RDX、第3个参数R8、第4个参数R9,其他参数从右往左依次人栈。
- x64应用程序调用函数时参数会放到rsp-rbp段,局部变量会存放到rbp+xxx中。而32位程序中参数会放到ebp+xxx段,局部变量会放到esp-ebp段中。
汇编指令由操作码和操作数组成, 以下是全部操作数形式:
操作数 | 描述 | 举例 |
---|---|---|
r8 | 8位寄存器 | |
r16 | 16位寄存器 | |
r32 | 32位寄存器 | |
reg | 任意寄存器 | |
seg | 段寄存器 | |
m8 | 8位内存空间 | |
m16 | 16位内存空间 | |
m32 | 32位内存空间 | |
mem | 任意内存空间 | |
i8 | 8位立即数 | |
i16 | 16位立即数 | |
i32 | 32位立即数 | |
imm | 任意大小立即数 |
数据传输类型指令
- mov
mov a,b:
- a不能取立即数
- a和b的位数要一致
- a和b不能同时取内存地址
- 使用地址的时候要慎重,不能操作无法操作的内存空间
- mov eax, 0x123 : 立即数
- mov eax, ecx :寄存器
- mov bh, al
- mov eax, dword ptr ds:[0x12121212]:内存([]表示地址,dword表示要从该内存取4个字节的内容)
- mov ax, word [0x12121212]
- mov word ptr ds:[0x12121212], ax
- movzx
movzx a,b
- a必须是寄存器,b可以是寄存器或内存。a,b都不能是立即数
- b的位数要小于a的位数
- 不足的部分用0填充
- movzx eax, ax
- movsx
movsx a,b
- a必须是寄存器,b可以是寄存器或内存。a,b都不能是立即数
- b的位数要小于a的位数
- 不足的部分用b的符号位填充
- movsx eax, ax
- lea
lea a,b
- a和b不能是立即数
- a和b的位数要相同
- a和b不能同时取内存地址
- 使用地址的时候要慎重,不能操作无法操作的内存空间
- lea ecx, dword ptr ds:[eax]:将eax中的值赋值给ecx(而不是把eax当作地址),相当于 mov ecx, eax
- lea cx,word ptr ds:[edx]
算数运算指令
- add
add a,b:
- a不能是立即数
- a和b的位数要一致。若a的位数多于b的位数,则b的高位填充0;若b的位数多于a的位数,则错误
- a和b不能同时取内存地址
- 使用地址的时候要慎重,不能操作无法操作的内存空间
- add ecx, 0x12345678
- add eax, ecx
- add dword ptr ds:[0x12121212], eax
- sub
sub a,b:
- a不能是立即数
- a和b的位数要一致。若a的位数多于b的位数,则b的高位填充0;若b的位数多于a的位数,则错误
- a和b不能同时取内存地址
- 使用地址的时候要慎重,不能操作无法操作的内存空间
- sub ecx, 0x12345678
- sub eax, ecx
- sub dword ptr ds:[0x12121212], eax
位运算指令
- and
and a,b:
- a不能是立即数
- a和b的位数要一致。若a的位数多于b的位数,则b的高位填充0;若b的位数多于a的位数,则错误
- a和b不能同时取内存地址
- 使用地址的时候要慎重,不能操作无法操作的内存空间
- and ecx, 0x12345678
- and eax, ecx
- and dword ptr ds:[0x12121212], eax
- or
or a,b:
- a不能是立即数
- a和b的位数要一致。若a的位数多于b的位数,则b的高位填充0;若b的位数多于a的位数,则错误
- a和b不能同时取内存地址
- 使用地址的时候要慎重,不能操作无法操作的内存空间
- or ecx, 0x12345678
- or eax, ecx
- or dword ptr ds:[0x12121212], eax
- xor
xor a,b:
- a不能是立即数
- a和b的位数要一致。若a的位数多于b的位数,则b的高位填充0;若b的位数多于a的位数,则错误
- a和b不能同时取内存地址
- 使用地址的时候要慎重,不能操作无法操作的内存空间
应用:
- 一般用于清零操作:xor eax, eax
- 一般用于加密解密:xor eax, 0x51515151; xor eax, 0x51515151; eax两次异或同一个值,eax会恢复为原来的值。
- xor ecx, 0x12345678
- xor eax, ecx
- xor dword ptr ds:[0x12121212], eax
- not
not a:
- a不能是立即数
- 使用地址的时候要慎重,不能操作无法操作的内存空间
- not eax
- not word ptr ds:[0x12121212]
逻辑运算指令cmp、test
JCC指令
CALL、RETN指令
call指令:
Call指令类似于JMP指令,也是用来修改EIP的值的,CALL指令后面跟一个操作数,操作数可以是寄存器、内存地址、立即数。
- CALL指令与JMP区别:
它除了修改EIP寄存器之外,还修改了ESP ,等价于:JMP 函数起始地址;
push CALL指令的下一条指令;(保存主程序运行位置)
RETN 指令:
类似于 POP EIP 指令。把栈顶的值(之前主程序运行位置)弹出到EIP中。
MOVS、STOS指令
MOVS指令:
只有以下三种写法:
- MOVS BYTE PTR ES:[EDI],BYTE PTR ES:[ESI] == 简写:MOVSB
- MOVS WORD PTR ES:[EDI],WORD PTR ES:[ESI]== 简写:MOVSW
- MOVS DWORD PTR ES:[EDI],DWORD PTR ES:[ESI]== 简写:MOVSD
STOS指令:
只有以下三种写法:
- STOS BYTE PTR ES:[EDI] 简写:STOSB
- STOS WORD PTR ES:[EDI] 简写:STOSW
- STOS DWORD PTR ES:[EDI] 简写:STOSD
REP:
重复操作前缀。通常会加到 MOVS、STOS等指令前面,表示重复执行后面的指令,重复的次数由ECX寄存器的值决定。
- rep movsb
- rep stosb
寻址方式
操作数 | 寻址方式 | 举例 |
---|---|---|
数字 | 立即数寻址 | mov eax,0x12345678 |
寄存器 | 寄存器寻址 | mov eax,ecx |
内存 | 直接寻址 | mov eax,[0x12345678] |
[reg] | 寄存器间接寻址 | mov eax,[ecx] |
[reg+imm] | 寄存器相对寻址 | mov eax,[eax+0xc] |
[reg+reg+imm] | 相对基址变址寻址 | mov eax,[ecx+ebx+4] |
[reg+reg*1,2,4,8] | 带比例存储器寻址 | mov eax,[ecx+ebx*2] |
- lea
lea a,b
- a和b不能是立即数
- a和b的位数要相同
- a和b不能同时取内存地址
- 使用地址的时候要慎重,不能操作无法操作的内存空间
- lea ecx, dword ptr ds:[eax]:将eax中的值赋值给ecx(而不是把eax当作地址),相当于 mov ecx, eax
- lea cx,word ptr ds:[edx]
堆栈
堆栈的概念和操作
堆栈:指的就是内存空间中的栈内存
栈内存:
- 不需要主动申请,由系统自动分配
- 不需要主动释放,系统自动释放
- 存放局部变量,函数参数
数据结构上的特点:
- 后进先出
- 从高地址往低地址存储(栈底(EBP寄存器):高地址;栈顶(ESP寄存器):低地址)
栈内存访问方式:
- 栈顶 + 偏移
- 栈底 - 偏移
栈内存的操作:
压入栈:
第一种方式:
lea ecx, dword ptr ds:[esp-0x4];
mov dword ptr ds:[ecx], 0x12345678;
sub esp, 0x4;第二种方式:
sub esp, 0x4;
mov dword ptr ds:[esp], 0x12345678;取出栈内存的值:
第一种方式(栈顶 + 偏移):
mov eax, dword ptr ds:[esp + 0x4]第二种方式(栈底 - 偏移)
mov eax, dword ptr ds:[ebp - 0x4]弹出栈:
mov ecx, dword ptr ds:[esp];
add esp, 0x4;
- push
push a(压入栈):
- a最小为16位
- 只要执行了push,就会占4个字节的空间
- push eax;
- push 0x12;
- push ax;
- push al;(这种不行)
- push word ptr ds:[0x12121212]
- push dword ptr ds:[0x12121212]
- pop
pop a(弹出栈):
- 相当于取出栈顶元素,存放到a中
- a只能是容器(即寄存器或者内存)
- a最小为16位
- pop ax
- pop eax
- pop word ptr ds:[0x12121212]
- pop dword ptr ds:[0x12121212]
- pushad
pushad
- 将所有寄存器的内容按照序号压入栈中,首先压入eax,最后压入ebi(注意压入的esp的值是压栈之前的esp的值)
- 无操作数
- 一次扩大4*8字节空间
应用:保存程序中寄存器的值
- popad
popad
- 从栈顶将内容按照序号弹出到寄存器中。首先弹出的数值存放在ebi,最后弹出到eax
- 无操作数
- 一次减少4*8字节空间
应用:保存程序中寄存器的值
函数调用约定
- 传参,通过push指令先将参数压入堆栈(c语言:从右向左压入)
- call指令函数入口地址(跳转到函数,并将下一条语句地址push到堆栈中)
- push ebp(保存栈底指针,用于后面恢复栈底)
- mov ebp, esp(提升栈底指针,准备给函数分配栈空间)
- sub esp, xxx(提升栈顶指针,开辟专属该函数的栈空间,共该函数局部变量使用)
- push 寄存器(保存进入该函数前的寄存器的值,保证函数调用完后可以恢复原始值)
- 执行函数代码
- pop 寄存器
- mov esp, ebp(降低栈顶指针,释放专属该函数的栈空间)
- pop ebp(降低栈底指针,恢复成原始值)
- retn(pop eip,此时栈顶指针指向的位置刚好保存着进入函数之前的下一条语句的地址)
- add esp, xxx(由于调用函数前push参数导致栈顶指针提升,因此调用完要释放掉参数栈空间)
问题:
- 为什么要进行堆栈平衡?
因为Windows操作系统应用层堆栈大小默认是1M,每次调用函数都会开辟一段堆栈空间,用完如果不平衡,调用几次函数就凉凉了
- 函数的返回值放哪里了?
大部分情况返回值会放到eax中,但不是绝对的。
- 传参只能通过push到堆栈的方式吗?
也可以通过寄存器传参。
函数调用约定:
- __cdecl: C/C++里中默认调用方式
特点1:push参数,顺序从右往左。
特点2:外部平衡堆栈。(即在retn之后(函数外部)释放参数栈空间)
- __stdcall:windows API函数的调用方式 用了WINAPI的宏进行代替
特点1:push参数,顺序从右往左。
特点2:内部平衡堆栈。(即在retn之前(函数内部)释放参数栈空间)
- __fastcall:快速调用方式 这种方式选择将参数优先从寄存器传入
特点1:寄存器传参,edx,ecx (如果参数多于2个,则用push),顺序从右向左
特点2:内部平衡堆栈。(即在retn之前(函数内部)释放参数栈空间)
- X64程序变化:
- 32位程序中的函数调用约定被废除了
- x64应用程序只有1种调用约定 类似于fastcall。前4个参数使用寄存器传递,如果参数超过4个,多余的参数就放在栈里,人栈顺序为从右到左,由函数调用方平衡栈空间。前4个参数存放的寄存器是固定的,分别是第1个参数RCX、第2个参数RDX、第3个参数R8、第4个参数R9,其他参数从右往左依次人栈。
- x64应用程序调用函数时参数会放到rsp-rbp段,局部变量会存放到rbp+xxx中。而32位程序中参数会放到ebp+xxx段,局部变量会放到esp-ebp段中。