最近一直被一些初学者问及有关于汇编指令的长度问题,因此为此专门撰写本文,以求为不知OpCode为何物,或者正为汇编长短不一的指令而烦恼的朋友一个最为快速的指引。
其实,OpCode并不复杂,在本文中我不打算细致入微的告诉大家OpCode的原理,不会为大家带来一大堆有关于什么是定长指令、什么是变长指令的理论知识,更不会带着各位读者玩OpCode Hacking,我只会告诉你“怎么了”、“为什么”以及“如何解决”。
1、我的汇编指令怎么了?
哦,天啊!怎么我今天突然发现汇编指令竟然是长短不一的!你还没发现吗?那么请过目:
[mw_shl_code=c,true]
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[/mw_shl_code]
我们可以看见“CALL 00430B86”这条汇编指令竟然占用了5个字节,而“PUSH ESI”则只占用了1个字节,汇编指令的脾气犹如一只滑头的猴子一样让你摸不到头脑,它很明显的告诉了你“嘿!兄弟,你别想搞懂我!”你也许会感到很郁闷,但是我并不这么想,因为如果我要想自己搞一个反汇编引擎,或者是我要在我的壳里加上代码混淆功能……嗯,算了,就算是我想娱乐一下搞搞免杀吧,那么我终归是要搞懂它的,为什么?因为如果搞懂它的话,那么我就没办法做到这些!
很明显我们的汇编指令继承了Intel工程师的狡猾本质,为了尽可能的减少体积,所以它们的体积被设计的不尽相同。
哇哦!很多读者此时似乎已经想明白是怎么回事了,肯定是不同的指令对应的字节数不一样,恩……这样只要我们搞到一张表就可以了!不是吗?一张可以描述每个指令所用二进制码的表格,然后我们就万事大吉了。
但是很不幸,我在初次接触OpCode时也想出了这个“超级点子”,但是很可惜我的“超级点子”与各位读者的一样,并没有为我解决任何问题,请过目:
[mw_shl_code=c,true]
B8 01000000 MOV EAX, 1
8BC3 MOV EAX, EBX
8BC7 MOV EAX, EDI[/mw_shl_code]
看到了吗,一样的指令,一样的目的操作数,得到的确是完全不同的机器码……
2、这是为什么?
嗯,我想这个问题是很明显的,源操作数如果是一个寄存器的话,那么能有几种可能呢?按照规则来讲貌似只有不超过50种可能,那么如果被操作数是一个数值呢?你想想,32位能表示多少数,将其乘以2就是最终的可能性了,这么多的可能性一定不是区区两个16位数就能表示过来的。
所以说我们的OpCode的长度不是一成不变是有道理的,那么既然如此,那么既然CPU可以正确时识别它,这里面肯定有什么方法是可以计算这些的,没错!这些确实是可以计算的,而且正像我们上面所设想的那样,Intel也确实为我们准备了表格,只不过不是一张,只不过有些复杂……
首先,我们要现拥有这些,以下是我提供的一些连接,因为我们需要这些,请你下载他们:
相关文档 相关文档.7z (3.14 MB, 下载次数: 2254) (下载页面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是如下玩意:
[mw_shl_code=c,true]83C0 01 ADD EAX, 1[/mw_shl_code]
我们可以看到它很神奇的多出来个“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页下面找到了这些信息:
—F0H—LOCK prefix.
—F2H—REPNE/REPNZ prefix (used only with string instructions)
—F3H—REP prefix (used only with string instructions).
—F3H—REPE/REPZ prefix (used only with string instructions).
—F3H—Streaming 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手册为蓝本手动试验,慢慢摸索。