OD专题
OD常用快捷键
快捷键 | 功能 |
---|---|
F2 | 下CC断点 |
F4 | 运行到选定位置 |
Ctrl+F2 | 重新运行程序 |
F7 | 单步走,遇到CALL就进 |
F8 | 单不走,遇到CALL不进 |
Ctrl+F9 | 直到出现RET指令时断下 |
Alt+F9 | 若进入系统模块领空,此命令可回到用户进程领空 |
F9 | 运行程序 |
- | 回到刚才的地方 |
Alt+K | 打开堆栈窗口 |
Enter | 可以进入CALL查看 |
空格或者双击 | 修改汇编指令 |
Ctrl+N | 查看程序函数列表 |
各类调试断点
- F2断点(CC断点,int3断点)
- 内存断点
- 内存访问断点(对内存执行、写入等都算访问。执行即把机器码当成程序执行时被断下;写入即改写这部分机器码时被断下)
- 内存写入断点(对内存写入)
- 硬件断点
- 硬件执行断点(运行到指定代码处触发断点)
- 硬件访问断点(对内存执行、写入等都算访问。执行即把机器码当成程序执行时被断下;写入即改写这部分机器码时被断下)
- 硬件写入断点(对内存写入)
- 记录断点
- 条件断点
- 消息断点
常用命令栏命令
命令 | 作用 |
---|---|
BP | 使用条件中断 |
BC | 清除断点 |
DD | 数据窗口查看指定地址 |
DB | 数据窗口十六进制字节格式查看指定地址 |
DU | 数据窗口 UNICODE 格式查看指定地址 |
HR | 下硬件访问断点 |
HW | 下硬件写入断点 |
HE | 下硬件执行断点 |
HD | 清除硬件断点 |
调试经验总结
- 大部分函数的返回值基本上都在eax里
- 如何判断CALL有多少个参数?
- 看CALL前面的有几个PUSH,假设有3个,就是有3个参数
- 进入CALL,看CALL尾部retn指令 是几,假设是:retn 4 说明该CALL有1个参数,假设retn 0c 说明有3个参数,0c(十六进制)=12(10进制),之所以4为一个参数,是因为1个PUSH 占用4字节。
- 如何平衡堆栈?
Cdecl调用约定:先判断出CALL有几个参数,平衡堆栈就是用add esp,参数个数*4 16进制格式
StdCall调用约定或Pascal调用约定:先判断出CALL有几个参数,平衡堆栈是函数内部用ret 参数个数*4 16进制格式
// 假设:有5个参数,5*4=20(十进制) =转换成十六进制(14) 也就是: add esp,14
Push 1
Push 2
Push 3
Push 4
Push 5
Call 00401000
Add esp,14
对汇编设立不变OD汇编死码与二进制死码
=======================
为什么要设立?
因为游戏更新基址,较大的偏移会变化,为了方便,
所以设立OD死码,方便快速获取游戏更新后的相关数据。
=======================
什么是OD汇编死码与OD二进制死码?
OD汇编死码指的就是OD反汇编中游戏更新后也不变的汇编
。
OD二进制死码指的就是OD反汇编中游戏更新后也不变的二进制
。
=======================
如何设立OD汇编死码?
条件:最多8条汇编,因为OD里ctrl+s搜的时候最多只能8条
哪些汇编不能用于OD死码:
call 内存地址
jz/jnz/je(等跳转语句) 内存地址
mov eax,[ecx+0abc] 偏移abc比较大的不可以。
mov eax,[esp+30] 汇编里有ESP堆栈寄存器的不可以,后面跟的偏移+30会变化
举例:
LEA EAX,DWORD PTR SS:[ESP+10]
LEA ECX,DWORD PTR SS:[ESP+40]
LEA EDX,DWORD PTR SS:[ESP+4]
PUSH ESI
PUSH EDI
PUSH EAX
=======================
如何设立OD二进制死码?
根据上面不能用于死码的情况来设立二进制死码:
具体设立是:OD中选中一块汇编,右键-二进制-二进制复制。
快捷键:ctrl+b
举例:
0077B832 |. 56 PUSH ESI
0077B833 |. 57 PUSH EDI
0077B834 |. 50 PUSH EAX
0077B835 |. 8B8424 D0000000 MOV EAX,DWORD PTR SS:[ESP+D0]
0077B83C |. 51 PUSH ECX
0077B83D |. 52 PUSH EDX
0077B83E |. 50 PUSH EAX
0077B83F |. E8 0C9B1200 CALL elementc.008A5350
56 57 50 8B 84 24 D0 00 00 00 51 52 50 E8 0C 9B 12 00
56 57 50 8B 84 24 ?? 00 00 00 51 52 50 E8 ?? ?? ?? ??
汇编专题
寄存器
IA-32架构中一共有4个32位寄存器,用于保存临时数据,它们分别是EAX、EBX、ECX和EDX。
这4个32位寄存器的通用寄存器名字前面都有一个“E”字母,含义是“Expand”扩展,这是由于在16位的时代,这4个通用寄存器的名字是AX、BX、CX和DX,到了32位后就在它们的名字前面加个“E”来区别是32位还是16位
这4个32位的通用寄存器可以当作16位使用,也可以当作8位使用。当作8位使用时,就将AX折开为AH和AL,AH中的“H”代表“high”,意思是高位的意思,AL中的“L”代表“low”,意思是地位的意思。同理,BX、CX和DX可折开为BH、BL、CH、CL、DH、DL来使用。
通用寄存器
EAX寄存器:EAX称为累加器,常用于算数运算、布尔操作、逻辑操作、返回函数结果等。
EBX寄存器:EBX称为基址寄存器,常用于存放内存地址。
ECX寄存器:ECX称为计数寄存器,常用于存放循环语句的循环次数,字符串操作中也常用。
EDX寄存器:称为数据寄存器,常常和EAX一起使用,例如MUL命令结果超过32位,则高32位使用EDX寄存器保存,EAX寄存器则保存低32位。
注意:上面所述的4个通用寄存器的专门用途不是一成不变的,编译器在编译程序的时候会根据很多因素,例如编译器、编译条件、操作系统等做出相应的改变,读者要知道着手研究的程序是用什么编译器编译,然后针对具体的编译器参考该编译器的说明。
变址寄存器
顾名思义,变址的含义是内存地址会变动的,也就是说变址寄存器中存放在变动的内存地址。80386架构中有两个变址寄存器,分别是ESI和EDI。
ESI:ESI称为源变址寄存器,通常存放要处理的数据的内存地址。
EDI:EDI称为目的变址寄存器,通常存放处理后的数据的内存地址。
ESI和EDI寄存器的用途:
ESI和EDI常用来配合使用完成数据的赋值操作,下面是一个ESI和EDI配合使用的例子。
rep movs dword ptr [edi],dword ptr [esi]
rep:重复执行指令
意思就是将ESI指向的地址的值以4字节方式拷贝到EDI指向的地址中,重复执行ECX次,每次执行后ESI+4,EDI+4,ECX-1,OD中在这段代码中下断后按F7单步步入就可以观察到这3个寄存器的变化
指针寄存器
80386的指针寄存器有基址寄存器EBP,堆栈指针寄存器ESP和指令指针寄存器EIP。只需要了解基址寄存器EBP和堆栈指针寄存器ESP即可,指令指针寄存器EIP总是指向下一条要执行的指令的地址,一般情况下无需修改EIP。
EBP:EBP称为基址寄存器,可作为通用寄存器用于存放操作数,常用来代替堆栈指针访问堆栈中的数据
ESP:ESP称为堆栈指针寄存器,不可作为通用寄存器使用,ESP存放当前堆栈栈顶的地址,一般情况下,ESP和EBP联合使用来访问函数中的参数和局部变量
EBP和ESP寄存器的用途:
EBP和ESP常配合使用完成堆栈的访问,下面是一段常见的堆栈访问指令。
push ebp
mov ebp,esp
sub esp,78
push esi
push edi
cmp dword ptr [ebp+8],0
标志寄存器
标志寄存器EFLAGS一共有32位,在这32位中大部分是保留和给编写操作系统的人用的,一般情况下只需知道32位的低16位中的8位即可,下图列出了标志寄存器EFLAGS中需要了解的8个位的位置。
- OF (Overflow Flag):溢出标志,溢出时为1,否则置0。
- DF (Direction Flag):方向标志,在串处理指令中控制信息的方向。
- IF (Interrupt Flag):中断标志
- AF (Auxiliary carry Flag):辅助进位标志,有进位时置1,否则置0。
- ZF (Zero Flag):零标志,运算结果为0时ZF位位置1,否则置0。
- SF (Sign Flag):符号标志,结果为负时置1,否则置0。
- CF (Carry Flag):进位标志,进位时置1,否则置0。
- PF (Parity Flag):奇偶标志。结果操作数中1的个数为偶数时置1,否则置0。
EFLAGS寄存器的用途:
正如上面所说EFLAGS是实现条件判断和逻辑判断的一种机制,在汇编语言中一般不直接访问EFLAGS寄存器,而是通过指令的操作隐含访问EFLAGS寄存器,下面是一个利用EFLAGS寄存器的例子。
cmp dword ptr [ebp+8],0 ;影响标志CF、ZF、SF、OF、AF和PF
jz 00405898 ; 如果ZF等于1,则跳转到00405898
6种寻址方式
(1) 立即寻址
示例:
mov eax,56H
作用:通常用于赋值。
(2) 直接寻址
示例:
mov eax,[12558878H]
作用:通常用于处理变量。
(3) 寄存器寻址
示例:
mov eax,[edi]
作用:地址在寄存器中。
(4)寄存器相对寻址
示例:
mov EAX,[EDI+32H]
作用:常用于访问数组和结构。
(5)基址加变址寻址
示例:
mov EAX,[EBP+ESI]
作用:常用于访问数组
(6)相对基址加变址寻址。
示例:
MOV EAX,[EBX+EDI-10H]
作用:常用于访问结构。
常用汇编指令
MOV
MOV:称为数值传送指令,格式是MOV DST,SRC
。
MOV指令将源操作数SRC传送到目的操作数DST中,
传送的数据格式可以是8字节、16字节和32字节。
示例代码:
MOV EAX,56 //将56H立即数传送到EAX寄存器
MOV ESI,DWROD PTR [EAX*2+1] //将内存地址为EAX*2+1处的4字节数据传送到ESI寄存器。
MOV AH,BYTE PTR [ESI*2+EAX] //将内存地址为ESI*2+EAX处的8位数据传送到AH寄存器。
MOV DWORD PTR [ESP+36],EBX //将EBX寄存器的值以4字节传送到堆栈地址为ESP+36所指向的地方。
XCHG
XCHG:称为交换指令,XCHG实现寄存器间和内存间的数据交换。
格式是“XCHG DST,SRC”。XCHG指令交换SRC和DST之间的数据。
交换的数据可以是8字节、16字节和32字节,其中SRC和DST必须格式相同。
示例代码:
XCHG EAX,EDX //将EDX寄存器的值和EAX寄存器的值交换
XCHG [ESP-55],EDI //将EDI寄存器的值和堆栈地址为[esp-55]处的值交换。
XCHG BH,BL //将BL寄存器和BH寄存器的值交换。
PUSH POP
PUSH和POP:称为压入堆栈指令和弹出堆栈指令,格式是PUSH SRC(源操作数)
和POP DST(目的操作数)
。
PUSH指令和POP指令是匹配出现的,上面的代码有多少个PUSH下面的代码就有多少个POP,否则堆栈就会不平衡。
PUSH指令将源操作数SRC压入堆栈,同时ESP-4,而POP恰恰相反,POP指令从堆栈的顶部弹出4字节的数值然后放入DST。在32位的Windows操作系统上,PUSH和POP指令的操作是以4字节为单位
的。
PUSH和POP指令常用于向函数传递参数。
示例代码:
PUSH EAX //将EAX寄存器的值以4字节压入堆栈,同时ESP-4
PUSH DWORD PTR [12FF8589] //将内存地址为12FF8589所指向的值以4字节压入堆栈,同时ESP-4
-------------------------
POP DWORD PTR [12FF8589] //将堆栈顶部的4字节弹出到内存地址为12FF8589所指地方,同时ESP+4
POP EAX //将堆栈顶部的4字节弹出到EAX寄存器,同时ESP+4
LEA
80x86有3条地址传送指令,分别是LEA,LDS和LES。其实LDS和LES指令和段寄存器有关,在32位的Windows操作系统上,一般的程序员都不需要管理段寄存器,所以相对而言,LDS和LES寄存器使用得比较少,一般情况下常见的只有LEA指令。
LEA:称为地址传送指令,格式是LEA DST,ADDR
。LEA指令将ADDR地址加载到DST,其中ADDR可以是内存,也可以是寄存器,而DST必须是一个通用寄存器。
LEA指令相当于C语言中的“&”and操作符,需要注意的是LEA和MOV是不同的,前者传送的是地址,后者传送的是操作数。
示例代码:
LEA EAX,[12345678] //指令执行后EAX寄存器的值为12345678H
MOV EAX,[12345678] // MOV EAX,[12345678] 指令执行后EAX寄存器的值为内存地址12345678指向的那个数值
还有一点需要注意LEA指令可用于算法运算
。
示例代码:
LEA ECX,[ECX+EAX*4] //ECX=ECX+EAX*4
ADD
ADD:称为加法指令,格式是add OPER1,OPER2
。(操作数(operand))
ADD指令将OPER1+OPER2结果存放在OPER1中。
示例代码:
ADD EAX,ESI //将EAX寄存器的值加上ESI寄存器的值,得出的结果保存在EAX寄存器中。
ADD EBX,DWORD PTR [12345678] //将EBX寄存器的值加上内存地址为12345678所指的4字节值,得出的结构保存在EBX寄存器中,其中DWORD PTR的意思是显示说明按多少字节来操作,DWORD是DOUBLE WORD的缩写,也就是两个WORD的意思。
不同的平台和编译器中,DWORD占用的字节数不同,在32位的Windows中一个WORD占用16字节空间,DWORD占用32字节空间,读者可以在32位的Windows平台上使用Visual C++编译器编写C语言Printf("%d",sizeof(DWORD))
来验证。
在汇编语言中常用的还有WORD PTR
和BYTE PTR
,表示的意思分别是按WORD来操作和按BYTE来操作。
SUB
SUB称为减法指令,格式是SUB OPER1,OPER2
。
SUB 指令将OPER1-OPER2结果存放在OPER1中。
示例代码:
SUB ECX,4H //将ECX寄存器的值减去4H,得出的结果保存在EAX寄存器中。
SUB BYTE PTR[EAX],CH //将内存地址为EAX所指向的数据按字节为单位和CH寄存器相减,得出的结果按字节为单位保存在EAX所指向的地方。
INC
INC:称为加1指令,格式是INC OPER
。
INC指令将操作数OPER加1,得出的结果保存在OPER中。
示例代码:
INC EAX //将EAX寄存器的值加1,得出的结果存放在原来的地方。
INC WORD PTR [EBX+2] //将内存地址为EBX+2的数据按WORD为单位加1,得出的结果存放在原来的地方。
DEC
称为减1指令,格式是EDC OPER
。
DEC指令将操作数OPER减1,得出的结果保存在OPER中。
示例代码:
DEC EDX //将EDX寄存器的值减1,得出的结果存放在原来的地方
DEC DWORD PTR [EBP+36] //将堆栈地址为EBP+36的数据按DWORD为单位减1,得出的结果存放在原来的地方。
CMP
CMP:称为比较指令,格式是CMP OPER1,OPER2
CMP指令将OPER1减去OPER2,得出的结果不保存,只是相应地设置寄存器EFLAGS的CF、PF、ZF、AF、SF和OF。也就是说可以通过测试寄存器EFLAGS相关的标志的值得知CMP指令执行后的结果。
示例代码:
CMP EAX,56H //将EAX寄存器的值减去56H,得出的结果不保存,并且设置寄存器EFLAGS相关的标志位。
CMP EDX,DWORD PTR [ECX+2] //将EDX寄存器的值以DWORD为单位减去内存地址为ECX+2所指向的数据,得出的结果不保存,并且设置寄存器EFLAGS相关的标志位。
NEG
NEG:称为取补指令,格式是NEG OPER
。
NEG指令将OPER操作数按位取反,简而言之就是将零减去OPER操作数
,得出的结果保存在OPER自身中。
在计算机的CPU中没有减法的机制,减法是用加法实现的,例如:100-55这个操作,CPU实际执行的是100+(-55),而-55相当于是求55的相反数,求相反数的时候NEG指令正好派上用场了。
示例代码:
NEG EAX //将EAX寄存器的值取反,得出的结果保存在EAX中
NEG WORD PTR [12345678] //将内存地址为12345678所指向的数据以WORD为单位取反,得出的结果以WORD为单位,保存在内存地址为12345678
MUL
MUL:称为无符号乘法指令,格式是MUL OPER
。
MUL指令隐含了一个参加运算的操作数EAX寄存器,MUL指令将EAX寄存器的值乘以OPER,得出的结果保存在EAX寄存器中。如果结果超过32位,则高32位使用EDX寄存器保存,EAX寄存器则保存低32位。
示例代码:
MUL EDX //将EAX寄存器的值乘以EDX寄存器的值,得出的结果保存在EAX寄存器中。
MUL BYTE PTR [EDI] //将EAX寄存器的值乘以以BYTE为单位内存地址为EDI所指向的数据,得出的结果保存在EAX寄存器中。
IMUL
IMUL称为有符号乘法指令,原理和操作可以参考MUL指令,IMUL和MUL的区别是IMUL将参与运算的操作数当成有符号数来处理。
DIV
DIV:称为除法指令,格式是DIV OPER
。
DIV 指令将64位(EDX和EAX)或32位(EAX)的值除以OPER,得出的商保存在EAX寄存器中,而余数则保存在EDX寄存器中,由OPER操作数决定按多少字节操作。
示例代码:
DIV ECX //将EAX寄存器的值按4字节为单位除以ECX寄存器的值,得出的结果商保存在EAX寄存器中,余数保存在EDX寄存器中。
DIV WORD PTR [ESP+36] //将EAX寄存器的值按WORD为单位除以堆栈地址为ESP+36所指向的数据,得出的结果商保存在EAX寄存器中,余数保存在EDX寄存器中。
真正代码中除法指令前一半会先调用cwd命令
mov eax,1000
cwd
div ecx
1000(H)/7= 4096/7 =585.1
CWD
是汇编语言中的字扩展指令,它的功能是将一个字型变量扩展为双字型变量,即Change Word to Double word
。
IDIV
IDIV称为有符号除法指令,原理和操作可以参考DIV指令,IDIV和DIV的区别是IDIV将参与运算的操作数当成有符号数来处理
。
OR
OR:称为或操作指令,格式是OR OPER1,OPER2
。OR指令将OPER1操作数和OPER2操作数进行或运算,得出的结果保存在OPER1中。
OR指令主要用于维持某个二进制的某些位的值不变,而另一些位设置为1的情况。把不需要改变的位用0进行或运算,把要设置为1的位用1进行或运算即可。
示例代码:
OR EAX,80008000H //将EAX寄存器和立即数80008000H进行或运算,实际上是将EAX寄存器的31位和15位置1
OR AH,BH //将AH寄存器或BH寄存器进行或运算。
EAX=1C1EA4
OR EAX,80008000H
得到:eax=801C9EA4
AH=1 BH=2
1 二进制 0001
Or 2 二进制 0010
0011(也就是10进制3)
因为第一位是1,第二位是0,而第二个数的第一位是0第二位是1
根据 or计算原理.1 or 0=1,0 or 0=0,0 or 1=1,1 or 1=1
的原理得到的就是0011
AND
AND:称为与操作指令,格式是AND OPER1,OPER2
。AND指令将OPER1操作数和OPER2操作数进行与运算,得出的结果保存在OPER1中。
AND指令主要用于维持某个二进制数的某些位的值不变,而另一些位设置为0的情况。把不需要改变的位用1进行与运算,把要设置为0的位用0进行与运算即可。
示例代码:
AND CH,80H //将CH寄存器的值和80H进行与运算,实际上是将CH寄存器的第7位保存不变,其余位置0.
AND DWORD PTR [EAX],80008000H //将内存地址为EAX所指向的数据按DWORD为单位与80008000H进行与运算,实际上是将内存地址为EAX指向的4字节数据的第31位和15位置保存不变,其余位置0。
AND CH,80H
CH=1 二进制 00000001
80 二进制 10000000
And 00000000
根据 and计算原理 0 and 1 =0 ,0 and 0=0,1 and 1=1的原理得到的就是00000000
AND DWORD PTR [EAX],80008000H
EAX=001C1EA4
001C1EC8 二进制_00000000000111000001111011001000
080008000 二进制10000000000000001000000000000000
And 00000000000000000000000000000000 (十六进制就是00000000)
根据 and计算原理 0 and 1 =0 ,0 and 0=0,1 and 1=1的原理得到的就是00000000000000000000000000000000
NOT
NOT:称为取反指令,格式是NOT OPER
。NOT 指令将OPER操作数取反。注意NOT和NEG不同,NOT指令是按位取反,NEG是求补,意即将0减去操作数。
例如:
15H的二进制为 0 0 0 1 0 1 0 1 B
NEG 15H 的二进制结果为 1 1 1 0 1 0 1 1 B
NOT 15H 的二进制结果为 1 1 1 0 1 0 1 0 B
根据 NOT计算原理 0 not 1, 1 not 0 得 1 1 1 0 1 0 1 0
NOT ECX // ECX=1 执行后就是用FFFFFFFF-1=FFFFFFFE
ECX=00000000000000000000000000000001b
NOT ECX=11111111111111111111111111111110b=FFFFFFFEh
XOR
XOR:称为异或操作指令,格式是XOR OPER1,OPER2
。XOR指令将OPER1操作数和OPER2操作数进行异或运算,得出的结果保存在OPER1中。
XOR指令主要用于维持某个二进制数的某些位的值不变,而某些位取反的情况。把不需要改变的位用0进行异或运算,把需要取反的位用1进行异或运算即可。
示例代码:
XOR EAX,FFFF0000H //将EAX寄存器的值和立即数FFFF0000H进行异或运算,实际上将EAX寄存器的值的高16位取反,低16位保存不变。
XOR AH,F0H //将AH寄存器的值和立即数F0F0H进行异或运算,实际上是将AH寄存器的值的第15位和7位取反,其余位保持不变。
EAX=001C1EA4
001C1EA4_0000000000111000001111010100100
FFFF0000 _11111111111111110000000000000000
Xor _11111111111000110001111010100100 (十六进制:FFE31EA4)
根据 XOR 计算原理:0 xor 0 =0, 1 xor 1 =0,1 xor 0 =1 得到:FFE31EA4
XOR AH,F0H
AH=50_01010000
F0_11110000
XOR 10100000 (A0十六进制)
根据 XOR 计算原理:0 xor 0 =0, 1 xor 1 =0,1 xor 0 =1 得到:A0
TEST
TEST:称为测试指令,格式是TEST OPER1,OPER2
。TEST指令将OPER1操作数和OPER2操作数进行与运算,不保存结果,只设置标志寄存器EFLAGS相应的标志位的值。
TEST指令常用于测试一个二进制位的某些位是否为1,但不改变源操作数的情况。
TEST EAX,F0000000H //将EAX寄存器的值和立即数F00000000H进行与运算,实际上是测试EAX寄存器的第31、30、29、28位是否为1,并且设置标志寄存器EFLAGS相应的标志位的值。
TEST EAX,F0000000H
EAX=001C1EA4
001C1EA4_000000000000000111000001111010100100
F0000000_111100000000000000000000000000000000
And 000000000000000000000000000000000000
根据 and计算原理 0 and 1 =0 ,0 and 0=0,1 and 1=1的原理得到的就是 0
SAL SAR SHL SHR
80x86有4条普通移位指令和4条循环移位指令,它们都隐含地使用CF寄存器参与运算。
4条普通移位指令:SAL算术左移指令、SAR算术右移指令、SHL逻辑左移指令和SHR逻辑右移指令。
这4条普通移位指令的格式都是一样的:普通移位指令名称 OPER1,OPER2
,其中OPER1可以是寄存器或内存,OPER2代表的是移位的位数。其中SAL指令和SHL指令指向结果是一样的。
对于有符号和无符号数而言,SAL算术左移指令和SHL逻辑左移指令每移动一位相当于乘以2
。而SAR算术右移指令和SHR逻辑右移指令有点不同。对于有符号和无符号而言,SAR算术右移指令每移动一位相当于除以2,而SHR逻辑右移指令不管操作数是有符号数还是无符号数,每向右移动一位,左边都是用0填充,所以当操作数是无符号数的时候,SHR逻辑右移指令每移动一位才等于除以2.
示例代码:
SAL EAX,2 //将EAX寄存器的值向左移动2位,得出的结果保存在EAX寄存器中,相当于EAX=EAX*4
SAR DWORD PTR DS:[ESI],4 //将内存地址为ESI所指向的数据按DWORD为单位右移4位,相当于将内存地址为ESI所指向的数据按DWORD为单位的数据除以2*2*2*2=16(十进制)
SHL DWORD PTR [EBP+2H],2 //将堆栈地址为EBP+2H所指向的数据按DWORD为单位左移2位,相当于将内存地址为EBP+2H所指向的数据按DWORD为单位的数据乘以4.
SHR EDI,4 //将EDI寄存器的值逻辑右移ECX位。EDI值除以2*2*2*2=16(十进制)
ROL ROR RCL RCR
4条循环移位指令:ROL左循环移位指令、ROR右循环移位指令、RCL带进位左循环移位指令和RCR带进位右循环移位指令。
这4条循环移位指令的格式都是一样的:循环移位指令名称 OPER1,OPER2
,其中OPER1可以是寄存器或内存,OPER2要么是CL寄存器要么是1,代表移动的次数,如果要移的次数多于1次,则需要把移位次数存放在CL寄存器中。
ROL、ROR和RCL、RCR的区别是前者没有将标志寄存器EFLAGS的CF进位标志包含参与循环移位,后者则把CF进位标志包含参与循环移位。
示例代码:
ROL AL,1 //将EAX寄存器的值向左移动一位,被移出的位送到CF,同时将被移出的位放到最低位。
ROR EAX,CL //将EAX寄存器的值向右移动CL位,被移出的位送到CF,同时将被移出的位放到最高位。
RCL EAX,1 //将EAX寄存器的值向左移动1位,被移出位送到CF,同时将CF之前的值放到最低位。
RCR EAX,CL //将EAX寄存器的值向右移动CL位,被移出位送到CF,同时将CF之前的值放到最高位。
ROL AL,1
ROR EAX,CL
把82H转成二进制数10000010(B)
循环左移1位后变成:00000101(B),换算成十六进制数便是05(H)
循环右移1位后变成:01000001(B),换算成十六进制数便是41(H)
RCL AL,1
RCR AL,CL
首先把82H转换成二进制数:10000010B
带进位循环左移1位后变成:00000100B,CF=1换算成十六进制数便是04H
带进位循环右移1位后变成:11000001B,CF=0 换算成十六进制数便是C1H
JMP
JMP:无条件转移指令
JMP指令格式是:JMP OPER
,其中OPER是目的地址。
示例代码:
JMP EAX //跳转到EAX寄存器指示的4字节地址。
JMP DWORD PTR DS:[ESI+2] //跳转到内存地址为ESI+2指示的2字节地址。
JA/JNBE JAE/JNB JB/JNAE JBE/JNA
条件转移指令格式是:条件转移指令名称 OPER
,其中OPER是目的地址。
无符号数的条件转移说明表
指令名称 | 转移条件 | 转移说明 |
---|---|---|
JA/JNBE | CF=0 且 ZF=0 | 结果低于等于转移,或者高于转移 |
JAE/JNB | CF=0 | 结果不低于转移,或者高于转移 |
JB/JNAE | CF=1 | 结果低于转移,或者不高于等于转移 |
JBE/JNA | CF=1 | 结果低于等于转移,或者不高于转移 |
JG/JNLE JGE/JNL JL/JNGE JLE/JNG
有符号数的条件转移说明表
指令名称 | 转移条件 | 转移说明 |
---|---|---|
JG/JNLE | ZF=0 且SF=OF | 不小于等于转移,或者大于转移 |
JGE/JNL | SF=OF | 不小于转移,或者大于等于转移 |
JL/JNGE | SF≠OF | 小于转移,或者大于等于转移 |
JLE/JNG | ZF=1 且F≠OF | 小于等于转移,或者不大于转移 |
JZ/JE JNZ/JNE JS JNS JO JNO JC JNC JP/JPE JNP/JPO
算术条件转移说明表
指令名称 | 转移条件 | 转移说明 |
---|---|---|
JZ/JE | ZF=1 | 等于0转移,或者相等转移 |
JNZ/JNE | ZF=0 | 不等于0转移,或者不相等转移 |
JS | SF=1 | 为负转移 |
JNS | SF=0 | 为正则转移 |
JO | OF=1 | 溢出转移 |
JNO | OF=0 | 不溢出转移 |
JC | CF=1 | 进位标志被置转移 |
JNC | CF=0 | 进位标志被清转移 |
JP/JPE | PF=1 | 偶数转移 |
JNP/JPO | PF=0 | 奇数转移 |
CALL
函数调用指令CALL:在高级语言中,对函数再熟悉不过了,一个程序的功能可以认为是一组函数互相调用的结果,函数是计算并且返回某些值的一段代码。
在汇编语言中,
使用CALL指令和RET指令或者CALL指令和ADD ESP,OPER指令实现函数的调用与函数返回。
CALL指令的格式是:CALL OPER
,其中OPER是函数地址(子程序地址)
CALL指令首先将ESP堆栈指针寄存器的值减4,然后将CALL后的指令地址压入堆栈,最后计算函数地址,将当前EIP程序指令计数器的值设置为函数地址。
例如:调用下面的C语言函数,计算机会执行如下操作:
Print("%d",a[i]) :
PUSH a[i] //参数入栈
PUSH OFFSET String "%d" //参数入栈
CALL print
Add esp,8
在高级语言中调用函数时不需要编程人员管理堆栈和恢复函数调用前的环境,因为高级语言的编译器在编译源代码的时候已经做好了。
在汇编语言中,编程人员使用CALL调用完函数后需要使用RET指令或ADD ESP指令恢复函数调用前的坏境,以便调用完函数后程序能继续正常执行。
RET
RET函数返回指令的格式是:RET OPER
,其中OPER是需要从堆栈中弹出的字节数。
RET指令首先将堆栈中弹出4字节数据到EIP,然后ESP=ESP+2,最后根据OPER的值修改ESP堆栈指针的值ESP=ESP+OPER。
对于遵从Cdecl调用约定(后面会讲到)的那些函数而言,函数调用完后不是使用RET指令,而是由调用者使用ADD ESP,OPER从堆栈中弹出OPER字节数据来清理堆栈。
函数(子程序)调用约定-3种常用的调用约定
在早期的编程语言中具有代表性的有C语言、Pascal、Basic、不辛的是这些具有代表性的编程语言各自使用不同的函数参数传递方式和由谁负责清除传递参数时使用的堆栈,俗称堆栈平衡方式,从而导致了有几种调用约定。
调用约定是规定函数参数传递的顺序和堆栈平衡的方式。常见的函数调用约定有Pascal调用约定
、Cdecl调用约定
和StdCall调用约定
。
使用Delphi编写的程序都遵从Pascal调用约定,C/C++/JAVA编写的程序都遵从Cdecl调用约定,Windows的API调用遵从的是StdCall调用约定。
常见的函数调用约定表
Pascal调用约定 | Cdecl调用约定 | StdCall调用约定 |
---|---|---|
PUSH parameter1 | PUSH parameter3 | PUSH parameter3 |
PUSH parameter2 | PUSH parameter2 | PUSH parameter2 |
PUSH parameter3 | PUSH parameter1 | PUSH parameter1 |
CALL Message | CALL Message | CALL Message |
- | ADD ESP,0CH | - |
参数从左到右传递,由被调用的函数清理堆栈 | 参数从右到左传递,调用函数清理堆栈 | 参数从右到左传递,由被调用的函数清理堆栈。 |
名词解释
调试器
windows三环下的调试器是利用异常处理来实现的。其主要流程可简要概述如下:创建或附加一个调试进程,然后进入等待调试事件的循环,整个调试工作大 部分都在处理异常调试事件中完成。除了进程创建时需要在入口点设断点(以便程序断在入口点)、进程结束时进行一些必要的释放资源的操作。
创建调试进程和普通的创建进程一样都是使用CreateProcess API。只是创建调试进程时需要设置CreateProcess的第6个参数dwCreationFlags为 DEBUG_PROCESS,这些细节都可以在MSDN中查到。
CC断点
也称int3断点
,简单地说就是将你要断下的指令地址处的第一个字节设置为0xCC,软件执行到0xCC(对应汇编指令INT3)时,会触发异常代码为EXCEPTION_BREAKPOINT的异常。这样我们的调试程序就能够接收到这个异常,然后进行相应的处理。
内存断点
内存断点通过修改内存分页的属性,使被调试程序触发内存访问、写入异常而断下。
硬件断点
硬件断点,顾名思义是由硬件提供给我们的调试寄存器组,我们可以对这些硬件寄存器设置相应的值,然后让硬件帮我们断在需要下断点的地址。
硬件断点是CPU提供的功能,所以要怎么做就得听CPU的硬件寄存器的了。先来看看关于硬件寄存器的说明。Intel 80386以上的CPU 提供了调试寄存器以用于软件调试。386和486拥有6个(另外两个保留)调试寄存器:Dr0,Dr1,Dr2,Dr3,Dr6和Dr7。
条件断点
条件断点, 是指在达到设置的条件时才触发的断点。这个条件不是访问,写入,或者执行,而是由逻辑表达式构成的条件,显然它的功能更加强大。
记录断点
调试时,我们经常需要看看CPU究竟执行什么命令。这就需要用到记录功能。与其叫记录断点,不如叫记录功能。因为在记录功能的基础上,你可以选择断下或者不断下。
消息断点
消息断点是对条件记录断点的应用,是针对一个特殊函数:winproc函数的 Message 参数设置的条件记录断点 。
原理:windows是基于消息的操作系统,每一个消息都按照相同的格式被写入一个结构体,这个结构体叫MSG。消息发生后windows把该结构体传给WinProc函数,用它来执行消息处理功能。每一个窗口有不同控件,一个窗口至少一个消息循环。
单步步入(T)、步过(P)
对于单步步入、步过等命令本质上是利用断点实现,再将执行的下一行代码处下断并运行。
下断点
在指定的代码处设置(BP)一个断点,当运行到断点处时将根据断点类型触发相应的异常事件,调试器捕获异常事件并将程序停止在断点处。
OD中给常见的winApi函数下断,原理就是自动在winApi函数首行代码处下断点,调用winApi函数时将触发断点异常事件并停止在断点处。