作者:A1Pass
时间:2010-05-20
来源:黑客反病毒 (http://bbs.hackav.com)
出处:看雪论坛    (http://bbs.pediy.com)
注意:转载请务必附带本组信息,否则侵权必究!

    最近一直被一些初学者问及有关于汇编指令的长度问题,因此为此专门撰写本文,以求为不知OpCode为何物,或者正为汇编长短不一的指令而烦恼的朋友一个最为快速的指引。
    其实,OpCode并不复杂,在本文中我不打算细致入微的告诉大家OpCode的原理,不会为大家带来一大堆有关于什么是定长指令、什么是变长指令的理论知识,更不会带着各位读者玩OpCode Hacking,我只会告诉你“怎么了”、“为什么”以及“如何解决”。

1、我的汇编指令怎么了?
    ;哦,天啊!怎么我今天突然发现汇编指令竟然是长短不一的!你还没发现吗?那么请过目:

代码:
E8 31880000      CALL 00430B86
E9 17FEFFFF      JMP 00428171
8B4424 04        MOV EAX, DWORD PTR SS:[ESP+4]
85C0             TEST EAX, EAX
56               PUSH ESI
8BF1             MOV ESI, ECX
    我们可以看见“CALL 00430B86”这条汇编指令竟然占用了5个字节,而“PUSH ESI”则只占用了1个字节,汇编指令的脾气犹如一只滑头的猴子一样让你摸不到头脑,它很明显的告诉了你“嘿!兄弟,你别想搞懂我!”你也许会感到很郁闷,但是我并不这么想,因为如果我要想自己搞一个反汇编引擎,或者是我要在我的壳里加上代码混淆功能……嗯,算了,就算是我想娱乐一下搞搞免杀吧,那么我终归是要搞懂它的,为什么?因为如果不能搞懂它的话,那么我就没办法做到这些!
    很明显我们的汇编指令继承了Intel工程师的狡猾本质,为了尽可能的减少体积,所以它们的体积被设计的不尽相同。
    哇哦!很多读者此时似乎已经想明白是怎么回事了,肯定是不同的指令对应的字节数不一样,恩……这样只要我们搞到一张表就可以了!不是吗?一张可以描述每个指令所用二进制码的表格,然后我们就万事大吉了。
    但是很不幸,我在初次接触OpCode时也想出了这个“超级点子”,但是很可惜我的“超级点子”与各位读者的一样,并没有为我解决任何问题,请过目:
代码:
B8 01000000   MOV EAX, 1
8BC3          MOV EAX, EBX
8BC7          MOV EAX, EDI
    看到了吗,一样的指令,一样的目的操作数,得到的确是完全不同的二进制码……

2、这是为什么?
    嗯,我想这个问题是很明显的,源操作数如果是一个寄存器的话,那么能有几种可能呢?按照规则来讲貌似只有不超过50种可能,那么如果被操作数是一个数值呢?你想想,32位能表示多少数,将其乘以2就是最终的可能性了,这么多的可能性一定不是区区两个16位数就能表示过来的。
    所以说我们的OpCode的长度不是一成不变是有道理的,那么既然如此,那么既然CPU可以正确时识别它,这里面肯定有什么方法是可以计算这些的,没错!这些确实是可以计算的,而且正像我们上面所设想的那样,Intel也确实为我们准备了表格,只不过不是一张,只不过有些复杂……
    首先,我们要现拥有这些,以下是我提供的一些连接,因为我们需要这些,请你下载他们:
      zip附件上传限制1000KB,而且不能上传分卷,所以大家还是到以下下载方便些:
http://bbs.hackav.com/thread-1641-1-1.html
      拥有了这些文档后,我们就可以开始“破译”它了,现在加入我们要“破译”的是“ADD EAX,1”这条指令,请各位读者跟我一起做:
    我们先打开《处理器指令参考》手册(x86eas.hlp),找到汇编指令ADD,我满看到了如下解释:
引用:
注:前面的标号是笔者为了大家方便阅读而加上去的。
    Opcode    Instruction  Description
01  04 ib    ADD AL,imm8  Add imm8 to AL
02  05 iw    ADD AX,imm16  Add imm16 to AX
03  05 id    ADD EAX,imm32  Add imm32 to EAX
04  80 /0 ib    ADD r/m8,imm8  Add imm8 to r/m8
05  81 /0 iw    ADD r/m16,imm16  Add imm16 to r/m16
06  81 /0 id    ADD r/m32,imm32  Add imm32 to r/m32 
07  83 /0 ib    ADD r/m16,imm8  Add sign-extended imm8 to r/m16
08  83 /0 ib    ADD r/m32,imm8  Add sign-extended imm8 to r/m32
09  00 /r    ADD r/m8,r8  Add r8 to r/m8
10  01 /r    ADD r/m16,r16  Add r16 to r/m16
11  01 /r    ADD r/m32,r32  Add r32 to r/m32
12  02 /r    ADD r8,r/m8  Add r/m8 to r8
13  03 /r    ADD r16,r/m16  Add r/m16 to r16
14  03 /r    ADD r32,r/m32  Add r/m32 to r32
解释:
imm是立即数的意思,而imm8就是指8个比特大小的立即数,下面将一一对上面的简写作出解释
imm:立即数,例如01、123、0FAB等
r:寄存器,如r16就代表ax、cx等,r32就代表eax、ebx等
m:内存地址,如[01]、[123]、[0FFFF]等
r/m:寄存器或内存
ib:代表OpCode后面跟着一个byte型数值
iw:代表OpCode后面跟着一个word型数值
id:代表OpCode后面跟着一个dword型数值
/0:代表此OpCode存在ModR/M结构(后面有讲)
/r:代表此OpCode存在ModR/M结构(后面有讲)
    这是什么意思呢?我们以第一条信息为例,它的意思是,如果OpCode的表现形式为04后面在跟一个字节,那么它的指令格式(Description)必然是“ADD AL,8位立即数”,例如“ADD AL,11”。
    啊哈,那么问题到这就解决了,我们上面的“ADD EAX,1”符合第8行的“ADD r/m32,imm8”,那么它的OpCode就应该是“83 01”了吧……
    结果估计大家已经猜到了“事情没那么简单”,实际上我们的汇编指令“ADD EAX,1”所对应的OpCode是如下玩意:
代码:
83C0 01       ADD EAX, 1
      我们可以看到它很神奇的多出来个“C0”不知道是干什么的,这让我们很郁闷!

3、我们如何解决这个问题?
    到这里,我们就要步入正轨了,通过这一节我们要搞明白那个“C0”究竟是怎么出来的。
    既然要步入正轨,我们就要了解一下Intel的指令结构(在24319102.PDF的第31页),具体情况如下:

Prefixes:前缀(最多4个前缀,每个1字节,并不是必需的)
code:主操作码(1-3字节不等)
ModR/M:固定1字节大小,并不是必需的
SIB:固定1字节大小,并不是必需的
Displacement:偏移量(1、2、4字节,并不是必需的)
Immediate:立即数(1、2、4字节,并不是必需的)

    由上可见,其实Intel指令格式中只有一个是必须存在的,就是“主操作码”,也就是我们在上一节查到的那堆东西。不过其他结构索然是可有可无,但是往往在某些时候它们当中的某些结构是必须添加上去的,例如上个例子中的“ADD EAX,1”就是如此。
    在我们讲解Prefixes之前,首先请大家务必牢记一件事,就是OpCode的结构是绝对不能被打乱的,例如Prefixes肯定是要在code前面,而Immediate肯定是在最后面。
    好了,记住上面的基本原则后,我就为大家简单讲解一下这个前缀(Prefixes)究竟做了些什么,非要把指令结构搞得这么复杂,我在24319102.PDF的第31页下面找到了这些信息:

F0HLOCK prefix.
F2HREPNE/REPNZ prefix (used only with string instructions)
F3HREP prefix (used only with string instructions).
F3HREPE/REPZ prefix (used only with string instructions).
F3HStreaming SIMD Extensions prefix.

    这都是什么意思呢?我们拿第一个来说,Intel对它的解释是锁定前缀,首先各位的汇编语言要过关,所谓的锁定就是将我们的指令变为原子指令,具体例子如下:

代码:
F0:8300 01    LOCK ADD DWORD PTR DS:[EAX], 1           ;  锁定前缀
F0:0FB10A     LOCK CMPXCHG DWORD PTR DS:[EDX], ECX     ;  锁定前缀
    这两条指令前都多了个“LOCK”,但是请注意,这只是一个特例,并不是所有的前缀都会导致汇编指令前非要加些什么,这点一定要注意。
    Intel手册给了我们很多其他的前缀,功能也各不相同,本文中作者不可能对其一一进行解释,因此深入的学习就要靠各位自己的努力了。
    而关于操作码,我们在上一节中已经讲了,这里不再多说,因此直接入“ModR/M”与“SIB”中。
    关于“ModR/M”,我认为它在汇编指令中应该是最难的了(虽然只是简单的查表,不过我说的是编程实现),有关于ModR/M的表格在Intel指令手册24319102.PDF的第36页,读到这里的朋友不妨先去看看。
    看完后千万不要头大,我们那一条指令解释一下,就什么都清楚了,其实很简单的,我们仍然拿“add EAX,1”为例吧。
    我在倒数第8行找到了目的操作数,Intel在表中描述如下:

引用:
Effective Address   Mod  R/M
EAX/AX/AL/MM0/XMM0  11   000
    但是我们的源操作数要怎么找呢?上面一行似乎并没有符合的,这就要看我们此条汇编语句在定义时指定了那里,还记得我们在上一节中查到的信息吗:

83 /0 ib  ADD r/m32,imm8  Add sign-extended imm8 to r/m32

    在上一节我仅告诉各位“/0”是代表此OpCode里存在ModR/M结构,但并没有多说什么,其实这里的“/0”就是代表此表中竖排(列)中第一排,其内容如下:

引用:
r8(/r)    AL
r16(/r)    AX
r32(/r)    EAX
mm(/r)    MM0
xmm(/r)    XMM0
/digit (Opcode)  0
REG =    000
     到这里,其实我们的“ModR/M”已经出来了,我们将其以“Mod”“R/M”“/digit”“REG”的方式组合到一起后,正好组合为如下数值:
引用:
/digit REG Mod R/M  
 0     000 11  000 = 000011000 = C0h
    其实在他们的交汇处我们可以看到Intel已经帮我们算好了,真实一张贴心的表呀。
    “ModR/M”解决了,还剩最后的“SIB”了,要想学习“SIB”,我们先要搞明白他什么时候会出现,因此我找到了第36页下面的注释:
引用:
NOTES:
1.The [--][--] nomenclature means a SIB follows the ModR/M byte.
2.……
    注释一的大致意思是“[--][--]”表示ModR/M 后跟随有一个SIB字节,因此我们现在创造一个带有“SIB”结构的汇编指令:
代码:
01048E  ADD DWORD PTR DS:[ESI+ECX*4], EAX
    我们重新回顾一下所学知识,首先我们分析它的指令格式如下:
引用:
01 /r    ADD r/m32,r32  Add r32 to r/m32
    根据“/r”我们可以得知这是一个有“ModR/M”结构的OpCode,因此查表得出其“ModR/M”信息如下:
引用:
横排
Effective Address  Mod  R/M
[--][--]            00  100

竖列
r8(/r)    AL
r16(/r)    AX
r32(/r)    EAX
mm(/r)    MM0
xmm(/r)    XMM0
/digit (Opcode)  0
REG =    000

结果
/digit REG Mod R/M  
0      000 00  100 = 000000100 = 04h
    根据“Effective Address”的“[--][--]”可知此OpCode还存在“SIB”结构,于是继续查位于Intel指令手册24319102.PDF第37页的表格。
    这里我们要着重分解目的操作数“[ESI+ECX*4]”里的内容,我们可以将其分为两部分,既索引与倍率因子(或叫做比率因子)。
    索引指的是基址,本例中就是ESI了,而倍率因子在本例中则是“ECX*4”,我们先从横排取得倍率因子信息如下:
引用:
Scaled Index  SS  Index
[ECX*4]       10  001

    而后由竖排取得索引信息如下:

r32  ESI
Base=  6
Base=  110

    将其组合起来就是:

SS  Index  Base
10  001    110  = 10001110 = 8Eh
    由此,我们便成功的解析了汇编指令“ADD DWORD PTR DS:[ESI+ECX*4], EAX”。
    到了这里本文也该结束了,但是各位读者需要注意的是,本文的责任就像是标题所体现的一样,只是带领大家快速入门,因此有关于很多OpCode的细节本文并没有体现出来,如果你需要深入了解的话,建议各位还是以Intel手册为蓝本手动试验,慢慢摸索。