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)
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)