盐城网站建设包括哪些,做网站要考虑什么问题,网站页面制作软件,thinkphp仿wordpress前言
逆向是一种新型的思维模式也是软件开发领域中极为重要的技术#xff0c;涵盖各种维度去深挖软件架构的本质和操作系统原理#xff0c;学习逆向后可以在各领域中发挥至关重要的作用#xff0c;其中包括黑灰色#xff0c;安全开发#xff0c;客户端安全#xff0c;物…前言
逆向是一种新型的思维模式也是软件开发领域中极为重要的技术涵盖各种维度去深挖软件架构的本质和操作系统原理学习逆向后可以在各领域中发挥至关重要的作用其中包括黑灰色安全开发客户端安全物联网车联网游戏安全红队免杀等行业中绘制出更高的闪光点。
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 FN进制的定义由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无符号数的表示范围为02^4-1即015\\ 有符号数的表示范围为-2323-1即-87
常见的数据类型重要
BYTE 字节 8BIT 1字节WORD 字 16BIT 2字节DWORD 双字 32BIT 4字节
常见的运算符类型重要:
或运算or |: 两个数只要有一个为1则结果为1
与运算and : 两个数都是1结果才为1
异或运算xor ^: 两个数相同为0 不同为1
非运算(not !): 两个数取反 1是0 0是1
CPU如何计算23?
X:0010
Y:0011
先异或 R:0001
异或完以后要判断是否运算结束
将两个数进行与运算 然后左移一位 00101 0100
如果结果全为0,结果则为我们所要的结果
否则把上面异或得到的值赋值到X
把左移后的结果赋值到Y
X0001
Y0100
重复操作
先异或 R:0101
再将两个数进行与运算 然后左移一位 左移完结果全是0结果则为我们所要的
最终结果为01015
CPU如何计算2-3?
X0010
Y1101
先异或 R1111
将两个数进行与运算 然后左移一位 000010000
如果结果全为0,结果则为我们所要的结果
最终结果为1111 -1
如何取某个值的第N位的数值
与操作
如我们想要查看23h这个十六进制数的第3位则可以进行如下运算:
先将23h转化为二进制0010 0011 最简单的加密算法
通过异或加密数据 再次异或后则解密数据
要加密的数据20210010 0000 0010 0001
密钥540101 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, imm32MOV 目标操作数源操作数
作用拷贝源操作数到目标操作数
源操作数可以是立即数、通用寄存器、段寄存器、或者内存单元
目标操作数可以是通用寄存器、段寄存器或者内存单元
操作数的宽度必须一样
源操作数和目标操作数不能同时为内存单元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/m32ADD 目标操作数源操作数
作用将源操作数加到目标操作数上
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/m32SUB 目标操作数源操作数
作用将源操作数减到目标操作数上
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/m32AND 目标操作数源操作数
作用将源操作数与目标操作数与运算后将结果保存到目标操作数中
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/m32OR 目标操作数源操作数
作用将源操作数与目标操作数或运算后将结果保存到目标操作数中
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/m32XOR 目标操作数源操作数
作用将源操作数与目标操作数异或运算后将结果保存到目标操作数中
NOT指令
NOT 的语法
NOT r/m8NOT r/m16NOT r/m32NOT 操作数作用取反
LEA指令
lea:Load Effective Address即装入有效地址的意思它的操作数就是地址
lea r32dword ptr ds:[内存编号(地址)]
将内存地址赋值给32位通用寄存器
lea是传址mov是传值注意区别
堆栈结构 Windows分配栈时 是从高地址往低地址分配:
MOV EBX,0x13FFDC BASEMOV 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:[EDX4] 先取出数据再修改栈顶 取出数据
MOV EAX,DOWRD PTR DS:[EDX] 修改栈顶
ADD EDX,4 先修改栈顶再取出数据 修改栈顶
LEA EDX,DWORD PTR DS:[EDX4] 取出数据
MOV EAX,DOWRD PTR DS:[EDX-4] 入栈和出栈操作也有对应的指令
上面我们自己模拟的两个用作栈顶和栈底的寄存器在WINDOWS中分别对应ESP和EBP
并且前面我们自己模拟的入栈和出栈操作也有对应的指令PUSH 和 POP
就是封装了压入数据和修改栈顶的操作
PUSH 和 POPpush 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,0xFFADD AL,10x800x40
加黑的为最高位
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,10x803: 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,55EEFFFFADD EAX,216位例
MOV AX,5EFEADD AX,28位例
MOV AL,4EADD 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,8AL的数据宽度为8即
无符号数范围为0FF即0255
8816在0~255内 不溢出
有符号数的范围为
正数07F 即0127
负数80FF 即 -1280
8816 在0~127内 两正数相加结果仍为正数不溢出
无符号溢出、有符号不溢出例
MOV AL,0FF
ADD AL,2无符号数时
FF22552257 在0~255外溢出
有符号数时
FF2-121
正 负 永远都不会有溢出
无符号不溢出、有符号溢出例
MOV AL,7F
ADD AL,2无符号数时
7F21272129 在0255内 不溢出
有符号数时
7F20x81在80FF (负数范围)内两正数相加结果为负数溢出
无符号、有符号都溢出
MOV AL,0FE
ADD AL,80无符号数时
FE225422560x100 在0255外 溢出
有符号数时
FE20x100在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就是运算0x800xc0
0x80:1 0 00 0000
0xC0:1 1 00 0000
符号位11有产生进位于是符号位有进位为1
最高有效数值位向符号位产生的进位01没有产生进位于是最高有效数值位向符号位产生的进位为0
OF 符号位有进位 xor 最高有效数值位向符号位产生的进位
OF 1 xor 0 1 所以此时OF1
方向标志DF(Direction Flag)
DF方向标志位
DF1时串操作为减地址方式 DF0为增地址方式
下面的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原本123但是现在变成了4注意与ADD的区别就在于进位
SBB指令带借位减法
格式SBB R/M,R/M/IMM 两边不能同时为内存 数据宽度要一样
MOV AL,4
MOV CL,2
手动修改CF为1
SBB AL,CL计算结果为1原本4-22但是现在变成了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执行前AL1 CL2
执行后AL2 CL1
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
为什么执行完是加而不是减?
由DFDirection 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为十六进制也就是0x1016
代码将会重复执行16次会不会往同一个地方覆盖
不会因为每执行一次EDI和ESI都会变化4变化方向由DF决定
汇编跳转和比较指令
JCC指令JMP指令CALL指令RET指令CMP指令TEST指令JCC指令表
JCC指令
cc 代表 condition code(状态码)
Jcc不是单个指令,它只是描述了跳转之前检查条件代码的跳转助记符
例如JNE在跳转之前检查条件代码
典型的情况是进行比较(设置CC)然后使用跳转助记符之一
CMP EAX,0JNE XXXXX条件代码也可以用AND、OR、XOR、加法、减法(当然也可以是CMP)等指令来设置
JCC指令用于改变EIPCPU要读取的指令地址
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,100MOV EBX,200CMP EAX,ECXCMP 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
只有当eax0时 eax and eax才会是0
所以
TEST EAX,EAX观察ZF零标志位就可以判断EAX是否为0
堆栈结构图重点
调用CALL又可以分为六个部分提升堆栈保护现场初始化提升的堆栈执行实际内容恢复现场返回 提升堆栈 对应语句为
00401040 / \55 push ebp00401041 |. 8BEC mov ebp,esp00401043 |. 83EC 40 sub esp,0x40将堆栈提升了0x40
保护现场 对应语句为
00401046 |. 53 push ebx00401047 |. 56 push esi00401048 |. 57 push edi将ebx、esi、edi三个通用寄存器保存到堆栈中前面的push ebp其实也属于保护现场
初始化提升的堆栈
00401049 |. 8D7D C0 lea edi,dword ptr ss:[ebp-0x40]0040104C |. B9 10000000 mov ecx,0x1000401051 |. B8 CCCCCCCC mov eax,0xCCCCCCCC00401056 |. F3:AB rep stos dword ptr es:[edi]这里将我们提升的堆栈中的内容全部初始化为CCCCCCCC
为什么是初始化为CC防止缓冲溢出
CC的硬编码对应的指令为int 3即断点
这么做有什么好处呢当程序执行超过缓冲区时遇到int 3就会自动停下来
执行实际的内容 对应语句为
00401058 |. 8B45 08 mov eax,dword ptr ss:[ebp0x8]0040105B |. 0345 0C add eax,dword ptr ss:[ebp0xC]就是将前面压入的参数2和1进行相加得到3
恢复现场 对应语句为
0040105E |. 5F pop edi ; HelloWor.004011710040105F |. 5E pop esi ; HelloWor.0040117100401060 |. 5B pop ebx ; HelloWor.0040117100401061 |. 8BE5 mov esp,ebp00401063 |. 5D pop ebp ; HelloWor.00401171与前面保护现场相对应
返回 对应语句为
00401064 \. C3 retnCALL返回后 对应语句为
00401171 |. 83C4 08 add esp,0x8作用为平衡堆栈
逆推C语言代码
根据我们前面的分析我们不难发现这其实就是个简单的加法函数
int add(int x,int y){xxy; //这里的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 ediPS前面的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,espsub esp,0x40//保护现场push ebxpush esipush edi//初始化提升的堆栈填充缓冲区mov eax,0xCCCCCCCCmov ecx,0x10lea edi,dword ptr ds:[ebp-0x40]rep stosd//函数核心功能//取出参数mov eax,dword ptr ds:[ebp8]//参数相加add eax,dword ptr ds:[ebp0xC]//恢复现场pop edipop esipop ebx//降低堆栈mov esp,ebppop 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 a0xFF;short b0xFF;int c0xFF;long d0xFF;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 global0x610;
int main(int argc, char* argv[])
{//局部变量int temp0x160;globalglobaltemp;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 i0xFF;printf(%d\n,i);int ji1;ii1;printf(%d\n,i);printf(%d\n,j);return 0;
}汇编嵌套if else #include stdafx.hint result0;
int getMax(int i,int j,int k){if(ij){if(ik){return i;}else{return k;}}else{if(jk){return j;}else{return k;}}
}int main(int argc, char* argv[])
{resultgetMax(1,2,3);printf(%d\n,result);resultgetMax(1,3,2);printf(%d\n,result);resultgetMax(2,1,3);printf(%d\n,result);resultgetMax(2,3,1);printf(%d\n,result);resultgetMax(3,1,2);printf(%d\n,result);resultgetMax(3,2,1);printf(%d\n,result);return 0;
}#include stdafx.h
int getMax2(int i,int j){if(ij){return i;}else{return j;}
}
int main(int argc, char* argv[])
{getMax2(1,2);return 0;
}汇编比较三种循环 1.
#include stdafx.h
int loop1(){int i0,j0;for(i0;i10;i){jji;}return j;
}int loop2(){int i0,j0;while(i10){jji;ii1;}return j;
}
int loop3(){int i0,j0;do {jji;ii1;} while(i10);return j;
}int main(int argc, char* argv[])
{int result0;resultloop1();printf(%d\n,result);resultloop2();printf(%d\n,result);resultloop3();printf(%d\n,result);return 0;
}汇编一维数组
数组的寻址 数组的存储并不复杂接下来看看如何来找到数组的某个成员
#include stdafx.h
void function(){int x1;int y2;int r0;int arr[5]{1,2,3,4,5};rarr[1];rarr[x];rarr[xy];rarr[x*2y];
}
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 i1;ii*5;ii*6;ii*7;int i1;ii*4;ii*8;ii*16;int arr[3][4]{{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int aarr[2][3];
int i1,j2;
int barr[i][j];
int carr[ij][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.hp100;player.mp50;player.money1000;player.atk10; strcpy(player.name,lyl610abc);player.x600;player.y100;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;playerretStruct();return 0;
}内存对齐
1.内存对齐也称作字节对齐 2.前面或多或少都有提到过内存对齐但没有具体展开现在来谈谈内存对齐 3.为什么要内存对齐
性能原因
寻址时提高效率采用了以空间换时间的思想
当寻址的内存的单位和本机宽度一致时寻址的效率最高举个例子
在32位的机器上一次读32位4字节的内存 效率最高
在64位的机器上一次读64位8字节的内存 效率最高汇编switch比较if else #include stdafx.hvoid 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;a610;a(int)610;
}void function(){ char* a;short* b;int* c;a(char*) 1;b (short*) 2;c(int*) 3;
}#include stdafx.hvoid 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 40x0004 func1 ??这里的??表示长度未知实际目标文件的各个数据可能不是连续的也不一定从0x0000开始。比如这里的func1可能是这样
0x0004 inc DWORD PTR[0x0000]0x00?? ret这里把gVal翻译为inc语句也就是把本单元的0x0000地址的一个DWORD4字节进行加一。
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。
内部链接内部链接的符号不能在别的编译单元中使用。但不同的编译单元可以拥有同样的名称的符号