信息的存储:一切信息在电脑中的存储方式都是用二进制存储。为了方便显示和,电脑将二进制数以十六进制数显示,八位二进制数为一字节,一字节是系统操作的基本单位,系统通常用两个十六进制数来表示一字节。
PE文件结构:pe文件结构是在Windows上运行的可执行文件必须遵守的格式。(程序从哪里开始,数据存在哪里,程序放在哪里)
十六进制的3c转换成十进制就是60,表示从六十以后四个字节就是三十二位信息的起始点,但并不是程序运行的起点。
依照顺序依次查找,最终定位到00000111,这个才是入口点。
计算机中的常用单位:八位二进制数为一字节(8 Bit),十六位为一字(word)(16 Bit)三十二位为双字(dword)(32 Bit)
Windows操作系统存储数据采用地位在前高位在后,所有看到一串二进制数,如:E8 00 00 00 实际上在Windows系统中它是000000E8
PE结构图
逻辑运算:在计算机中所有的运算都是采用逻辑位运算(与、或、异或、非、左移<<)
计算机计算:2+3=?
转换成二进制数
x:0010
y:0011
第一步异或运算
0010
xor 0011
--------------
0001 R:0001
判断是否运算结束,进行与运算
0010
and 0011
--------------
0010
对与运算结果进行移位操作后再判断是否运算完毕
0010 << 1 == 0100 (左移一位)
如果左移结果等于零,那么这个R就是运算结果
如果左移结果不等于零,则再进行异或运算
将上次异或运算出的结果作为新的运算值
x:0001
将左移得到的结果也作为新的运算值
y:0100
再进行异或运算
0001
xor 0100
--------------
0101 R:0101
同样进行与运算
0001
and 0100
--------------
0000
0000 << 1 == 0000
左移结果为0000,所以运算结果就是 R:0101 ---> 十进制:5
逻辑运算还可以用来数据加密
客户端发送数据:2015
加密密钥:54
20 15
20: 0010 0000
54: 0101 0100
xor: 0111 0100 对 2 0 进行加密后得 7 4
再对15进行加密
15: 0001 0101
54: 0101 0100
xor: 0100 0001 加密后传出的数据就是 4 1
服务器端接收到的是:7441
服务器端再利用密钥进行解密
服务端:7441
密钥:54
74: 0111 0100
54: 0101 0100
xor: 0010 0000 20
41: 0100 0001
54: 0101 0100
xor: 0001 0101 15
最后得到的就是2015
存储数据的地方:寄存器与内存
寄存器是再CPU中的用于存储数据的,但是因为CPU主要用于运算,所以寄存器的存储量很小,但是读取速度很快,所以一般是CPU将要处理的数据或程序放入到寄存器中。内存相比起寄存器,存储量更大,对应的读取速度会慢一点,也没那么贵。
32位通用寄存器的指定用途:
EAX 累加器
ECX 计数
EDX I/O指针
EBX DS段的数据指针
ESP 堆栈指针
EBP SS段的数据指针
ESI 字符串操作的原指针;SS段的数据指针
EDI 字符串操作的目标指针;ES段的数据指针
通用寄存器的使用(EAX/ECX/EDX/EBX)
汇编指令 MOV/ADD/SUB
MOV EAX, 12345678
指令 目标 数据(这个数称为立即数)
MOV:移动
将12345678这个立即数放到EXA这个寄存器中
ADD EAX,1
ADD:相加
把1与EXA中的数相加(12345678+1)
MOV ECX,2
将2放入到ECX中
ADD EAX,ECX
把ECX中的数与EAX的数相加(不止是单纯的数字加,也可以不同区域的寄存器相加,相移)
SUB EAX,3
SUB:相减
将EAX中的数减3
内存
内存单元宽度为8,能存储8位二进制数,也就是一个内存单元一字节
以WindowsXP-32位系统为例:
内存的地址编号从:0x00000000~0xFFFFFFFF
八个十六进制地址位,也就是说一个地址就是32位的地址位(4*8=32)
这个地址位也就是它的地址总线数,地址总线数为32
内存大小:
0~F,十六
总地址数:0x100000000(因为有个0x00000000)转换成十进制后:4294957296
一个地址就是一个地址单元,大小位一个字节
进行换算:4294957296/1024 = 4194304 Kb
继续换算:4194304/1024 = 4096 Mb
继续换算:4096/1024 = 4 Gb
所以32位的操作系统最大能识别的内存就是4Gb
内存的读写
寻址公式一:[立即数]直接写入,读出数据
读取内存的值:
MOV EAX,DWORD PTR DS:[0X13FFC4]
意思:从内存0X13FFC4中读取一个32bit的数据放到前面EAX这个寄存器中
DWOR代表大小
PTR是固定写法
DS是段寄存器
如果操作的是数就输DS:;
如果操作的是EBP、ESI(寄存器)就输SS:;
如果操作的是EDI就输ES:
MOV
可以在ESP的地址左右练手,防止32位的系统没有该地址
将数据写入内存:
MOV DWORD PTR DS:[0X13FFC4],0x87654321
意思:将后面的十六进制数存入到前面的地址中
取出该地址
LEA ECX,DWORD PTR DS:[0X13FFC4]
LEA EAX,DWORD PTR DS:[ESP+8]
意思:把这个地址编号给到ECX寄存器中
寻址公式二:[reg]reg代表寄存器可以是8个通用寄存器中的任意一个
读取内存的值:
MOV ECX,0X13FFD0
MOV EAX,DWORD PTR DS:[ECX]
向内存中写入数据:
MOV EDX,0X13FFD8
MOV DWORD PTR DS:[EDX],0X87654321
获取内存编号:
LEA EAX,DWORD PTR DS:[EDX]
MOV EAX,DWORD PTR DS:[EDX]
寻址公式三:[reg+立即数]
读取内存的值:
MOV ECX,0X13FFD0
MOV EAX,DWORD PTR DS:[ECX+4]
向内存中写入数据:
MOV EDX,0X13FFD8
MOV DWORD PTR DS:[EDX+0XC],0X87654321
获取内存编号:
LEA EAX,DWORD PTR DS:[EDX+4]
MOV EAX,DWORD PTR DS:[EDX+4]
寻址公式四:[reg+reg*{1,2,4,8}]
读取内存的值:
MOV EAX,13FFC4
MOV ECX,2
MOV EDX,DWORD PTR DS:[EAX+ECX*4]
向内存中写入数据:
MOV EAX,13FFC4
MOV ECX,2
MOV DWORD PTR DS:[EAX+ECX*4],87654321
获取内存编号:
LEA EAX,DWORD PTR DS:[EAX+ECX*4]
寻址公式五:[reg+reg*{1,2,4,8}+立即数]
读取内存的值:
MOV EAX,13FFC4
MOV ECX,2
MOV EDX,DWORD PTR DS:[EAX+ECX*4+4]
向内存中写入数据:
MOV EAX,13FFC4
MOV ECX,2
MOV DWORD PTR DS:[EAX+ECX*4+4],87654321
获取内存编号:
LEA EAX,DWORD PTR DS:[EAX+ECX*4+4]
堆栈:
ESP存了栈顶的位置,EBP存了栈底的位置。
push向栈存入数据(入栈):push 0X12345678,pop从栈中取出数据(出栈):pop eax。栈底不变,栈顶变(栈的数据结构特点)。
push eax代码相当于:
lea esp,dword ptr ss:[esp-4]
mov dword ptr ss:[esp],eax
或者
mov dword ptr ss:[esp-4],eax
lea eso,dword ptr ss:[esp-4]
lea esp,dword ptr ss:[esp-4] == mov esp,esp-4 == sub esp,4
pop ecx 代码相当于:
mov ecx,dword ptr ss:[esp]
lea esp,dword ptr ss:[esp+4] == mov esp,esp+4 == add esp,4
或者
lea esp,dword ptr ss:[esp+4]
mov ecx,dword ptr ss:[esp-4]
EIP寄存器:cpu当前执行到的位置
CALL指令:将EIP的指针跳到目标地址,并将当前指令的下一行地址放入堆栈中便于执行完跳转后继续回来执行当下的指令
每个指令都对应了相应字节的十六进制机器码,所以系统才能通过当前地址加上指令的大小确定下一条指令的位置,CALL也才能将其放入堆栈中
画堆栈图,能更好的了解数据变化的过程
跳转之后,首先做的是Push之前的所以内容,保留现场,相当于中断。
局部变量是放在堆栈中的,使用完之后如果不回收清理,就会一直留在堆栈中。函数中声明的变量是局部变量。
定义的函数中设置了两个局部变量,这两个局部变量都存放在堆栈中,循环是从0开始,然后到10之后结束。这使得缓冲区溢出,[ESP+4]
进制的定义:由几个符号组成,逢几进一。进制的本质是任意符号去表示数据,从一个角度理解算是一种加密。
二进制:0~100:
0 1 10 11 100 101 110 111 1000 1001 1010 1011 1100 1101 1110 1111
10000 10001 10010 10011 10100 10101 10110 10111 11000 11001 11010 11011 11100 11101 11110 11111 100000 100001 100010 100011 100100 100101 100110 100111 101000 101001 101010 101011 101100 101101 101110 101111 110000 110001 110010 110011 110100 110101 110110 110111 111000 111001 111010 111011 111100 111101 111110 111111 1000000 1000001 1000010 1000011 1000100 1000101 1000110 1000111 1001000 1001001 1001010 1001011 1001100 1001101 1001110 1001111 1010000 1010001 1010010 1010011 1010100 1010101 1010110 1010111 1011000 1011001 1011010 1011011 1011100 1011101 1011110 1011111 1100000 1100001 1100010 1100011 1100100
进制的换算可用权重去计算。重点在二进制和十六进制的转换。进制的运算重在查表:如八进制,先把八进制数都写出来列成一张表。然后2+2相当于2之后两位,2*2相当于2+2
在计算机中,由于受硬件的制约,数据都是有长度限制的(称为数据宽度),超过最大宽度的数据就会被丢弃。
逻辑运算:CPU进行运算的本质。或:or |;与: and &;异或: xor ^;非:not !;
逻辑运算的具体运用:
CPU计算2+3 (从电路的角度)
x:0010
y:0011
第一步异或运算
0010
xor 0011
--------------
0001 R:0001
判断是否运算结束,进行与运算
0010
and 0011
--------------
0010
对与运算结果进行移位操作后再判断是否运算完毕
0010 << 1 == 0100 (左移一位)
如果左移结果等于零,那么这个R就是运算结果
如果左移结果不等于零,则再进行异或运算
将上次异或运算出的结果作为新的运算值
x:0001
将左移得到的结果也作为新的运算值
y:0100
再进行异或运算
0001
xor 0100
--------------
0101 R:0101
同样进行与运算
0001
and 0100
--------------
0000
0000 << 1 == 0000
左移结果为0000,所以运算结果就是 R:0101 ---> 十进制:5
最简单的加解密:
客户端发送数据:2015
加密密钥:54
20 15
20: 0010 0000
54: 0101 0100
xor: 0111 0100 对 2 0 进行加密后得 7 4
再对15进行加密
15: 0001 0101
54: 0101 0100
xor: 0100 0001 加密后传出的数据就是 4 1
服务器端接收到的是:7441
服务器端再利用密钥进行解密
服务端:7441
密钥:54
74: 0111 0100
54: 0101 0100
xor: 0010 0000 20
41: 0100 0001
54: 0101 0100
xor: 0001 0101 15
最后得到的就是2015
小结:计算机中数的存储(二进制与十六进制),计算机数的运算(逻辑)
寄存器 | 编号(二进制) | 编号(十进制) | ||
---|---|---|---|---|
32位 | 16位 | 8位 | ||
EAX | AX | AL | 000 | 0 |
ECX | CX | CL | 001 | 1 |
EDX | DX | DL | 010 | 2 |
EBX | BX | BL | 011 | 3 |
ESP | SP | AH | 100 | 4 |
EBP | BP | CH | 101 | 5 |
ESI | SI | DH | 110 | 6 |
EDI | DI | BH | 111 | 7 |
寄存器结构理解:
不同位数的寄存器方便存储大小不同的数据,这样能提高利用效率,最后分为高八位和低八位。
代码演示:立即数
EAX 00000000
MOV EAX,0xAAAAAAAA
>>> EAX AAAAAAAA
MOV AX,0xBBBB
>>> EAX AAAABBBB
MOV AH,0xDD
>>> EAX AAAADDBB
MOV AL,0xEE
>>> EAX AAAADDEE
操作数
ECX 00000000
MOV ECX,EAX
>>> ECX AAAADDEE
MOV 的语法:
1.MOV r/m8,r8
2.MOV r/m16,r16
3.MOV r/m32,r32
4.MOV r8,r/m8
5.MOV r16,r/m16
6.MOV r32,r/m32
7.MOV r8,imm8
8.MOV r16,imm16
9.MOV r32,imm32
r: 通用寄存器
m: 代表内存
imm:代表立即数
r8: 代表8位通用寄存器
m8: 代表8位内存
imm8:代表8位立即数
MOV 目标操作数,源操作数
作用:拷贝源操作数到目标操作数1.源操作数可以是立即数、通用寄存器、段寄存器、或者内存单元
2.目标操作数可以是通用寄存器、段寄存器或者内存单元
3.操作数的宽度必须一样
4.源操作数和目标操作数不能同时为内存单元
!!!注意,在向某地址写入数据时要指定写入数据大小
数据运算:
ADD 的语法:
1.ADD AL,imm8
2.ADD AX,imm16
3.ADD EAX,imm32
4.ADD r/m8,imm8
5.ADD r/m16,imm16
6.ADD r/m32,imm32
7.ADD r/m8,r8
8.ADD r/m16,r16
9.ADD r/m32,r32
10.ADD r8,r/m8
11.ADD r16,r/m16
12.ADD r32,r/m32
SUB 的语法:
1.SUB AL,imm8
2.SUB AX,imm16
3.SUB EAX,imm32
4.SUB r/m8,imm8
5.SUB r/m16,imm16
6.SUB r/m32,imm32
7.SUB r/m8,r8
8.SUB r/m16,r16
9.SUB r/m32,r32
10.SUB r8,r/m8
11.SUB r16,r/m16
12.SUB r32,r/m32
逻辑运算:
AND 的语法:
1.AND AL,imm8
2.AND AX,imm16
3.AND EAX,imm32
4.AND r/m8,imm8
5.AND r/m16,imm16
6.AND r/m32,imm32
7.AND r/m8,r8
8.AND r/m16,r16
9.AND r/m32,r32
10.AND r8,r/m8
11.AND r16,r/m16
12.AND r32,r/m32
OR 的语法:
1.OR AL,imm8
2.OR AX,imm16
3.OR EAX,imm32
4.OR r/m8,imm8
5.OR r/m16,imm16
6.OR r/m32,imm32
7.OR r/m8,r8
8.OR r/m16,r16
9.OR r/m32,r32
10.OR r8,r/m8
11.OR r16,r/m16
12.OR r32,r/m32
XOR 的语法:
1.XOR AL,imm8
2.XOR AX,imm16
3.XOR EAX,imm32
4.XOR r/m8,imm8
5.XOR r/m16,imm16
6.XOR r/m32,imm32
7.XOR r/m8,r8
8.XOR r/m16,r16
9.XOR r/m32,r32
10.XOR r8,r/m8
11.XOR r16,r/m16
12.XOR r32,r/m32
NOT 的语法:
NOT r/m8
NOT r/m16
NOT r/m32
寄存器与内存的区别:
常规来说,32位的CPU只能接收4GB的寻址编号,但是可以通过段地址+偏移地址的方法让能够寻址的寻址编号更多。
在堆栈区是每四个字节一个地址编号,堆栈中显示出来的是8位16进制数每2位就是一个字节,而内存中每一个地址编号就是一个字节。
mov r,m:相当于把该地址给到了寄存器
mov r,dword ptr ds:[r/m]:这才是将某地址中的值给到寄存器
lea r,dword ptr ds:[r/m]:将目标地址给到寄存器
数据压栈方式:
MOV EBX,0x13FFDC ----> BASE
MOV EDX,0x13FFDC ----> TOP
方式一:
MOV DWORD PTR DS:[EDX-4],0xAAAAAAAA
#这一步是先把元素入栈
SUB EDX,4
#在调整栈顶指针位置
#Windows系统的特点是堆栈由大到小,所以-4是栈顶上升入栈的过程
方式二:
SUB EDX,4
#先移动栈顶指针位置
MOV DWORD PTR DS:[EDX],0xBBBBBBBB
#再向当前栈顶指针中放入元素
方式三:
MOV DWORD PTR DS:[EDX-4],0xCCCCCCCC
LEA EDX,DWORD PTR DS:[EDX-4]
#这里采用LEA这个方式直接获取下一个堆栈的地址
方式四:
LEA EDX,DWORD PTR DS:[EDX-4]
MOV DWORD PTR DS:[EDX-4],0xDDDDDDDD
#反过来同理
#入栈(压栈)的原理就是通过栈顶指针找到具体位置,将目标操作数放入栈顶地址,放入后将指针移到当前元素所在位置。
#放入元素、移动指针,两个步骤可交换
读取第N个数
方式一:
#通过BASE加偏移来读取
>>>读第一个压入数据
MOV ESI,DWORD PTR DS:[EBX-4]
>>>读取第四个压入的数据
MOV ESI,DWORD PTR DS:[EBX-0x10]
方式二:
#通过Top加偏移来读取
>>>读取第二个压入数据
MOV EDI,DWORD PTR DS:[EDX+4]
>>>读第三个压入数据
MOV EDI,DWORD PTR DS:[EDX+8]
弹出数据
方式一:
MOV ECX,DWORD PTR DS:[EDX]
方式二:
MOV ESI,DWORD PTR DS:[EDX]
ADD EDX,4
方式三:
LEA EDX,DWORD PTR DS:[EDX+4]
MOV EDI,DWORD PTR DS:[EDX-4]
读取与弹出的不同就在于栈顶指针是否有改变。
PUSH指令:
1,PUSH r32
2,PUSH r16
3,PUSH m16
4,PUSH m32
5,PUSH imm8/imm16/imm32
#将寄存器中的值、某地址的值、立即数压入栈顶
POP指令:
1,POP r32
2,POP r16
3,POP m16
4,POP m32
#将栈顶的值放入寄存器、某地址
PUSHAD指令、POPAD指令
PUSHAD 用来保存通用寄存器当时存储的值,保存现场
POPAD 用来还原存储的寄存器的值,还原现场
标志寄存器中各位代表了不同的消息,EFL 202(0010 0000 0010)一一对位去分析
1、进位标志CF(Carry Flag):如果运算结果的最高位产生了一个进位或借位,那么,其值为1,否则其值为0。
这里的最高位是 宽度+1。
1000 0000 - 0100 0000 = 0100 0000 这里CF的值是0
1000 0000 - 1000 0001 = 1111 1111 这里CF的值是1
MOV AL,0xEF
ADD AL,2
>>>PC--->0
MOV AL,0xFE
ADD AL,2
>>>PC--->1
2、奇偶标志PF(Parity Flag):奇偶标志PE用于反映运算结果中“1”的个数的奇偶性。
看最低有效字节,如果“1”的个数为偶数,则PE的值为1,否则其值为0.
MOV AL,3
>>>PE--->2
ADD AL,3
>>>PE--->2
ADDAL,2
>>>PE--->1
#看运算完后结果的二进制数中一的个数
3、辅助进位标志AF(Auxiliary Carry Flag)
看低四位、八位、十六位是否会有向高位进位,有进位才会将AF置为1,其他的都没有变化
MOV EAX,0x55EEFFFF
ADD EAX,2
>>>AF--->1
MOV AX,5EFE
ADD AX,2
>>>AF--->1
MOV AL,4E
ADD AL,2
>>>AF--->1
4、零标志ZF(Zero Flag):零标志ZF用来反映运算结果是否为0.
如果运算结果为0,则其值为1,否则其值为0.再判断运算结果是否为0时,可使用此标志位。
XOR EAX,EAX
>>>ZF--->0
#该条命令用来把EAX清零
MOV EAX,2
SUB EAX,2
>>>ZF--->0
5、符号标志SF(Sign Flag):符号标志SF用来反映运算结果的符号位,它与运算结果的最高位相同。
MOV AL,7F(0111 1111)
>>>SF--->0
ADD AL,2(1000 0001)
>>>SF--->1
6、溢出标志OF(Overflow Flag):溢出标志OF用于反映有符号数加减运算所得结果是否溢出。
如果运算结果超过当前运算位数所能表示的范围,则称为溢出,OF的值被置为1,否则,OF的值被清为0。
C位溢出是无符号位溢出,O位溢出是有符号位的溢出
溢出主要是给有符号运算使用的,再有符号的运算中,有如下的规律:
#正+正=正 如果及结果是负数,则说明有溢出
#负+负=负 如果结果是正数,则说明有溢出
#正+负 永远都不会有溢出
MOV AL,8(0000 1000)
ADD AL,8(0000 1000)
>>>OF--->0(0001 0000)
MOV AL,FF(1111 1111)
ADD AL,2(0010)
>>>OF--->(无符号就溢出了,有符号就是:负+正。负数+正数永远都不会溢出)
MOV AL,7F(0111 1111)
ADD AL,2(0010)
>>>OF--->(无符号位就可以进位,有符号位就会溢出)
MOV AL,FF(1111 1111)
ADD AL,80(1000 0000)
>>>OF--->(最高位都会有溢出。负数相加为正,则溢出)
ADC指令:带进位加法
格式:ADC R/M,R/M/IMM 两边不能同时为内存 宽度要一样
ADC AL,CL
ADC BYTE PTR DS:[12FFC4],2
ADC BYTE PTR DS:[12FFC4],AL
ADD 是求两个指定整数的和,而 ADC 除了两个指定整数以外,还会加上 C(进位)状态的值。需要 ADC 指令,是因为如果要加的整数长于微处理器每次能加的位元数,就要分开来加,高位字节的结果取决于低位字节相加时有没有进位。
举例:假如有8位元微处理器每次只能加一个字节,
如果我们要加两个 16 位元整数:00110101 11001010 + 00010100 01111101
先用 ADD 加 11001010 和 01111101,得 01000111,
有进位,状态 C 设为 1 再用 ADC 加 00110101 和 00010100 和 状态C(现在是1),
得 01001010 所以和是 01001010 01000111
SBB指令:带借位减法
格式:SBB R/M,R/M/IMM 两边不能同时为内存 宽度要一样
SBB AL,CL
SBB BYTE PTR DS:[12FFC4],2
SBB BYTE PTR DS:[12FFC4],AL
XCHG指令:交换数据
格式:XCHG R/M,R/M/IMM 两边不能同时为内存 宽度要一样
XCHG AL,CL
XCHG DWORD PTR DS:[12FFC4],EAX
XCHG BTYE PTR DS:[12FFC4],AL
两者交换数据的宽度要一样。
MOVS指令:移动数据 内存与内存之间
在正向中很可能是字符串的复制
BYTE/WORD/DWORD
MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI] 简写为:MOVSB
MOVS WORD PTR ES:[EDI],BYTE PTR DS:[ESI] 简写为:MOVSW
MOVS DWORD PTR ES:[EDI],BYTE PTR DS:[ESI] 简写为:MOVSD
方向标志DF(Direction Flag):决定了ESI和EDI在MOVS完了之后的增长方向。
DF为零就加,为一就减。加减多少取决于移动数的大小。
STOS指令:将AL/AX/EAX的值存储到[EDI]指定的内存单元
STOS BYTE PTR ES:[EDI] 简写为:STOSB
STOS WORD PTR ES:[EDI] 简写为:STOSW
STOS DWORD PTR ES:[EDI] 简写为:STOSD
它执行后EDI的值的移动方向也是由DF决定。同样DF为零就加,为一就减
REP指令:按计数寄存器(ECX)中指定的次数重复执行字符串指令
MOV ECX,10
REP MOVSD
REP STOSD
jmp指令:修改EIP寄存器中指针指向下一条指令的位置(修改IP指针,使CPU执行对应指令)。
如果跳转位置离当前指向位置小于128个字节,就会有一个SHORT
JUM只修改EIP的值,寄存器和堆栈都不会发生改变
MOV EIP,寄存器/立即数
简写为:
JMP 寄存器/立即数
call指令:修改EIP指向,和jmp功能一样
call与jmp的区别:call会将一个返回地址压入堆栈中。
返回地址:call指令计算出给指令的字节数后,在当前位置+当前指令的字节数=返回地址
PUSH 地址B
MOV EIP,地址A/寄存器
简写为:
CALL 地址A/寄存器
ret指令:与call指令成对出现,到达返回地址
退栈操作
LEA ESP,[ESP+4]
MOV EIP,[ESP-4]
简写为:
RET
cmp指令:比较两个操作数
实际上相当于SUB指令,但是相减的结构并不保存到第一个操作数中。
只是根据相减的结果来改变零标志位,当两个操作数相等的时候,零标志位置1;奇偶标志位也置一。
MOV EAX,100
MOV ECX,100
CMP EAX,ECX
Z标志位:1
>>>表示两个数相等
MOV EAX,100
MOV ECX,200
CMP EAX,ECX
S标志位:1
>>>表示第一个数比第二个数小
CMP AX,WORD PTR DS:[405000]
CMP AL,BYTE PTR DS:[405000]
CMP EAX,DWORD PTR DS:[405000]
除了可以比较寄存器之间值的大小,还可以比较内存和寄存器之间的大小。但是注意要位数相同。
test指令:也是对两个操作数进行检测,但是时对两个数值进行与操作,结果不保存,只改变相应标志位。
常用于确定某寄存器是否等于0.
TEST EAX,EAX
z标志位:1
>>>表示EAX的值为零
一些小指令:
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 | (null) |
JNP/JPO | 若1出现次数为奇数则跳转 | jump if not parity (odd) | PF=0 | (null) |
JO | 若溢出则跳转 | jump if overflow | OF=1 | (null) |
JNO | 若无溢出则跳转 | jump if not overflow | OF=0 | (null) |
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 | 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) |
堆栈图:
堆栈图分析步骤:
第一步:观察寄存器(ESP、EBP、EIP)确定运行前的堆栈位置。画出初始堆栈图。
第二步:单步调试,观察栈顶与栈底的变化,以及是否有值入栈或出栈。
第三步:重复第一步,第二步。
函数:
计算机的函数,是一个固定的一个程序段,或称其为一个子程序,它在可以实现固定运算功能的同时还带有一入口和一个出口,所谓的入口,就是函数所带的各个参数,我们可以通过这个入口,把函数的参数值代入子程序,供计算机处理,所谓出口,就是指函数的计算结果,也称为返回值,在计算机求得之后,由此口带回给调用它的程序。
Windows堆栈:特点: 1、先进后出;2、向低位地址扩展
堆栈平衡:windows中的堆栈,是一块普通的内存,主要用来存储一些临时的数据和参数等。可以把windows中的堆栈想象成是一个公用的书箱,函数就像是使用箱子的人。函数在执行的时候,会用到这个书箱,把一些数据存到里面,但用完的时候一定要记得把书拿走,否则会乱的,也就是说,你放进去几本书,走的时候也要拿走几本书,这个就是堆栈平衡.
VC++6.0挺好的,但是太旧了,版本不兼容问题有点麻烦,即使克服了版本不兼容成功安好了,使用起来也会不太顺手。Microsoft Visual C++ 2010 Express挺好的,也顺手,相应的功能都有,快捷键也差不多。强行用了一段时间VC++6.0后的感受。
快捷键:
C语言函数的规则:
裸函数:
返回类型、函数名、参数都可以设置
#include "stdio.h"
void _declspec(naked) luoluo ()
{
}
void Plus ()
{
}
int main ()
{
luoluo();
Plus();
return 0;
}
从上面两个定义的裸函数与普通函数就可以看出两者的不同之处。(从反汇编角度来讲,裸函数需要直接去编写所有东西,普通函数定义系统会帮你做好基础指令)
在c语言中写汇编代码:____asm{ret}(两个下划线)
//C代码转汇编代码
//练习一:
int plus(int x, int y){
return x+y;
}
//练习二:
int plus(){
int a=2;
int b=3;
int c=4;
return a+b+c;
}
//练习三:
int plus(int x, int y, int z){
int a=2;
int b=3;
int c=4;
return x+y+z+a+b+c;
}
常见的调用约定:
调用约定 | 参数压栈顺序 | 平衡堆栈 |
---|---|---|
____cdecl | 从右至左入栈 | 调用者清理栈(外平衡) |
____stdcall | 从右至左入栈 | 自身清理堆栈(内平衡) |
____fastcall | ECX/EDX传送前两个,剩下的从右至左入栈 | 自身清理堆栈 |
默认使用的是____cdecl这种方式进行,在调用完函数后,在函数外进行堆栈的清理(参数去除)。
____srdcall与____cdecl的区别就在于它是在函数内部就完成了堆栈的清理。
____fastcall的特点就是快,快的原因是它将参数暂放到寄存器中,这使得读取数据的速度更快效率也更快。但是它只能将两个参数放入寄存器中,如果参数个数更多,那多出的参数还是会放到堆栈中,这样效率也没多高了。如果就两个或一个参数那就不用考虑清理堆栈消除参数的问题,毕竟是放在寄存器当中的也管不着。如果是多出且放在堆栈中的参数,那就进行自身清理堆栈(函数内部解决)。
通过 ret 8 堆栈平衡来思考参数个数,其结果并不准确。最好还是进入函数内部去仔细分析。
函数:
带参数的函数:
参数会在进入函数前就把值压栈。而且是一个外平栈。
plus1(1,2);
00A914DE push 2
00A914E0 push 1
00A914E2 call @ILT+115(_plus1) (0A91078h)
00A914E7 add esp,8
估计是C语言编译器的特点,它会把所有的调用函数都放在一起,call语句不是直接跳转到目标函数,像是寄存器间接寻址的方式,先跳转到一个jmp语句再通过jmp语句正式跳转到函数体。
可以看的出来定义的函数都有一个基本的模板。
#这里是一个带参的简单函数的开头:
int plus1(int x, int y){
//保存栈底
00A913C0 push ebp
//提升堆栈
00A913C1 mov ebp,esp
00A913C3 sub esp,0C0h
//保存现场
00A913C9 push ebx
00A913CA push esi
00A913CB push edi
00A913CC lea edi,[ebp-0C0h]
//填充堆栈
00A913D2 mov ecx,30h
00A913D7 mov eax,0CCCCCCCCh
00A913DC rep stos dword ptr es:[edi]
#再看一个空函数的反汇编:
void Plus ()
{
00B013D0 push ebp
00B013D1 mov ebp,esp
00B013D3 sub esp,0C0h
00B013D9 push ebx
00B013DA push esi
00B013DB push edi
00B013DC lea edi,[ebp-0C0h]
00B013E2 mov ecx,30h
00B013E7 mov eax,0CCCCCCCCh
00B013EC rep stos dword ptr es:[edi]
一摸一样。
#其实我们定义主函数的时候也是一样的
int main ()
{
00A914C0 push ebp
00A914C1 mov ebp,esp
00A914C3 sub esp,0C0h
00A914C9 push ebx
00A914CA push esi
00A914CB push edi
00A914CC lea edi,[ebp-0C0h]
00A914D2 mov ecx,30h
00A914D7 mov eax,0CCCCCCCCh
00A914DC rep stos dword ptr es:[edi]
函数plus1是的功能就是return x+y;它显示的反汇编代码:
return x+y;
00A913DE mov eax,dword ptr [x]
00A913E1 add eax,dword ptr [y]
#这里的[x]和[y]的地址就是之前入栈的参数1和参数2。于是结果就放到了eax这个寄存器中了。
函数的结尾也都是一样的。
//恢复现场
00B013EE pop edi
00B013EF pop esi
00B013F0 pop ebx
//恢复堆栈
00B013F1 mov esp,ebp
00B013F3 pop ebp
00B013F4 ret
无参有局部变量的函数:
这个就很简洁了,在主函数中,只有一个call语句去调用函数,应该是没有参数的原因,它并没有参数平衡这一步。
plus2();
00A914EA call @ILT+120(_plus2) (0A9107Dh)
#除了操作部分的反汇编代码不一样,其他都是一样的
int plus2(){
00A91400 push ebp
00A91401 mov ebp,esp
00A91403 sub esp,0E4h
00A91409 push ebx
00A9140A push esi
00A9140B push edi
00A9140C lea edi,[ebp-0E4h]
00A91412 mov ecx,39h
00A91417 mov eax,0CCCCCCCCh
00A9141C rep stos dword ptr es:[edi]
int a=2;
00A9141E mov dword ptr [a],2
int b=3;
00A91425 mov dword ptr [b],3
int c=4;
00A9142C mov dword ptr [c],4
return a+b+c;
00A91433 mov eax,dword ptr [a]
00A91436 add eax,dword ptr [b]
00A91439 add eax,dword ptr [c]
}
00A9143C pop edi
00A9143D pop esi
00A9143E pop ebx
00A9143F mov esp,ebp
00A91441 pop ebp
00A91442 ret
#有注意这几个局部变量的地址,发现就是上一个函数地址接着的。可是,,参数是放在ebp下面的呀(ebp+8这个位置开始的),局部变量是放在ebp上面的呀???所以地址怎么会一样呢?
既有参数又有局部变量的函数:
这个没什么特别的,还是那句话,除了操作部分,没什么差别。但是因为这个既有参数又有局部变量,所以可以看到两种数据的地址,他们的十六进制地址有一样的。个人暂时认为局部变量和参数被放在了两个不同的寄存器中。
数据类型的三个要素:
整数类型:char,short,int,long
类型 | 位数 | 大小 | 对应宽度 |
---|---|---|---|
char | 8bit | 1字节 | byte |
short | 16bit | 2字节 | word |
int | 32bit | 4字节 | dword |
long | 32bit | 4字节 | dword |
整数类型又分为有符号型(signed)和无符号型(unsigned)
两者间在计算机的存储上是没有差别的,唯一的区别在于读取时将其视为什么数。
符号型的应用:类型转换、比较大小、数学运算。
浮点数的存储:
浮点数存储步骤:
在VC++ 6.0里面查看,浮点数是十六进制显示。
英文字符存储:
ASCII编码:
七位ASCII码表:
由于标准ASCII码字符集字符数目有限,在实际应用中往往无法满足要求。为此,国际标淮化组织又将ASCII码字符集扩充为8位代码及ASCIl码的扩充。这样,ASCII码的字符集可以扩充128个字符,也就是使用8位扩展ASCII码能为256个字符提供编码这些扩充字符的编码均为高位为1的8位代码(即十进制数128一255),称为扩展ASCII码。扩展ASCII码所增加的字符包括加框文字、圆圈和其他图形符号。
计算机发明之处及后面很长一段时间,只用应用于美国及西方一些发达国家,ASCII能够很好满足用户的需求。但是当中国也有了计算机之后,为了显示中文,必须设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数字。
中国专家把那些127号之后的奇异符号们(即EASCII)取消掉,规定:一个小于127的字符的意义与原来相同,
但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节〕从OxA1用到OxFT,后面一个字节(低字节)从0xA1到oxFE,这样我们就可以组合出大约7000多个简体汉字了。
在这些编码里,还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
上述编码规则就是GB2312或GB2312-80
GB2312:
全局变量与局部变量:
在VC++6.0中,函数外定义的是全局变量,函数内部的是局部变量。在声明 了全局变量的情况下,可以直接在函数内部对全局变量进行修改。在函数内部定义一个与全局变量重名的变量,就不会影响全局变量。
#include "stdafx.h"
int x=10;
void Plus()
{
int x;
x=11;
printf("%d",x);
}
void Plus1()
{
printf("%d",x);
}
void Plus2()
{
x=12;
printf("%d",x);
}
int main()
{
Plus();
Plus1();
Plus2();
getchar();
return 0;
}
内存图:
操作 | 区域 |
---|---|
存放代码,可读可执行 | 代码区 |
存放参数,局部变量,临时数据(函数不执行的时候是没有堆栈的) | 堆栈 |
动态申请的,大小可变的。可读可写 | 堆 |
可读可写(int x) | 全局变量区 |
只读 | 常量区 |
代码不在堆栈区中,在代码区。函数分在代码区。函数调用的时候才会为其分配空间,用来存临时变量和参数。
全局变量的特点:
MOV寄存器, byte/word/ dword ptr ds: [ox12345678](通常直接给出的地址就是全局变量)
通过寄存器的宽度,或者byte/word/ dword来判断全局变量的宽度。
全局变量就是所谓的基址
局部变量的特点:
[ebp-4] [esp+4]
[ebp-8] [esp+8]
[ebp-0xC] [esp+0xC]
函数参数分析(判断函数有几个参数):
步骤一:观察调用处的代码
push 3
push 2
push 1
call 0040100f
#因为这里有三个值入栈,所以可以猜测有三个参数
步骤二:找到平衡堆栈的代码继续论证
call 0040100f
add esp,0Ch
或者函数内部
ret 4/8/0xC/0x10
最后两者综合,函数的参数个数基本确定。
但是如果调用约定如果是____fastcall,且参数是两个,那么它是通过寄存器存储参数,所以也无法判断。
参数个数-小结:
当在外部无法判断参数个数的时候,就进入函数内部进行判断。
公式一:寄存器 + ret 4 = 参数个数
公式二:寄存器 + [ebp+8] + [ebp+0x] = 参数个数
if语句逆向分析:
mov eax,dword ptr [ebp+8]
cmp eax,dword ptr [ebp+0Ch]
jle Function+29h(00401049)
if语句的特点:先改变标志寄存器,然后再执行JCC
函数内部功能分析步骤:
[ebp+8]:x
[ebp+0Ch]:y
[ebp-4]
[4bp-8]
mov dword ptr 004225c4,ecx
mov eax,dword ptr [ebp+8]
cmp eax,dword ptr [ebp+0Ch]
if begin:
先执行各类影响标志位的指令
然后jxx ok
else
mov eax,ecx
add eax,eax
jmp end
end
ok
特点分析:
跳转执行一部分代码,不跳转执行另一部分代码
if…else if…else if…else 判断的嵌套语句的分析:
if A:
影响标志位的指令
jxx B
mov ......
add ......
jmp out
else if B
影响标志位的指令
jxx C
mov ......
add ......
jmp out
else if C
影响标志位的指令
jxx D
mov ......
add ......
jmp out
else D
mov ......
add ......
out
特点分析:
如果判断为假就用jmp,如果判断为真就用jcc。相当于筛选,在筛选过程中如有不符,就直接跳出(out)。
//基于缓冲区溢出的HelloWord函数:
#include "stdio.h"
void HelloWord(){
printf("Hello World");
getchar();
}
void Fun(){
int arr[5] = {1,2,3,4,5};
arr[6] = (int)HelloWord;
}
int main(int argc,char *argv[]){
Fun();
return 0;
}
//这是可以成功运行的
//永不停止的HelloWorld
#include "stdio.h"
void Fun(){
int i;
int arr[5] = {0};
for(i=0;i<=5;i++){
arr[i] = 0;
printf("Hello World!");
}
}
int main(int argc,char *argv[]){
Fun();
return 0;
}
变量的声明:
向计算机寻求内存空间,计算机需要知道我们的需求。比如:我们想要的空间大小(宽度),我们想要放什么样的东西(种类),还有这个东西放在哪儿(作用域)。
声明变量就是告诉计算机,我要用一块内存,你给我留着,宽度和存储格式由数据类型决定.
计算机什么时候把这块内存给你,取决于变量的作用范围,如果是全局变量,在程序编译完成就已经分配了空间,如果是局部变量,只有在它所处的这块空间被调用时才会分配,否则是不分配的。
全局变量如果不赋初始值,默认是0,但是局部变量在使用前一定要赋初值。
类型转换:
MOVSX:有符号位的拓展
mov al,0x88
#0x88:1000 1000
movsx cx,al
#拓展后:1111 1111 1000 1000
mov al,0x77
#0x77:0111 0111
movsx cx,al
#拓展后:0000 0000 0111 0111
#如果符号位为1,那么拓展位全为1;反之符号位为0,那么拓展位为0.
MOVZX:无符号位的拓展
mov al,0x11
#0x11:0001 0001
movzx cx,al
#拓展后:0000 0000 0001 0001
mov al,0xff
#0xff:1111 1111
movzx cx,al
#拓展后:0000 0000 1111 1111
#无符号位拓展就是用0占拓展位
数据类型在转换的时候就是用的MOVSX和MOVZX,其中MOVSX用于有符号位的转换,MOVZX用于无符号位的拓展。
在C语言当中,一般数据不加说明都默认为是有符号的。浮点数是自带符号位的,没有无符号的浮点数。
数据类型的转换,本质上是原有内存空间太小装不下数据,所以需要扩展数据存储的内存
大类型的数据放到小类型数据中:
截取:从低位开始截取数据放入内存中。
表达式:
//特点三:
void fun (int x, int y){
char a;
int b;
a=10;
b=20;
printf("%d",a+b);
}
//特点四:
void fun(int x, int y){
unsigned char a;
char b;
a=0xfe;
b=1;
printf("%d",a+b);
}
//奇奇怪怪
void fun(){
char a = 1;
unsigned int b = 0xffffffff;
printf("%d",a+b);
}
// >>>结果为-1
%d:有符号的十进制输出;%u:无符号的十进制输出
存储的数据可能是相同的,但它的显示输出就可能不一样。
语句、程序块
//#关系运算符:
void fun(){
int a=1;
int b=2
if(a==b){
printf("ok");
}
}
//对应汇编操作:
// cmp eax,dword ptr [ebp-4],ecx
// sete cl(如果cmp相等没救sete cl,将cl设置为1,不相等就跳转)
//逻辑运算符:
void fun(int x, int y, int z){
if(x>1 && y>1 && z>1){
printf("ok");
}
else{
printf("no");
}
}
//对应反汇编操作:
//利用cmp进行判断,与逻辑--判断为真则不跳转,为假就跳出
//或逻辑相识,但是跳出的判断相反