收集两个指令,参考格蠹汇编和高级调试网站
AdvDbg System Section : 强大的x86指令
函数开头的通用写法
00401013 83ec40 sub esp,40h
00401016 53 push ebx
00401017 56 push esi
00401018 57 push edi
00401019 8d7dc0 lea edi,[ebp-40h]
0040101c b910000000 mov ecx,10h
00401021 b8cccccccc mov eax,0CCCCCCCCh
00401026 f3ab rep stos dword ptr es:[edi]
1. 最上面的减法指令是把栈指针向低地址方向调整,相当于腾出(分配)一段空间给当前函数中的局部变量使用。
2. 接下来的3个push语句是把EBX、ESI和EDI这3个寄存器的值保存到栈上,防止当前函数破坏了它们的值,根据约定,返回到父函数时这几个寄存器的值应该保持不变。
3. 再下面的4条指令是把刚刚分配的局部变量区全部填充为0xCC,也就是INT 3指令(断点)的机器码;
1)lea指令是把这段空间的起始地址放到EDI寄存器
2)第一个mov指令是把空间的长度(以DWORD为单位)放入ECX寄存器
3)第二个mov指令是把填充内容放入EAX寄存器
4)最后的rep stos指令是具有循环功能的串指令,它把EAX的值存入EDI指向的内存,然后自动递增EDI,递减ECX。这条串指令的机器码只有两个字节(0xf3ab),真可谓是精悍。
再说明一下两个著名串指令
在下面这条循环赋值(串赋值)指令中,ESI和EDI分别指向源和目标,ECX用作计数器,控制要复制的长度。
rep movs dword ptr es:[edi],dword ptr [esi]
CPU执行这条指令时,会自动调整ESI、EDI和ECX的值,循环执行。著名的memcpy函数内部就使用了这条指令。
类似地,memset函数内部主要依靠的是下面这条串存储指令:
rep stos dword ptr es:[edi]
这条指令执行时,CPU会将EAX中的值写到EDI指向的内存,然后调整EDI和ECX。
值得一提的是,上面这两条串指令的机器码都只有两个字节,分别是0xf3a5和0xf3ab。
一个复杂表达式
今天调试一个应用程序时,偶然中看到一条很长的x86指令,机器码有11个字节,目标操作数是一个堪称复杂的表达式,于是摘录下来。
1020f789 c78491c400000000000000 mov dword ptr [ecx+edx*4+0C4h],0
其中:
- 1020f789是这条指令的线性地址
- c78491c400000000000000是机器码
- mov dword ptr [ecx+edx*4+0C4h],0是指令的助记符,MOV是操作码,dword ptr [ecx+edx*4+0C4h]是目标操作数,0是源操作数
晚上回到家里后,拿出IA手册来查,虽然手册很厚,但是手册里描述的还是非常清楚明了,很快就明白了11个机器码的意义
- c7是“MOV r/m32, imm32”这种形式的MOV指令的操作码(Op Code),MOV指令可以说是形式最多的一条x86指令,根据操作数的类型的不同,又细分为很多种。比如“MOV r/m32, imm32”代表的是目标操作数是内存或者寄存器,而源操作数是立即数。也就是把一个立即数赋值给一个变量(在寄存器或者内存中)。
- 84是所谓的ModR/M字节,简单说就是用来指定地址模式,有时也翻译为取址模式,其中的R代表寄存器,M代表内存。ModR/M字节的编码在IA手册中有一张表可以查,
- 84代表的是[--][--]+disp32,表下的备注解释了[--][--]的意思:
- The [--][--] nomenclature means a SIB follows the ModR/M byte
- 通常方括号里是寄存器的名字,而这里,用这种形式表示,寄存器的使用方法是用接下来的一个字节单独描述的。这个字节有个专门的名字叫SIB。SIB代表的是Scale,Index和Base
- 91便是SIB字节,它的编码也有一张表,查表得知,ECX是Base,Index和Scale[EDX*4](Scale是4,EDX是Index)。
- 接下来的4个字节(32位)c4000000是“位移(displacement)”,也84所代表的[--][--]+disp32中的disp32
- 最后的四个字节是立即数
那么这样的复杂指令用来做什么呢?一种典型的使用场合就是用来循环处理一个数组,ECX指向数组的基地址,EDX做循环变量,索引数组的元素,而数组元素的长度可以为2、4、8三种。设计的真是别具匠心啊!