前言
逆向是一种新型的思维模式也是软件开发领域中极为重要的技术,涵盖各种维度去深挖软件架构的本质和操作系统原理,学习逆向后可以在各领域中发挥至关重要的作用,其中包括黑灰色,安全开发,客户端安全,物联网,车联网,游戏安全,红队免杀等行业中绘制出更高的闪光点。
C与汇编的关系
基本语法的学习:
- 各种进制的转换和原理
- 十进制的定义:由十个符号组成,分别是0 1 2 3 4 5 6 7 8 9 逢十进一
- 九进制的定义:由九个符号组成,分别是0 1 2 3 4 5 6 7 8 逢九进一
- 十六进制的定义:由十六个符号组成,分别是0 1 2 3 4 5 6 7 8 9 A B C D E F
- N进制的定义:由N个符号组成 逢N进一
- 数据类型与逻辑运算
在计算机中,由于硬件的制约,数据是有长度限制的,超过数据宽度的数据会被丢弃
同一个数据,表示无符号数和有符号数则其含义不同
- 无符号数:正数
- 有符号数:正数、负数
例:
- 当数据宽度为4时,即数据只能存储4位2进制位0000~1111
无符号数:
- 数据:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
- 十六进制:0 1 2 3 4 5 6 7 8 9 A B C D E F
- 二进制:0000000100100011010001010110011110001001101010111100110111101111
有符号数:
正数:
- 数据:0 1 2 3 4 5 6 7
- 十六进制:0 1 2 3 4 5 6 7
- 二进制:00001001000110100010101100111
负数:
- 数据:-1 -2 -3 -4 -5 -6 -7 -8
十六进制:F E D C B A 9 8
二进制: 11111110110111001011101010011000 - 可以发现当数据为1011,把数据看作无符号数时,数据表示为B
- 把数据看作有符号数时,数据表示为-5
- 无符号数的表示范围为0~2^4-1即0~15\\ 有符号数的表示范围为-23~23-1即-8~7
常见的数据类型(重要):
- BYTE 字节 8BIT 1字节
- WORD 字 16BIT 2字节
- DWORD 双字 32BIT 4字节
常见的运算符类型(重要):
或运算(or |):
两个数只要有一个为1则结果为1
与运算(and &):
两个数都是1结果才为1
异或运算(xor ^):
两个数相同为0, 不同为1
非运算(not !):
两个数取反 1是0, 0是1
CPU如何计算2+3?
X:0010
Y:0011
先异或
R:0001
异或完以后要判断是否运算结束
将两个数进行与运算 然后左移一位
0010<<1 ==0100
如果结果全为0,结果则为我们所要的结果
否则,把上面异或得到的值赋值到X
把左移后的结果赋值到Y
X:0001
Y:0100
重复操作
先异或
R:0101
再将两个数进行与运算 然后左移一位
左移完结果全是0,结果则为我们所要的
最终结果为0101=5
CPU如何计算2-3?
X:0010
Y:1101
先异或
R:1111
将两个数进行与运算 然后左移一位
0000<<1=0000
如果结果全为0,结果则为我们所要的结果
最终结果为1111 = -1
如何取某个值的第N位的数值:
与操作
如我们想要查看23h这个十六进制数的第3位则可以进行如下运算:
先将23h转化为二进制:0010 0011
最简单的加密算法:
通过异或加密数据 再次异或后则解密数据
要加密的数据:2021:0010 0000 0010 0001
密钥:54:0101 0100
高位:0111 0100 = 74
低位:0111 0101 = 75
原本的2021加密成了7475
然后再次进行异或操作进行解密:
高位:0010 0000 = 20
低位:0010 0100 = 21
解密回了原来的数值2021
- 通用寄存器和内存读写
32位通用寄存器的指定用途如下:
堆栈相关汇编指令:
MOV指令
MOV 的语法:
MOV r/m8,r8
MOV r/m16,r16
MOV r/m32,r32
MOV r8,r/m8
MOV r16,r/m16
MOV r32,r/m32
MOV r8, imm8
MOV r16, imm16
MOV r32, imm32
MOV 目标操作数,源操作数
作用:拷贝源操作数到目标操作数
源操作数可以是立即数、通用寄存器、段寄存器、或者内存单元
目标操作数可以是通用寄存器、段寄存器或者内存单元
操作数的宽度必须一样
源操作数和目标操作数不能同时为内存单元
ADD指令
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
ADD 目标操作数,源操作数
作用:将源操作数加到目标操作数上
SUB指令
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
SUB 目标操作数,源操作数
作用:将源操作数减到目标操作数上
AND指令
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
AND 目标操作数,源操作数
作用:将源操作数与目标操作数与运算后将结果保存到目标操作数中
OR指令
OR 的语法:
OR r/m8, imm8
OR r/m16,imm16
OR r/m32,imm32
OR r/m16, 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
OR 目标操作数,源操作数
作用:将源操作数与目标操作数或运算后将结果保存到目标操作数中
XOR指令
XOR 的语法:
XOR r/m8, imm8
XOR r/m16,imm16
XOR r/m32,imm32
XOR r/m16, imm8
XOR r/m8, r8
XOR r/m32, r32
XOR r8, r/m8
XOR r16, r/m16
XOR r32, r/m32
XOR 目标操作数,源操作数
作用:将源操作数与目标操作数异或运算后将结果保存到目标操作数中
NOT指令
NOT 的语法:
NOT r/m8
NOT r/m16
NOT r/m32
NOT 操作数
作用:取反
LEA指令
lea:Load Effective Address,即装入有效地址的意思,它的操作数就是地址
lea r32,dword ptr ds:[内存编号(地址)]
将内存地址赋值给32位通用寄存器
lea是传址,mov是传值,注意区别
- 堆栈结构
Windows分配栈时 是从高地址往低地址分配:
- MOV EBX,0x13FFDC BASE
- MOV EDX,0x13FFDC TOP
栈底和栈顶可以是两个任意的寄存器(Windows采用的是EBP和ESP)
刚开始堆栈为空,栈顶和栈底相同
先将数据压入后再修改栈顶
数据压入
MOV DWORD PTR DS:[EDX-4],0xAAAAAAAA
修改栈顶
SUB EDX,4
先修改栈顶后再将数据压入
修改栈顶
LEA EDX,DWORD PTR DS:[EDX-4] (和上面的SUB一样)
数据压入
MOV DOWRD PTR DS:[EDX],0xAAAAAAAA
栈顶加偏移读取
MOV ESI,DWORD PTR DS:[EBX-8]
栈底加偏移读取
MOV EDI,DWORD PTR DS:[EDX+4]
先取出数据再修改栈顶
取出数据
MOV EAX,DOWRD PTR DS:[EDX]
修改栈顶
ADD EDX,4
先修改栈顶再取出数据
修改栈顶
LEA EDX,DWORD PTR DS:[EDX+4]
取出数据
MOV EAX,DOWRD PTR DS:[EDX-4]
入栈和出栈操作也有对应的指令:
上面我们自己模拟的两个用作栈顶和栈底的寄存器在WINDOWS中分别对应ESP和EBP
并且前面我们自己模拟的入栈和出栈操作也有对应的指令:PUSH 和 POP
就是封装了压入数据和修改栈顶的操作
- PUSH 和 POP
- push xxx将 xxx的数据压入堆栈
- pop xxx将栈顶的数据存储到xxx中
PUSH指令:
PUSH r32
PUSH r16
PUSH m16
PUSH m32
PUSH imm8/imm16/imm32
所有的push都是将esp-4?
压入的数据的数据宽度:
当push的是立即数将esp-4
当push r32如push eax时将esp-4
当push dword ptr ds:[12FFDA]即压入双字内存地址中的数据时将esp-4
当push word ptr ds:[12FFDA]即压入字内存地址中的数据时将esp-2
当push ax,即r16 ,16位通用寄存器时,esp-2
push 不允许压入数据宽度为8的数据 如ah al 和byte ptr ds:[内存编号]
POP指令
POP r32
POP r16
POP m16
POP m32
PUSHAD和POPAD指令
将所有的32位通用寄存器压入堆栈,方便后面随意使用寄存器,用于保护现场
与POPAD对应
PUSHFD和POPFD指令
然后将32位标志寄存器EFLAGS压入堆栈
与POPAD对应
其它相关指令
pusha:将所有的16位通用寄存器压入堆栈
popa:将所有的16位通用寄存器取出堆栈
pushf::将的16位标志寄存器EFLAGS压入堆栈
popf:将16位标志寄存器EFLAGS取出堆栈
栈底和栈顶原理:
- 控制栈顶和栈底分别为两个固定的寄存器(EBP 基址指针寄存器 和 ESP 堆栈指针寄存器)
- 刚开始堆栈为空,栈顶和栈底相同
标志寄存器:
- 进位标志CF(Carry Flag)
- 奇偶标志PF(Parity Flag)
- 辅助进位标志AF(Auxiliary Carry Flag)
- 零标志ZF(Zero Flag)
- 符号标志SF(Sign Flag)
- 溢出标志OF(Overflow Flag)
- 方向标志DF(Direction Flag)
EFLAGS寄存器
进位标志CF(Carry Flag)
如果运算结果的最高位产生了一个进位或借位,那么,其值为1,否则其值为0
例子:
MOV AL,0xFF
ADD AL,1
- 0x80+0x40
加黑的为最高位
0x80:0 1000 0000
0x40:0 0100 0000
结果为1100 0000 最高位并没有发生变化,于是CF位为0
- 0x80-0x40
注意这里借位的位是1000 0000中的加黑部分
而非0 1000 0000这里的最高位
结果为0100 0000 最高位并没有发生变化,于是CF位为0
- 0x80-0x81
0x80:1000 0000
0x81:1000 0001
结果为1111 1111= -1,最高位被借位,于是CF位为1
奇偶标志PF(Parity Flag)
奇偶标志PF用于反映运算结果中最低有效字节中“1”的个数的奇偶性
如果“1”的个数为偶数,则PF的值为1,否则其值为0。
指令指令执行后AL的结果PFMOV AL,300111ADD AL,301101ADD AL,210000
例:
MOV AX,803
ADD AX,1
0x803: 0000 1000 0000 0011
执行结果
0x804: 0000 1000 0000 0100 总共2个1 ,PF应为1,但实际运行结果PF为0
因为PF是根据最低有效字节来看,即804后面04的这部分
04: 0000 0100 总共1个1,所以PF为0
辅助进位标志AF(Auxiliary Carry Flag)
在发生下列情况时,辅助进位标志AF的值被置为1,否则其值为0:
- 在字操作时,发生低字节向高字节进位或借位时
- 在字节操作时,发生低4位向高4位进位或借位时
AF与数据宽度相关
32位时 FFFF F FFF
16位时 FF F F
8位时 F F
加黑的字体为AF标志位判断的位置,如果该位置要向前进位则AF为1,否则为0,和CF相似,不过判断的位置不同
32位例:
MOV EAX,55EEFFFF
ADD EAX,2
16位例:
MOV AX,5EFE
ADD AX,2
8位例:
MOV AL,4E
ADD AL,2
零标志ZF(Zero Flag)
零标志ZF用来反映运算结果是否为0
如果运算结果为0,则其值为1,否则其值为0
作用:在判断运算结果是否为0时,可使用此标志位
例子:
XOR EAX,EAX
通过xor将eax清零,会改变zf标志位为1
MOV EAX,0
通过MOV将EAX赋值为0,非运算,不改变zf标志位
符号标志SF(Sign Flag)
符号标志SF用来反映运算结果的符号位,它与运算结果的最高位相同
例子:
MOV AL,7F
ADD AL,2
溢出标志OF(Overflow Flag)
溢出标志OF用于反映有符号数加减运算所得结果是否溢出
注意与CF区分!!!
最高位进位与溢出的区别:
进位标志表示无符号数运算结果是否超出范围.
溢出标志表示有符号数运算结果是否超出范围.
溢出主要是给有符号运算使用的,在有符号的运算中,有如下的规律:
- 正 + 正 = 正 如果结果是负数,则说明有溢出
- 负 + 负 = 负 如果结果是正数,则说明有溢出
- 正 + 负 永远都不会有溢出
无符号、有符号都不溢出例
MOV AL,8
ADD AL,8
AL的数据宽度为8,即
无符号数范围为0~FF即0~255
8+8=16在0~255内 不溢出
有符号数的范围为
正数:0~7F 即0~127
负数:80~FF 即 -128~0
8+8=16 在0~127内 两正数相加结果仍为正数,不溢出
无符号溢出、有符号不溢出例
MOV AL,0FF
ADD AL,2
无符号数时
FF+2=255+2=257 在0~255外,溢出
有符号数时
FF+2=-1+2=1
正 + 负 永远都不会有溢出
无符号不溢出、有符号溢出例
MOV AL,7F
ADD AL,2
无符号数时
7F+2=127+2=129 在0~255内 不溢出
有符号数时
7F+2=0x81在80~FF (负数范围)内,两正数相加结果为负数,溢出
无符号、有符号都溢出
MOV AL,0FE
ADD AL,80
无符号数时
FE+2=254+2=256=0x100 在0~255外 溢出
有符号数时
FE+2=0x100在0~FF外,溢出
CPU如何计算OF位
首先引入两个概念:
- 符号位有进位
- 最高有效数值位向符号位产生的进位
对于一个有符号数:如0x80和0xC0
符号位有进位
0x80:1 000 0000
0xC0:1 100 0000
最高有效数值位向符号位产生的进位
0x80:1 0 00 0000
0xC0:1 1 00 0000
接下来看一组汇编指令
MOV AL,80
ADD AL,0C0
就是运算0x80+0xc0
0x80:1 0 00 0000
0xC0:1 1 00 0000
符号位1+1有产生进位,于是符号位有进位为1
最高有效数值位向符号位产生的进位0+1没有产生进位,于是最高有效数值位向符号位产生的进位为0
OF = 符号位有进位 xor 最高有效数值位向符号位产生的进位
OF = 1 xor 0 = 1 所以此时OF=1
方向标志DF(Direction Flag)
DF:方向标志位
DF=1时串操作为减地址方式 DF=0为增地址方式
下面的MOVS指令有说明DF的具体应用
相关汇编指令
符号含义r寄存器m内存imm立即数r88位通用寄存器m88位内存imm88位立即数
ADC指令:带进位加法
格式:ADC R/M,R/M/IMM 两边不能同时为内存 数据宽度要一样
例:
MOV AL,1
MOV CL,2
手动修改CF为1
ADC AL,CL
计算结果为4,原本1+2=3,但是现在变成了4,注意与ADD的区别就在于进位
SBB指令:带借位减法
格式:SBB R/M,R/M/IMM 两边不能同时为内存 数据宽度要一样
MOV AL,4
MOV CL,2
手动修改CF为1
SBB AL,CL
计算结果为1,原本4-2=2,但是现在变成了1,注意与SUB的区别就在于进位
XCHG指令:交换数据
格式:XCHG R/M,R/M 两边不能同时为内存 数据宽度要一样
XCHG AL,CL
XCHG DWORD PTR DS:[12FFC4],EAX
XCHG BYTE PTR DS:[12FFC4],AL
例:
MOV AL,1
MOV CL,2
XCHG AL,CL
执行前:AL=1 CL=2
执行后:AL=2 CL=1
MOVS指令:移动数据 内存-内存
BYTE/WORD/DWORD
MOVS指令常用于复制字符串
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
例:
MOV EDI,12FFD8
MOV ESI,12FFD0
MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI]
执行后,EDI内存里的值被修改为ESI内存里的值,且EDI和ESI各加4
为什么各加4?
和DOWRD数据宽度相关,如果为WORD 则各加2
为什么执行完是加而不是减?
由DF(Direction Flag)方向标志位决定,当DF位为1时为减,当DF位为0时,则为加
STOS指令
将Al/AX/EAX的值存储到[EDI]指定的内存单元,和数据宽度相关
STOS BYTE PTR ES:[EDI] 将AL存储到[EDI]
STOS WORD PTR ES:[EDI] 将AX存储到[EDI]
STOS DWORD PTR ES:[EDI] 将EAX存储到[EDI]
注意这里使用的是ES: 之前写的都是DS:
当后面为[EDI]时要使用ES: 这和后面要学的段寄存器有关,先记住
存储完数据后EDI地址的变化方向也受DF标志控制,1减0增
REP指令
按计数寄存器 (ECX) 中指定的次数重复执行指令
MOV ECX,10
REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI] 也可以写成REP MOVSD
这里的10为十六进制,也就是0x10=16
代码将会重复执行16次,会不会往同一个地方覆盖?
不会,因为每执行一次EDI和ESI都会变化4,变化方向由DF决定
汇编跳转和比较指令:
- JCC指令
- JMP指令
- CALL指令
- RET指令
- CMP指令
- TEST指令
- JCC指令表
JCC指令
cc 代表 condition code(状态码)
Jcc不是单个指令,它只是描述了跳转之前检查条件代码的跳转助记符
例如JNE,在跳转之前检查条件代码
典型的情况是进行比较(设置CC),然后使用跳转助记符之一
CMP EAX,0
JNE XXXXX
条件代码也可以用AND、OR、XOR、加法、减法(当然也可以是CMP)等指令来设置
JCC指令用于改变EIP(CPU要读取的指令地址)
JMP指令
JMP指令:修改EIP的值
JMP指令只影响了EIP,不影响堆栈和其它通用寄存器
JMP 寄存器/立即数 相当于 MOV EIP,寄存器/立即数
CALL指令
CALL指令和JMP指令都会修改EIP的值
但CALL指令会将返回地址(CALL指令的下一条指令地址)压入堆栈
因此也会引起esp的变化
RET指令
call调用跳转后执行完相关代码完要返回到call的下一条指令时使用ret指令
ret指令相当于pop eip
比较指令
CMP指令
指令格式:CMP R/M,R/M/IMM
CMP指令只改变标志寄存器的值
该指令是比较两个操作数,实际上,它相当于SUB指令,但是相减的结果并不保存到第一个操作数中
只是根据相减的结果来改变ZF零标志位的,当两个操作数相等的时候,零标志位置1
例:
MOV EAX,100
MOV EBX,200
CMP EAX,ECX
CMP AX,WORD PTR DS:[405000]
CMP AL,BYTE PTR DS:[405000]
CMP EAX,DWORD PTR DS:[405000]
TEST指令
指令格式:TEST R/M,R/M/IMM
该指令在一定程度上和CMP指令时类似的,两个数值进行与操作,结果不保存,但是会改变相应标志位
与的操作表项如下:
运算
结果
1 and 1
1
1 and 0
0
0 and 1
0
0 and 0
0
可以看到只要有任一操作数为0时,结果就为0
常见用法:用这个指令,可以确定某寄存器是否等于0
只有当eax=0时 eax and eax才会是0
所以
TEST EAX,EAX
观察ZF(零标志位)就可以判断EAX是否为0
堆栈结构图(重点):
- 调用CALL又可以分为六个部分:
- 提升堆栈
- 保护现场
- 初始化提升的堆栈
- 执行实际内容
- 恢复现场
- 返回
提升堆栈
对应语句为
00401040 /> \55 push ebp
00401041 |. 8BEC mov ebp,esp
00401043 |. 83EC 40 sub esp,0x40
将堆栈提升了0x40
保护现场
对应语句为
00401046 |. 53 push ebx
00401047 |. 56 push esi
00401048 |. 57 push edi
将ebx、esi、edi三个通用寄存器保存到堆栈中,前面的push ebp其实也属于保护现场
初始化提升的堆栈
00401049 |. 8D7D C0 lea edi,dword ptr ss:[ebp-0x40]
0040104C |. B9 10000000 mov ecx,0x10
00401051 |. B8 CCCCCCCC mov eax,0xCCCCCCCC
00401056 |. F3:AB rep stos dword ptr es:[edi]
这里将我们提升的堆栈中的内容全部初始化为CCCCCCCC
为什么是初始化为CC?防止缓冲溢出
CC的硬编码对应的指令为int 3,即断点
这么做有什么好处呢?当程序执行超过缓冲区时,遇到int 3就会自动停下来
执行实际的内容
对应语句为
00401058 |. 8B45 08 mov eax,dword ptr ss:[ebp+0x8]
0040105B |. 0345 0C add eax,dword ptr ss:[ebp+0xC]
就是将前面压入的参数2和1进行相加得到3
恢复现场
对应语句为
0040105E |. 5F pop edi ; HelloWor.00401171
0040105F |. 5E pop esi ; HelloWor.00401171
00401060 |. 5B pop ebx ; HelloWor.00401171
00401061 |. 8BE5 mov esp,ebp
00401063 |. 5D pop ebp ; HelloWor.00401171
与前面保护现场相对应
返回
对应语句为
00401064 \. C3 retn
CALL返回后
对应语句为
00401171 |. 83C4 08 add esp,0x8
作用为平衡堆栈
逆推C语言代码
根据我们前面的分析,我们不难发现这其实就是个简单的加法函数
int add(int x,int y){
x=x+y; //这里的x和y分别对应压入的参数
return x; //对应RETN 默认采用eax作为返回值的传递载体
}
- 反汇编分析C语言:
函数内部
接着进到函数的内部
有了之前画堆栈图的经验,我们不难看出,尽管我们的函数是个空函数,但其汇编代码依然完成了以下流程:
- 提升堆栈
- 保护现场
- 初始化提升的堆栈
- 恢复现场
- 返回
提升堆栈
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,40h
保护现场
00401016 push ebx
00401017 push esi
00401018 push edi
PS:前面的push ebp也是保护现场
初始化提升的堆栈
00401019 lea edi,[ebp-40h]
0040101C mov ecx,10h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
恢复现场
00401028 pop edi
00401029 pop esi
0040102A pop ebx
0040102B mov esp,ebp
0040102D pop ebp
//空函数
void function(){
}
int main(int argc, char* argv[])
{
//调用空函数
function();
return 0;
}
- C语言内联汇编和调用协定
int __declspec (naked) Plus(int x,int y){
__asm{
//保留调用前堆栈
push ebp
//提升堆栈
mov ebp,esp
sub esp,0x40
//保护现场
push ebx
push esi
push edi
//初始化提升的堆栈,填充缓冲区
mov eax,0xCCCCCCCC
mov ecx,0x10
lea edi,dword ptr ds:[ebp-0x40]
rep stosd
//函数核心功能
//取出参数
mov eax,dword ptr ds:[ebp+8]
//参数相加
add eax,dword ptr ds:[ebp+0xC]
//恢复现场
pop edi
pop esi
pop ebx
//降低堆栈
mov esp,ebp
pop ebp
//返回
ret
}
}
void __declspec (naked) function(){
__asm{
ret
}
}
- 汇编寻找C程序入口
- mainCRTStartup 和 wmainCRTStartup 是控制台环境下多字节编码和Unicode 编码的启动函数
- 而WinMainCRTStartup 和wWinMainCRTStartup 是windows 环境下多字节编码和Unicode 编码的启动函数
mainCRTStartup做了哪些事:
- 前面我们已经知道了mainCRTStartup也就是程序入口,那么如何通过mainCRTStartup来找到main函数入口
- 根据函数的参数来进行判断
- main函数貌似只有两个参数,但实际上main函数一共有三个参数,只不过一般第三个参数我们并没有用到,于是在使用main函数时并没有加上,完整的main函数原型如下:
int main(int argc,char *argv[],char *envp[]){}
这里的argv和envp对应mainCRTStartup里_setargv()和_setenvp()
main函数的三个参数:
- 汇编C语言基本类型
#include "stdafx.h"
int main(int argc, char* argv[])
{
char a=0xFF;
short b=0xFF;
int c=0xFF;
long d=0xFF;
return 0;
}
- 汇编 全局和局部 变量
全局变量
- MOV 寄存器,byte/word/dword ptr ds:[0x12345678]
- 上面的0x12345678是固定的地址,每次程序启动都不变,通过寄存器的宽度,或者byte/word/dword 来判断全局变量的宽度
- 全局变量就是所谓的基址
局部变量
- 局部变量在程序编译完成后并没有分配固定的地址
- 在所属的方法没有被调用时,局部变量并不会分配内存地址,只有当所属的程序被调用了,才会在堆栈中分配内存
- 当局部变量所属的方法执行完毕后,局部变量所占用的内存将变成垃圾数据.局部变量消失
- 局部变量只能在函数内部使用,函数A无法使用函数B的局部变量
- 局部变量的反汇编识别
[ebp-4]
[ebp-8]
[ebp-0xC]
例子:
#include "stdafx.h"
//全局变量
int global=0x610;
int main(int argc, char* argv[])
{
//局部变量
int temp=0x160;
global=global+temp;
return 0;
}
汇编C语言类型转换
类型转换相关汇编指令
-
MOVSX
-
先符号扩展,再传送
-
MOV AL,0FF
MOVSX CX,AL
MOV AL,80
MOVSX CX,AL -
MOVZX
-
先零扩展,再传送
-
MOV AL,0FF
MOVZX CX,AL
MOV AL,80
MOVSX CX,AL
#include "stdafx.h"
int main(int argc, char* argv[])
{
unsigned char i=0xFF;
printf("%d\n",i);
int j=i+1;
i=i+1;
printf("%d\n",i);
printf("%d\n",j);
return 0;
}
- 汇编嵌套if else
#include "stdafx.h"
int result=0;
int getMax(int i,int j,int k){
if(i>j){
if(i>k){
return i;
}else{
return k;
}
}else{
if(j>k){
return j;
}else{
return k;
}
}
}
int main(int argc, char* argv[])
{
result=getMax(1,2,3);
printf("%d\n",result);
result=getMax(1,3,2);
printf("%d\n",result);
result=getMax(2,1,3);
printf("%d\n",result);
result=getMax(2,3,1);
printf("%d\n",result);
result=getMax(3,1,2);
printf("%d\n",result);
result=getMax(3,2,1);
printf("%d\n",result);
return 0;
}
#include "stdafx.h"
int getMax2(int i,int j){
if(i>j){
return i;
}else{
return j;
}
}
int main(int argc, char* argv[])
{
getMax2(1,2);
return 0;
}
汇编比较三种循环
1.
#include "stdafx.h"
int loop1(){
int i=0,j=0;
for(i=0;i<10;i++){
j=j+i;
}
return j;
}
int loop2(){
int i=0,j=0;
while(i<10){
j=j+i;
i=i+1;
}
return j;
}
int loop3(){
int i=0,j=0;
do {
j=j+i;
i=i+1;
} while(i<10);
return j;
}
int main(int argc, char* argv[])
{
int result=0;
result=loop1();
printf("%d\n",result);
result=loop2();
printf("%d\n",result);
result=loop3();
printf("%d\n",result);
return 0;
}
汇编一维数组
- 数组的寻址
数组的存储并不复杂,接下来看看如何来找到数组的某个成员
#include "stdafx.h"
void function(){
int x=1;
int y=2;
int r=0;
int arr[5]={1,2,3,4,5};
r=arr[1];
r=arr[x];
r=arr[x+y];
r=arr[x*2+y];
}
int main(int argc, char* argv[])
{
function();
return 0;
}
#include "stdafx.h"
void function(){
int arr[5]={1,2,3,4,5};
}
int main(int argc, char* argv[])
{
function();
return 0;
}
数组越界的应用:
#include "stdafx.h"
void function(){
int arr[5]={1,2,3,4,5};
arr[6]=0x12345678;
}
int main(int argc, char* argv[])
{
function();
return 0;
}
- 汇编二维数组位移 乘法
位移
前面在寻址的过程中分别用到了乘法,当乘数为2的n次方时,可以直接使用左移来实现,无需imul指令
汇编中有常用的两种位移指令:shl和shr
使用方法并没有太大的区别,这里就拿shl指令作为例子
shl指令:
SHL是一个汇编指令,作用是逻辑左移指令,将目的操作数顺序左移1位或CL寄存器中指定的位数。左移一位时,操作数的最高位移入进位标志位CF,最低位补零。
运算例子:
乘法
imul指令:
imul指令使用起来和div指令有些类似
IMUL(有符号数乘法)指令执行有符号整数乘法
x86 指令集支持三种格式的 IMUL 指令:单操作数、双操作数和三操作数。单操作数格式中,乘数和被乘数大小相同,而乘积的大小是它们的两倍
这里限于篇幅,仅介绍上面使用到的三操作数,其余部分可以参考:汇编语言IMUL指令:汇编语言IMUL指令:有符号数乘法
例子
int i=1;
i=i*5;
i=i*6;
i=i*7;
int i=1;
i=i*4;
i=i*8;
i=i*16;
int arr[3][4]={
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
int a=arr[2][3];
int i=1,j=2;
int b=arr[i][j];
int c=arr[i+j][i*2];
int arr[3][4]={
{1,2,0,0},
{5,6,7,0},
{9,0,0,0}
};
int arr[3][4]={
{1,2},
{5,6,7},
{9}
};
- 汇编 结构体和内存对齐
#include "stdafx.h"
#include <string.h>
struct Player{
float hp; //人物血量
float mp; //人物魔力值
int money; //人物金钱
int atk; //人物攻击力
char name[10]; //人物昵称
float x; //人物x坐标
float y; //人物y坐标
};
int main(int argc, char* argv[])
{
Player player;
player.hp=100;
player.mp=50;
player.money=1000;
player.atk=10;
strcpy(player.name,"lyl610abc");
player.x=600;
player.y=100;
return 0;
}
#include "stdafx.h"
#include <string.h>
struct Player{
float hp; //人物血量
float mp; //人物魔量
int money; //人物金钱
int atk; //人物攻击力
char name[10]; //人物昵称
float x; //人物x坐标
float y; //人物y坐标
};
Player retStruct(){
Player player;
return player;
}
int main(int argc, char* argv[])
{
Player player;
player=retStruct();
return 0;
}
内存对齐
1.内存对齐也称作字节对齐
2.前面或多或少都有提到过内存对齐,但没有具体展开,现在来谈谈内存对齐
3.为什么要内存对齐
性能原因:
寻址时提高效率,采用了以空间换时间的思想
当寻址的内存的单位和本机宽度一致时,寻址的效率最高
举个例子:
在32位的机器上,一次读32位(4字节)的内存 效率最高
在64位的机器上,一次读64位(8字节)的内存 效率最高
- 汇编switch比较if else
#include "stdafx.h"
void MySwitch(int x){
switch(x) {
case 1:
printf("num is 1\n");
break;
case 2:
printf("num is 2\n");
break;
case 3:
printf("num is 3\n");
break;
default:
printf("no cases match\n");
break;
}
}
int main(int argc, char* argv[])
{
MySwitch(2);
return 0;
}
switch(表达式)中,表达式应该为整数类型:char short int long,其它类型诸如:float、double等类型均不可以
switch搭配case使用,case里如果没有添加break语句则会继续向下执行下面的case
default语句可以没有,如果所有case都不匹配会默认执行default语句
- 汇编角度看C指针模型
void function(){
char* a;
a=(char*) 610;
int** b;
b=(int**) 610;
}
void function(){
int a;
a=610;
a=(int)610;
}
void function(){
char* a;
short* b;
int* c;
a=(char*) 1;
b= (short*) 2;
c=(int*) 3;
}
#include "stdafx.h"
void function(){
char* a;
short* b;
int* c;
a=(char*) 1;
b= (short*) 2;
c=(int*) 3;
a++;
b++;
c++;
printf("a:%d\t b:%d\tc:%d\n",a,b,c);
}
int main(int argc, char* argv[])
{
function();
return 0;
}
(4):编译器原理
编译一个程序一般由预处理器、解释器、汇编器、链接器、加载器等组成,进而将一个高级语言编译成机器可以执行的二进制语言。
- 预处理器
主要进行宏处理,文件包含,语言拓展等。
- 解释器
将一个高级语言转化成一个机器语言,解释器从输入中读取一句,将其变成中间代码,执行它,再读取下一句,如果有错误发生,则解释器停止执行并且报告错误。
- 汇编器
将汇编语言转化成机器语言,汇编器的输出文件叫做目标文件,是一系列机器指令的集合,并且将它们存放到内存中去。
- 链接器
其作用是将一些目标文件连接起来形成一个可执行文件,所有这些文件可能有不同的汇编器汇编的,链接器的作用是寻找定位程序的参考模块,决定代码被加载到内存位置。
- 加载器
加载器是操作系统的一部分,负责加载可执行文件并且执行它们,它计算程序的大小,为其创造内存空间,初始化多个寄存器来进行执行初始化。
- 几个基本概念
编译:编译器对源文件的编译过程,就是将源文件中的文本形式代码翻译为机器语言形式的目标文件的过程,此过程中会有一系列语法检查、指令优化等,生成目标(OBJ)文件。
编译单元:每一个CPP文件就是一个编译单元,每个单元之间是互相独立且不可知的。
目标文件:编译步骤产生的文件,包含了编译单元内所有代码和数据,以二进制形式存在。请注意三个关键表:未解决符号表、导出符号表、地址重定向表。
链接:当编译器将工程中所有CPP文件以分离形式编译成各个对应目标(OBJ)文件之后,再由链接器进行链接生成一个EXE或者DLL文件。
- 一个例子
用一个例子来理解下编译器与链接器的工作:
file1.cpp
int gVal = 123;
void func1()
{
++gVal;
}
这个文件编译出来的目标文件file1.obj 就会有一个段来包含上面的数据和函数,内容大致如下(只是示意,并非完全一样):
偏移量 内容 长度
0x0000 gVal 4
0x0004 func1 ??
这里的??表示长度未知,实际目标文件的各个数据可能不是连续的,也不一定从0x0000开始。比如这里的func1可能是这样:
0x0004 inc DWORD PTR[0x0000]
0x00?? ret
这里把++gVal翻译为inc语句,也就是把本单元的0x0000地址的一个DWORD(4字节)进行加一。
file2.cpp
extern int gVal;
void func2()
{
++gVal;
}
对应的file2.obj文件内容应该是:
偏移量 内容 长度
0x0000 func2 ??
可以看到这里并没有gVal,原因是extern关键字声明这是个外部引用符号,已经在别的单元里面定义了。由于单元之间是隔离的,所以这里的func2代码就没有办法填写地址,大致是这样:
0x0004 inc DWORD PTR[????]
0x00?? ret
这个???就表示当前无法拿到有效地址,需要链接器来完成,如何告诉链接器?需要一个未解决符号表(unresolved symbol table),同样提供gVal符号的目标文件也要提供一个导出符号表(export symbol table)来告知链接器可以提供的符号内容。
这两个表之间依靠符号来进行关联,在C/C++中每一个变量和函数都有自己的符号名,函数名略复杂(依据编译器不同而有差异),假设这里函数func1的符号为_func1,那么file1.obj文件的导出符号为:
导出符号 地址
gVal 0x0000
_func1 0x0004
而对应的未解决符号表则为空,因为没有依赖其他单元的内容。另一个file2.cpp文件的导出符号表为:
导出符号 地址
_func2 0x0000
而对应的未解决符号表为(下表的含义是说0x0001位置有一个地址不明,符号叫gVal):
未决符号 地址
gVal 0x0001
链接器会针对未解决符号表中的在所有导出符号表中进行匹配,如果找到则匹配填写进来,如果找不到就会报链接错误。但这里可以发现一个问题,就是gVal的符号地址是0x0000,如果直接将解析的地址替换未解决符表中的???就会生成一个冲突的地址0x0000,与本单元的地址重复了。为了解决这个问题,还需要针对每一个编译单元引入的一个地址偏移。例如file1.obj的地址从0x00001000开始,file2.obj的地址从0x00002000开始,这样符号地址叠加后就不会重复了。记录这样的地址偏移量的表称之为地址重定向表(address redirect table)。
- 链接器工作顺序
当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。
说明:实现链接的时候会更加复杂,一般实现的目标文件都会把数据,代码分成好向个区,重定向按区进行,但原理都是一样的。
extern:这就是告诉编译器,这个变量或函数在别的编译单元里定义了,也就是要把这个符号放到未解决符号表里面去(外部链接)。
static:如果该关键字位于全局函数或者变量的声明前面,表明该编译单元不导出这个函数或变量,因些这个符号不能在别的编译单元中使用(内部链接)。如果是static局部变量,则该变量的存储方式和全局变量一样,但是仍然不导出符号。
默认链接属性:对于函数和变量,默认链接是外部链接,对于const变量,默认内部链接。
外部链接:外部链接的符号在整个程序范围内都是可以使用的,这就要求其他编译单元不能导出相同的符号(不然就会报duplicated external symbols)。
内部链接:内部链接的符号不能在别的编译单元中使用。但不同的编译单元可以拥有同样的名称的符号