x86机器码识别及其反汇编算法

本文介绍了x86体系结构中机器码的识别和反汇编算法,包括prefixes、code、ModR/M等六域的解析过程。重点讲解了prefixes的5种类别及其识别,code的哈弗曼编码特性,以及如何通过二维表格进行code识别。此外,还提到了堆栈操作的注意事项和效率问题。
x86机器码识别及其反汇编算法 x86体系结构CPU的每条指令都可能由以下六个域组成,并且它们在指令中的排列顺序是不能改变的。 这六个域分别是: prefixes code ModR/M SIB displacement immediate 在任何一条指令中code域是必须出现的,其他的域都是可选的。 由于这六个域在指令中的排列顺序是固定的,所以反汇编机器码,就是一个对它们的依次识别过程。 1.对prefixes的识别 Intel的官方手册上说有4类prefixes,为了便于编程和后面的描述,本人将prefixes分成了5类。 x86体系结构CPU的5类prefixes,它们分别为: lock prefix :F0 repeat prefixes :F2,F3 segment override prefixes :2E,36,3E,26,64,65 operand-size override prefix :66 address-size override prefix :67 指令的prefixes可以由这5类prefixes组成,但是每类prefixes只能在指令中出现一次,至于每类prefixes在指令的出项顺序是没有要求的,这点和指令的六个域是不同的。当某类prefixes在同一指令中出现多次的时候,CPU在执行过程中,可能会出现不可预料的结果,至于会不会出现异常,Intel的官方手册中只用了这句话来说明:such use may cause unpredictable behavior.鬼知道会出项什么情况,因此算法必须对这类机器码具有识别能力。但是也可能异常情况不会发生,在反汇编过程,遇到同一类prefixes出现多次的情况,以最后出现的prefix为准,进行机器码识别。 prefixes识别的核心代码: for( ; nSizeOfCode > 0; nSizeOfCode--, pCode++) { IsPrefix = 1; //这个是用来判断当前机器码是不是prefixes switch(*pCode) { case 0xF0: if(lockPrefix == 0) lockPrefix = 0xF0; else isPrefixRepeat = 1; //同一类重复出现 break; ... //这里的代码略了,但是这个地方要注意的是,对一类prefixes中有几个prefix的情况, //这几个prefix共用一个是不是重复出现的标识(lockPrefix是lock prefix的重复出现标识) default: IsPrefix = 0; //不是prefixes机器码了 break; } if(IsPrefix == 0) break; //表示prefixes识别结束 } if(isPrefixRepeat == 1) //说明指令的执行可能会发生异常 说明:lock prefix是用来在多处理器机器上保证对共享内存的互斥访问的,在反汇编的过程中,可以忽略这个前缀。 2.对code的识别 code的识别好象是最难的了,因为CPU中有几个个code,要对这些code进行识别的确不容易,而且CPU中的code还一直呈现出增长趋势,而且每个code对应不同的操作数个数,这些操作数的寻址方式也各异...... 如果你的"计算机体系结构"知识还没有还给老师的话,你应该知道,CPU在设计时,为了提高比特位的利用率,也为了保证一个code不是另一个code的前缀(否则CPU也无法译码),code的编码采用的是哈夫曼算法。利用这个特性,code及其后继的操作数等信息的识别,应该很容易了吧。 code的最大长度是3个字节,当然可以是1个字节,也可以是2个字节,另外,对于某些特定的code,还有3个比特的信息也会用来表示code.这3比特在ModR/M的3、4和5位。当然每个code也最多只能有三个operand哦。 对code的识别一般都是采用二维表格来驱动的。二维表格中记录了给各code的详细信息。这样code的识别就变成了查表,爽吧。这个表格建的怎么样,取决于你的需求。 下面举一个例子来说明表格的信息,及其code的识别过程(拿call指令为例): 查看Intel官方手册(A-M卷),你会发现call指令有四个code,手册列出分别为: E8 cw call re/16 E8 cd call re/32 F2 /2 call re/m16 F2 /2 call re/m32 F2 /2 call re/m64 9A cd call ptr16:16 9A cp call ptr16:32 FF /3 call m16:16 FF /3 call m16:32 关于它们的详细信息请查看Intel的官方手册,上面所列表明call的code占用一个字节,并且指令只有一个操作数,在手册上详细说明了E8是后面的操作数表示相对于下条指令的偏移,F2、FF和9A后面带的操作数是要调用的绝对地址。 根据code的编码规则以及Intel的手册信息,可以用如下结构体来组织数据: typedef tagCodeInfo { long lMask; //掩码 long lCode; //code int nCodeLen; //code的长度 int nBitFeature; //特殊code标识 int nArg0; //第一个operand的寻址方式,这个地方用enum来定义最好,这里只是为了说明算法,就用int来定义了 int nArg1; //第二个operand的寻址方式,用0表示没有这个operand int nArg2; //第三个operand的寻址方式 std::string strCodeName; //code的助记符 }CodeInfo, *PCodeInfo; 通过上面的结构体定义,可以很容易得到4个call的code对应的结构体数据定义了,如下: { 0x0000FF, 0x0000E8, 1, 0, 1, 0, 0, "call" }, { 0x0038FF, 0x0010FF, 1, 0, 2, 0, 0, "call" }, { 0x0000FF, 0x00009A, 1, 0, 3, 0, 0, "call" }, { 0x0038FF, 0x0018FF, 1, 0, 4, 0, 0, "call" }, 上面4个{ }里的第一项和第二项看晕了吧,在说明这个问题时,先说说用这个数据结构是怎么进行code的识别的,设传进来要识别的code为opCode,那么用这个计算公式可以识别code,(opCode ^ lCode) & lMask,只要这个家伙不为zero,就是我们千辛万苦要找的东东了。这里说下上面opCode的求法,opCode并不是传进来的buffer,因为每个code最多只有三个字节,而我们定义的结构体中用long来表示mask这些信息了,所以我们的opCode也要是long型的,很简单,只要传进来的buffer够长的话,用memcpy((char *)&opCode, buffer, 3),如果不够3个字节了,有几个字节就把几个字节copy到(char *)&opCode处,另外说明的是,repeat prefixes是比较讨厌的,如果有这个东西在带反汇编的机器码中,opCode的求法还要加个opCode = (opCode << 8) | repeat prefixes。 下面说那两个项是怎么计算的了。 mask的计算方法:有指令的地方用FF,如果这个code用到了ModR/M中的那3个比特位,这ModR/M对应字节用38. lCode的计算方法:它对应的code照搬,如果这个code用到了ModR/M中的那3个比特位,/2和/3应该看到了吧,这它们乘以8放ModR/M对应字节,为什么是8,是因为它ModR/M字节中表示code信息的那3个bits后面还有3个bits. 到这里code就识别完了,通过以上的那个结构体,我们连code对应的每个operand的寻址方式的求出来了,后面那几个域的识别就方便了,没有难度了。 当然当我们用这个没有识别到有用的code的,那就说明待反汇编的字符串是有错误的。 余下的4个域的识别全部是对operands的识别了,所以把它们放在一起识别。 到这里知道,上篇文章中的FF1578604000为什么是call [406078]了吧。 3.对operands的识别 前面已经把code的operand的个数和每个operand的寻址方式都搞定了,唉,不想说了。就是对每个寻址方式专门写个解析函数的问题了。这里不想讨论编码的问题。 这里说下ModR/M字节,在code的识别过程中我们已经搞定它的第二个域了(它有三个域),由于ModR/M和SIB经常一起来表示寻址信息,这里一块说。还是说个例子把,如果MoRM/M是F8,它的三个域分别是: Mod : 11 Reg/Opcode : 111 R/M : 000 对它的识别解析还是用一个二维表格来驱动的,查看Intel官方手册(A-M卷)第36页,可以查出是在表示用EAX/AX/AL/MM0/XMM0来表示operand,至于这几个寄存器里应该选用那个,在code识别中已经知道了。 这里给出一个普通寄存器的驱动表格的定义: char *regName[3][8] = { { "al", "cl", "dl", "bl", "ah", "ch", "dh", "bh" }, { "ax", "cx", "dx", "bx", "sp", "bp", "si", "di" }, { "eax","ecx","edx","ebx","esp","ebp","esi","edi" } }; 这个表格的查法是,Mod = 11, R/M = 000决定了我们必须查这个表,那查第几行呢,前面code的识别中已经知道了operand的大小了,因此这个信息由code给出。 立即数和[BX+DI]这种寻址就不讲了,同样的道理,可以搞定。 到这里为止就已经把一个指令给识别出来了。 4.对堆栈操作的说明 在Windows的32位程序设计中,堆栈是要双字对齐的了,即不容许下列这些指令的出现了: inc esp; //code是44 dec esp; //code是4c add esp , 小于4的正数; //code是81 sub esp , 小于4的正数; //code是83 反汇编中要注意这些指令。 5.效率问题 由于code的驱动表格没有办法优化(至少是现在还不知道怎么优化),所以导致每次code识别都是用线性查找算法,这个应该对性能影响很大,可以考虑对这个表格进行改造,用code的大小做索引来识别code,这个是以后的事了。 现在这个东西终于近尾声了,爽,回家过年也过的没有压力了,哈哈。 (转自:http://linxer.bokee.com/4277473.html)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值