第三部分:指令前缀(Prefixes)

指令前缀是解析一条指令之前所要做的第一项工作。从某种意义上来说,指令前缀是指令操作的辅助说明信息。

什么是指令前缀

指令前缀是作为指令的辅助说明信息的,指令前缀是独立于指令之外的,最熟悉的一个恐怕就是重复指令前缀了repz, repnz...。这些前缀作用于链式指令之前,可以在一定条件下重复这些操作,看看一个实际的例子,首先打开ollydbg,载入作为“空白纸”的blank.exe,在汇编窗口Ctrl + E输入A4这时你可以看到:
    00401000 >    A4            movs    byte ptr es:[edi], byte ptr [esi]
    这是链式指令之中的一条movsb指令,现在再Ctrl + E输入:F3 A4看到了吧:
    00401001      F3:A4         rep     movs byte ptr es:[edi], byte ptr [esi]
    这里F3就是指令前缀(rep)。链式指令前缀的位置和作用是很明显的,你也可以使用不同的指令(movsd, stosb等指令和repnz(F2)做一下测试。如果不知道指令的编码,可以在汇编窗口的汇编栏,双击鼠标,输入对应的汇编指令,就可以在右边的窗口中看到对应的指令编码了)。
    早期的intel处理器对内存的管理是采用分段式的,不同的分段由段寄存器来索引,所有的寻址方式跟这种管理策略密切相关。但是如果把段寄存器信息也编入指令之中,将使寻址编码方式太过复杂,这里intel采用了一种比较简单的方法,那就是对每一种寻址方式都假设一个默认的段,这就是我们学习汇编语言初期所学到的:bx, si, di等寄存器寻址默认的寄存器为ds;bp,sp等默认的寄存器为ss,ip默认的寄存器为cs...等等(其实默认寄存器也用到了指令编码之中,比如mul,div等指令都默认结果或被除数等为算术寄存器,这样的好处就是,可以大大减少指令大小,然而也同样破坏了指令的一致性)。这里就存在一个问题,如果我想访问特定的段怎么办?比如我想用bx, si, di等索引es, fs, gs等怎么办(保护模式之下,段寄存器的内容为段选择子,一般其内容都是由操作系统维护,我们现在用到的机会大大减少)?intel使用指令前缀来解决这个问题,输入以下指令(ctrl+e输入编码,或双击输入汇编代码):
    00401004      8B07          mov     eax, dword ptr [edi]
    [edi]默认的寻址寄存器为ds,再输入:
    00401006      2E:8B07       mov     eax, dword ptr cs:[edi]
    2E在这里就是段改写指令前缀(cs),还有其他的5个段寄存器的改写指令前缀(ss-36H, ds-3EH, es-26H, fs-64H, gs-65H),你可以用这些前缀再任何有内存寻址操作的指令之前,看看效果。
    
    当然还有其他一些指令前缀,后面将一一介绍,这里需要说明一下的是,指令前缀有一个特点,那就是它只针对特定的指令才有效。例如前面的重复指令,只有当它们用在链式指令之前的时候才有效,你也可以强行把它用在其他指令之前,做以下实验:
    00401000 >    40            inc     eax
    强行输入(Ctrl + E):
    00401001      F3:           prefix rep:                                      
    00401002      40            inc     eax
    这里虽然ollydbg识别了这种前缀指令,但是其实cpu还是把它当作同一条指令来执行的,把ollydbg调试选项-〉异常-〉无效或特权指令关掉,运行这条指令。你会看到并没有重复事件发生。
    再来看看段前缀指令,在没有内存寻址的指令中试验一下:
    00401003      40            inc     eax
    00401004      26:40         inc     eax
    前缀指令被完全忽略。
    intel在官方文档有中这样描述:such use may cause unpredictable behavior。所以在解析段前缀指令的时候,一定要主要该段前缀指令使用在何种指令之前。

指令前缀的分类:

在intel指令格式图中对指令前缀的字节数是这样说明的:Up to four prefixes of 1 byte each (optional)。很显然rep和repnz是不能同时使用的,intel把这些分为了一组,同样上面介绍的段改写指令也分成了一组。一条指令之前可以同时包含多个不同的组,不同的组只能使用改组其中一条前缀。
    intel指令前缀目前总共有四类,除了前面介绍的两类,还有两类关于操作数大小的指令:操作数数大小改写指令和地址大小改写指令。如图:

2.1 重复指令前缀(lock repz repnz)
     已经介绍了repz, repnz,还有一条lock前缀,这个前缀是用在多处理器之中的,具体的用途大家可以可以查看intel官方文档的相关的说明。

2.2 段改写指令前缀(cs ds ss es fs gs)

除了上面的说明,还有一点,当指令中有两个默认段(一般会出现在指令中有两个操作对象都为内存的时候)的时候,段改写指令前缀改写哪一个?例如:
    00401000 >    A5            movs    dword ptr es:[edi], dword ptr [esi]
    加上段改写指令,到底哪个会变?
    00401001      26:A5         movs    dword ptr es:[edi], dword ptr es:[esi]
    结果很明显。大家可以用其他链式指令做做实验,然后详细读一下svin关于这部分的讲述(opcode6#)。

Svin还有关于win32下段值的改变对寻址结果影响的实验(Opcode6#)。由于这跟指令编码没有什么关系,这里只是提一下实验结果:
    (1)在win32下,所有的cs, ds, ss使用的是同一个端选择子(尽管值不一样?高手解释一下)。
    (2)es的值对一些默认使用该段的指令是有影响的。比如movs默认的源为:ds:[esi]目的为:es:[edi],在执行这些指令之前,改变es的值,将会导致不正确的结果。
    (3)用户模式下,唯一用得到的段寄存器就是FS了,FS:[0]指向SEH链,关于这个,研究过系统结构化异常处理的朋友可能都知道。

2.3 操作数大小改写指令:(66H)

首先做一下试验:
    00401006      8B06          mov     eax, dword ptr [esi]
    00401008      66:8B06       mov     ax, word ptr [esi]
    可以看到,66H使得指令的操作数从32位变到了16位。386时,intel把所有的通用寄存器都增加到了32位,但是intel并没有增加操作这些寄存器的指令,他们解决的办法就是直接使用同16位操作时相同的编码,同时使用操作数大小改写指令作为二者之间的区分。这里要注意改写的涵义:当目前cpu工作在16位模式时,66H的出现将使操作变成32位。也就是cpu工作在16位的时候:
    00401006      8B06          mov     ax, word ptr [esi]
    00401008      66:8B06       mov     eax, dword ptr [esi]
    而当cpu工作在32位模式的时候,66H的出现就会直接使得操作数大小都变成16位。注意,66H只是16位和32位之间的转化指令前缀。32位和64位之间的区别我没有研究过(谁知道的话,可以说出来大家一起学习一下)。8位和16位,32位之间的区分就有着本质的区分了。对比一下:
    00401010      8A06          mov     al, byte ptr [esi]
    00401006      8B06          mov     eax, dword ptr [esi]
    00401008      66:8B06       mov     ax, word ptr [esi]
    可以看到al的指令编码都变化了。这里是一个概念的区分,al、 ah在intel文档中统统成为部分寄存器(partial register),这跟16位和32位(full-register)是有着本质的区别的。部分寄存器和全寄存器之间的区分是用编码中的一个为称为w位的bit来区分的,在后面的指令编码的解释部分会有详细的说明。但是,这里概念的区分一定要清楚。

稍微总结一下,操作数改写指令是在16位和32位(full-register)之间转换的前缀指令,转化的结果跟目前cpu工作的模式有关。还有,就是要注意,全寄存器和部分寄存器之间的概念上的差别。

2.4 地址大小改写指令前缀:(67H)

还是先做一下试验:
    00401000    8B03          mov     eax, dword ptr [ebx]
    00401002      67:8B03       mov     eax, dword ptr [bp+di]
    可以看到加上67指令前缀之后,寻址方式有变化——由32位模式转变为16位模式。注意这里的转变不是32位到16位的转变,而是32位模式到16位模式的转变。
    16位模式下能用于寻址的只有bp, bx, si, di寄存器:

32位模式之后,所有的通用寄存器都能用于寻址,而且32位模式中新增加了一种新的寻址方式(标量寻址)。关于寻址模式The Art of Assembly的第四章中有详细的介绍(下载:Ch04.pdf),SIB部分会有详细说明。

同66H一样,67H的作用是模式转换,也就是在16位环境中,67H的出现意味着指令使用32位的寻址方式。

这些指令就是目前intel 64 / IA32 结构体系中的所有指令前缀,这些四类指令前缀可以重复出现,但是同类指令中只能出现其中一种,看看下面的指令:
    00401005      26:66:67:8B03       mov     ax, word ptr es:[bp+di]
    这条指令中出现了3种指令前缀,这3中指令前缀出现的顺序可以随意,也就是说这些指令前缀都是平行的。

指令前缀的解析代码。

指令前缀是解析一条指令之前所要做的首要的工作。看完上面的介绍,解析指令前缀时需要注意和解决的问题就很显然了:
    (1) 保存当前的指令前缀,供后面的解析代码使用。
    (2) 当碰到连续相同的指令前缀的时候,要判断这些指令前缀是不是属于同一组,不属于同一组的指令前缀可以并行存在,而如果出现同一组中的指令的时候,做出适当的处理。例如,从何处开始下一条指令等等。

这部分如何实现是很直观的,说下我的实现方法:
    1、使用数组保存可能出现在指令中的字符串,把解析到的指令前缀的值赋为对应字符串的索引值。
    例如:定义

代码:

const char *RepeatPrefixes[] = {"lock", "rep", "repe", "repz", "repne", "repnz"};

const char *SegmentRegisters[] = {"es", "cs", "ss", "ds", "fs", "gs"};

如果当前指令前缀的值为F3H(rep或者其等价的repz)便可以把INSTRUCTION结构体中对应的RepeatPrefix(初始值设为-1)设为1或者3,这样解析指令的时候只要检查对应的前缀的值,小于零表示该指令不存在,否则所需要的字符串即为对应字符串数组中的值。

2同组指令是否重复出现的检查:
    使用上面的方案,只要检查到该组指令的值大于等于0,则表示该组中已经出现一条指令前缀了。这里的问题是由于4组指令前缀的出现顺序是任意的,我们不能在检查到冲突的时候就简单地退出该条指令的解析,如果出现这样的组合:
    group1 group2 group3 group1 Instruction。
    这样的组合该解析成这样:
    group1
    group2 group3 group1 Instruction。
    如果在检查到冲突的时候就结束本条指令的解析那么,就会出现下面错误的解析结果:
    group1 group2 group3
    group1 Instruction
    而CPU实际上是按照前面的那种解析结果运行的。所以,我们必须重新确认当前指令的起始位置。
    这里我采用了重新扫描的方法,设置重新扫描标记,如果发生冲突,那么重新解析指令,由于出现重复的组的指令已经设置了,再次解析的时候,就会停在出问题的指令前缀的位置了。例如这样的组合:
    group2 group1 group3 group1 Instruction
    解析到第二个group1时重新扫描,再次碰到group1的时候,就停下,这时候解析结果就会变成正确的:
    group2
    group1
    group3 group1 Instruction
    
    这部分的实现代码:prefix.rar

测试方法:可以在Code部分随意输入各种指令前缀\x123456789ABCDEF,看看上面的组合结果是否能正确出现。
    运行结果:

401000  26 :    prefix es:
    401001  F2 :    prefix repnz:
    401002  66 F3 90 :      ???
    401005  90 :    ???
    401006  26 :    prefix es:
    401007  F2 67 3E 90 :   ???
    40100B  90 :    ???
    40100C  90 :    ???
    40100D  00 :    ???
    40100E  00 :    ???
    
    可以对照ollydbg检查运行结果。

00401000      26:               prefix es:
    00401001      F2:               prefix repne:
    00401002      66:F3:            prefix rep:                              
    00401004      90                nop
    00401005      90                nop
    00401006      26:               prefix es:
    00401007      F2:67:3E:         prefix repne:
    0040100A      90                nop

(还是要再说一下,一些原理性的东西我可能讲不好,但是我会尽量把握掌握的资料跟大家分享,我主要关注的还是实现反汇编/汇编后端的实现细节,也希望感兴趣的朋友讨论这些细节。ps: 我记得在调试程序的时候曾经发现过ollydbg在上面我提到过的组合问题上有一个小小的bug只可惜当时没有记录下来,现在也重复不出来了,也或者是我的错觉?)

 

第四部分:指令(Opcode)

Opcode是机器码中用来编码操作的部分,一条指令应该执行什么样的操作由这部分决定。Opcode的识别方式决定了整个反汇编引擎的框架,在学习完后面编码操作对象(ModR/M,SIB,Immediate)之后,大家就会发现,如何识别Opcode及其一些附加信息成了从代码上完成一个反汇编引擎的关键问题,事实上,不同的反汇编引擎的主要差别就在Opcode的识别方式。这部分本来该在后面讨论更个反汇编引擎的框架的时候详细讨论,但是这里还是先讲讲我对这部分的一些理解和实际经验。

4.1 指令Opcode与机器码的对应关系

我想如果大家没有研究过反汇编引擎之前,一定跟我一样对汇编或反汇编过程有一个这样的直观的误解:一种mnemonic操作对应于一定的机器码,反编译只要识别了机器编码,从mnemonic操作-机器码对应的表中查到到对应的mnemonic就能完成反汇编操作了。实际上,intel的复杂指令真的是名副其实地复杂,可能是现有所有cpu中最为复杂的。mnemonic与机器码的一一对应关系并不存在,甚至(从某种意义上来说),反过来这种关系也并不成立。但是,有一种一一对应的关系是一定存在的,那就是一定的操作对应于一定的机器码。

上面讲的那些对应关系可能把大家弄糊涂了,但都只是些概念上的东西,或者说直观上的一些东西,再加上大家对上面mnemonic、opcode机器码,等代表的涵义各有各的理解,我可能更无法表达我想说的意思了。还是用例子来说明吧:

1. 助记符与机器码之间没有一一对应的关系:

我想这个很好理解,翻看intel指令集说明,以Add为例子:

Mnemonic        Opcode       Instruction  Discription
    ----------------------------------------------------------------------------
    ADD             04 ib            AL,imm8    Add imm8 to AL
    ADD             05 iw            AX,imm16    Add imm16 to AX
    ADD             05 id            EAX,imm32    Add imm32 to EAX
    ADD             80 /0 ib         r/m8,imm8    Add imm8 to r/m8
    ADD             81 /0 iw         r/m16,imm16  Add imm16 to r/m16
    ADD             81 /0 id         r/m32,imm32  Add imm32 to r/m32
    ADD             83 /0 ib         r/m16,imm8    Add sign-extended imm8 to r/m16
    ADD             83 /0 ib         r/m32,imm8    Add sign-extended imm8 to r/m32
    ADD             00 /r            r/m8,r8    Add r8 to r/m8
    ADD             01 /r            r/m16,r16    Add r16 to r/m16
    ADD             01 /r            r/m32,r32    Add r32 to r/m32
    ADD             02 /r            r8,r/m8    Add r/m8 to r8
    ADD             03 /r            r16,r/m16    Add r/m16 to r16
    ADD             03 /r            r32,r/m32    Add r/m32 to r32

可以看到,当add指令的操作对象不同的时候,指令编码是不同的。从机器码(Opcode)翻译到mnemonic的过程还直观一些,从Instruction翻译到机器码(Opcode)的汇编过程就没有那么容易了,尤其当存在后面将介绍的d位(影响两个操作码的顺序),翻译的过程将有更多的选择,如何选择最短的指令编码将是汇编程序要解决的最终要的问题。以Add举个例子吧:
    
    d位的影响:
    00401000      03C3          add     eax, ebx
    00401002      01D8          add     eax, ebx
    长短不一的指令编码:
    00401007      03041B            add     eax, dword ptr [ebx+ebx]
    0040100A      03045D 00000000   add     eax, dword ptr [ebx*2]

尤其是后面的例子,当一个汇编新手用add eax, dword ptr[ebx*2]指令的时候,好的汇编器必需能够把其翻译成等价的上面的编码方式。指令越短,占用空间越少,cpu取码时间、译码时间越短,执行效率越高。

2. 机器码于助记符之间没有一一对应的关系。

这样说严格上将是不正确的,上面我说的是从某种意义上来说。现在我们看看到底我想说些什么。首先,看看intel官方手册中关于指令格式部分出现了Opcode的地方:

大家可以看到,除了1-3byte的正规Opcode编码,还有一个位置出现了Opcode,那就是ModR/M的子项的第二项。ModR/M的3-5bit的3个bit的名称(或涵义)是:Reg/Opcode,反斜杠“/”代表或者的意思,也就是3个bit可能用来编码一个寄存器操作对象,但同时也可能联合前面的Opcode一起编码指令。我上面提到的“从某种意义上来说”就是指的如果不考虑这里的Opcode,说实话这3个bit破坏了指令编码的和谐一致性。如果没有这3个bit,所有的Opcode只用前面的1-3个byte编码,识别过程将简化很多。intel为了缩短指令所占有的空间,为了提高指令编码的bit利用率,真是无所不用其极。大家也可能想得到,出现这3个bit的地方有两种情况:(a)指令的操作对象中没有寄存器,只有内存和立即数。如:F6 (000) Test r/m8 imm8(b)指令操作的对象中的寄存器是默认已知的。如:F6 (100) mul r/m8。下面给出整个Opcode为F6的指令:
  Opcode  Reg/Opcode  Instruction
  -----------------------------------------------
      F6  1    TEST  r/m8   imm8
  F6  2    NOT  r/m8                
  F6  3    NEG  r/m8
  F6  4    MUL  r/m8
  F6  5    IMUL  r/m8
  F6  6    DIV  r/m8
  F6  7    IDIV  r/m8
 
     “从某种意义上来说”也可以说成“从解码的代码实现的角度来说“,一般的情况是,我们读到了对应的Opcode,但是要确定其对应的操作,还需要接着再读取ModR/M中的Opcode部分,两个部分结合起来才能得到正确的指令编码。

4.2 Intel指令编码Opcode的几种类型。

在学习sivn等前辈的教程的时候,intel指令的编码方式中的“规则”,让我如何设计一张menmonic于编码对应的表的时候困惑了很久,说实话,直到现在我也没有能想到一种比较好的方法设计出一张既能利用这些“有趣”的规则,又能有者统一规则的翻译表出来。(最后,我选择了最直观,当然也是最土最愚蠢的方法:switch..case。原因是多方面的,其中最重要的原因就是,如果我还继续考虑如何设计一张优美的翻译表,我可能永远也没有办法实现一个反汇编引擎了,边学习,边考虑,以后再设计一张翻译表可能对我来说更现实。另外:switch..case能非常有效地利用这些“规则”,虽说swtich..case方法土,但是执行效率可能是最高的,而且忠实地实现了那句话“反汇编是解析出来的,而不是查表查出来的”)。下面我介绍一下我学习到的这些有趣的指令编码“规则”,或许朋友们在以后设计翻译表的时候可以再返回这里,当考虑一下这些“规则”:

Opcode中编码操作对象:

Opcode并没有那么单纯,intel的工程师们为了提高bit利用率,在一些指令Opcode部分编码了一些寄存器操作对象。大家可能也知道Intel的cpu通用寄存器就只有8个,编码这8个寄存器用3个bit就够了,甚至一些1byte的指令也能节省出这3个bit出来。

首先介绍一下intel对通用寄存器的编码,这个可能在后面会再重新介绍:

可以看到,intel对8位,16位,32位的寄存器采用相同的编码,前面在介绍一些指令前缀的时候已经介绍了Intel如何识别8位,16位和32位操作对象的,这里对寄存器的识别也用的是同样的方式。还有mm和xmm寄存器,这些寄存器用在mmx指令之中,解析mmx指令的时候,对应的寄存器要作相应的转换。

00401000      40  0100 0[000]  inc     eax

00401001      41  0100 0[001]  inc     ecx

00401002      42  0100 0[010]  inc     edx

00401003      43  0100 0[011]  inc     ebx

00401004      44  0100 0[100]  inc     esp

00401005      45  0100 0[101]  inc     ebp

00401006      46  0100 0[110]  inc     esi

00401007      47  0100 0[111]  inc     edi

我把方括号加上之后大家可能一眼就能看出来了,这些Opcode的区别就在于最后的3个bit不同,而且这3个bit代表的数字刚好对应着指令唯一的寄存器操作对象的编码。相似的指令还有dec r16/32(48-4F),push r16/32(50-57),pop r16/32(58-5F),xchg r16/32(90-97),mov r8, imm8(B0-B7),mov r16/32, imm16/32(B8-BF),bswp r16/32(0F C8- 0F CF)。(建议大家亲手在od中做一下实验,不麻烦,但能加深一些理解)

flag条件相关的指令编码格式:

先看看下面的这两组指令:(jcc related8)

00401000   - 70 FE             jo      short 00401000  0111 [000]0

00401002    - 72 FE             jb      short 00401002  0111 [001]0   
    00401004    - 74 FE             je      short 00401004  0111 [010]0
    00401006    - 76 FE             jbe     short 00401006      0111 [011]0
    00401008    - 78 FE             js      short 00401008      0111 [100]0
    0040100A      7A FE             jpe     short 0040100A  0111 [101]0
    0040100C    - 7C FE             jl      short 0040100C  0111 [110]0                  
    0040100E    - 7E FE             jle     short 0040100E  0111 [111]0                  
    
    00401011    - 71 FE             jno     short 00401011  0111 [000]1
    00401013    - 73 FE             jnb     short 00401013      0111 [001]1           
    00401015    - 75 FE             jnz     short 00401015  0111 [010]1
    00401017    - 77 FE             ja      short 00401017  0111 [011]1                  
    00401019    - 79 FE             jns     short 00401019  0111 [100]1                  
    0040101B      7B FE             jpo     short 0040101B  0111 [101]1            
    0040101D    - 7D FE             jge     short 0040101D  0111 [110]1            
    0040101F    - 7F FE             jg      short 0040101F  0111 [111]1    
    
    上面一组指令和下面一组指令为互补指令,互补指令的差别很容易看出来,最后一个bit为1(set)则表示not。同组指令之间的差别在1-3bit这3个bit。
    这些条件跳转指令根flag寄存器中的各个位密切相关。这些跳转指令的条件:

(PS:记得当初在svin的教程上看到过对这些条件跳转编码的一些解析,大致就是上面方括号内的不同位对应着标志寄存器中的不同的位,现在我找不到那部分了,而我自己又实在推算不出来。也罢,把资料呈上,有兴趣的可以看看,如果谁推算出来了或读到了svin教程中对应的部分,共享一下。不过,从编程的角度,这些规律已经很明显了)。

相同物理操作的指令编码:

这里说的相同物理操作是指CPU运算类型相同的指令,看看下面这组指令:

00401000      8000 12       add     byte ptr [eax], 12  80 00[00 0]000 12
    00401003      8008 12       or      byte ptr [eax], 12  80 00[00 1]000 12
    00401006      8010 12       adc     byte ptr [eax], 12  80 00[01 0]000 12
    00401009      8018 12       sbb     byte ptr [eax], 12  80 00[01 1]000 12
    0040100C      8020 12       and     byte ptr [eax], 12  80 00[10 0]000 12
    0040100F      8028 12       sub     byte ptr [eax], 12      80 00[10 1]000 12         
    00401012      8030 12       xor     byte ptr [eax], 12  80 00[11 0]000 12        
    00401015      8038 12       cmp     byte ptr [eax], 12  80 00[11 1]000 12

这组指令的本质是相同的,都是CPU对操作码作加法运算。这里的编码用到了上面说起的ModR/M中的3个bit来区分不同的指令。不仅对于操作码为r/m8, imm8类型的操作如此,实际上对于改组指令其他类型的操作编码在Opcode字节中都能找到类似的区分编码,看看下面的指令组:

00  00[00 0]000  add  r/m8   r8
    01  00[00 0]001  add  r/m16/32/64  r16/32/64
    02  00[00 0]010  add  r8  r/m8
    03  00[00 0]011  add  r16/32/64  r/m16/32/64
    04  00[00 0]100  add  al  imm8
    05  00[00 0]101  add  rax  imm16/32

08  00[00 1]000  or  r/m8   r8
    09  00[00 1]001  or  r/m16/32/64  r16/32/64
    0A  00[00 1]010  or  r8  r/m8
    0B  00[00 1]011  or  r16/32/64  r/m16/32/64
    0C  00[00 1]100  or  al  imm8
    0D  00[00 1]101  or  rax  imm16/32

10  00[01 0]000  adc  r/m8   r8
    11  00[01 0]001  adc  r/m16/32/64  r16/32/64
    12  00[01 0]010  adc  r8  r/m8
    13  00[01 0]011  adc  r16/32/64  r/m16/32/64
    14  00[01 0]100  adc  al  imm8
    15  00[01 0]101  adc  rax  imm16/32

18  00[01 1]000  sbb  r/m8   r8
    19  00[01 1]001  sbb  r/m16/32/64  r16/32/64
    1A  00[01 1]010  sbb  r8  r/m8
    1B  00[01 1]011  sbb  r16/32/64  r/m16/32/64
    1C  00[01 1]100  sbb  al  imm8
    1D  00[01 1]101  sbb  rax  imm16/32

20  00[10 0]000  and  r/m8   r8
    21  00[10 0]001  and  r/m16/32/64  r16/32/64
    22  00[10 0]010  and  r8  r/m8
    23  00[10 0]011  and  r16/32/64  r/m16/32/64
    24  00[10 0]100  and  al  imm8
    25  00[10 0]101  and  rax  imm16/32

28  00[10 1]000  sub  r/m8   r8
    29  00[10 1]001  sub  r/m16/32/64  r16/32/64
    2A  00[10 1]010  sub  r8  r/m8
    2B  00[10 1]011  sub  r16/32/64  r/m16/32/64
    2C  00[10 1]100  sub  al  imm8
    2D  00[10 1]101  sub  rax  imm16/32

30  00[11 0]000  xor  r/m8   r8
    31  00[11 0]001  xor  r/m16/32/64  r16/32/64
    32  00[11 0]010  xor  r8  r/m8
    33  00[11 0]011  xor  r16/32/64  r/m16/32/64
    34  00[11 0]100  xor  AL  imm8
    35  00[11 0]101  xor  rAX  imm16/32

38  00[11 1]000  cmp  r/m8   r8
    39  00[11 1]001  cmp  r/m16/32/64  r16/32/64
    3A  00[11 1]010  cmp  r8  r/m8
    3B  00[11 1]011  cmp  r16/32/64  r/m16/32/64
    3C  00[11 1]100  cmp  AL  imm8
    3D  00[11 1]101  cmp  rAX  imm16/32

这些编码位置intel并没有给出什么特别的名称,然而这些规律是存在的,从编写代码的角度来看这些规律是很有用处的。后面学习完d,w位之后,大家可以看到,上面的这些指令除了名称不同外,其实有着相同的解析方式。相似的编码组还有移位操作指令组(rol ror rcl rcr shl shr sal sar),链式指令组(movs cmps stos lods scas)等等。

段寄存器相关指令的编码

看看下面的指令组:

00401019      06  00[00 0]110  push    es                               
    0040101A      07  00[00 0]111  pop     es                               
    0040101B      0E  00[00 1]110  push    cs                               
    0040101C      90      nop                                      
    0040101D      16  00[01 0]110  push    ss                               
    0040101E      17  00[01 0]111  pop     ss                               
    0040101F      1E  00[01 1]110  push    ds                               
    00401020      1F  00[01 1]111  pop     ds

386以前,只有这些段寄存器,而pop和push对这些段寄存器的操作,大致还是有些规律的。这个看起来跟上面的操作相似,我之所以单独把这个列出来是因为这些指令“散落”在各个角落,但是他们还是有着一定规律的。把它列出来的另外一个原因就是很多指令操作都是这样,它们的涵义相似,那么总能找到一点两点的小“规律”来简化指令解析的过程。比如下面的这些:

27  001[0 0]111  daa  (al)
    2F  001[0 1]111  das  (al)
    37  001[1 0]111  aaa  (al, ah)
    3F  001[1 1]111  aas  (al, ah)

然而,这些都是一些小的,并没有什么真正涵义的“规律”。

上面提到的一些“规律”我想都是对实际的机器码解析过程有所帮助的,至少我认为这些信息或者这些里面的一部分信息最好能编入到指令翻译表中,这样翻译表的才不会有太多的冗余信息。当然,对于用switch..case这样老土的办法实现的反汇编引擎,这些规律是应该而且我认为是必须要考虑的。

现在只是大致介绍了一些intel指令编码的一些“规律”,当然这些规律并不是像d, w, s位(后面介绍)这么通用,但的确存在,这就像ASCII中大小字符转换只需要set或clear字符编码的第5位一样,利用好的确能给人不少方便。

我不知道上面讲的那些是否很混乱,至少我觉得很乱。也不知道这样胡乱地找“规律”是不是有点可笑,因为CPU指令编码是有着固定的编码规则的,指令识别过程最好能像CPU的解码过程一样。能完全模拟电路的过程我想是最完美的,但是,这部分个人理解得十分有限,如果有高手知道设计CPU过程中的指令集的设计过程,还希望能共享一下,大家一起学习。

自己的反汇编引擎--Intel指令编码(2)相关推荐

  1. 【看雪学院论坛】打造自己的反汇编引擎——Intel指令编码学习报告 原帖地址

    http://bbs.pediy.com/showpost.php?p=514671&postcount=8 打造自己的反汇编引擎--Intel指令编码学习报告 作 者:egogg http: ...

  2. Linux intel-xed反汇编引擎使用

    最后一起看看Intel xed 反汇编引擎的使用方法,intel-xed是针对x86平台的反汇编引擎,对于其他架构的支持还是有所欠缺: github:https://github.com/intelx ...

  3. Linux radare2反汇编引擎使用

    已经说了三种反汇编引擎了,再一起学习一种radare2,这个开源项目不仅仅时做反汇编,还有其他功能等着我们去发掘,当然可以根据自己的需求选择合适的反汇编引擎,因为之前的文章都是简单的把反汇编引擎简单跑 ...

  4. 反汇编引擎diStorm3

    反汇编引擎diStorm3 diStorm3是Kali Linux自带的一款轻量级.容易使用的反汇编引擎.它可以反汇编生成16位.32位和64位指令.它支持的指令集包括FPU.MMX.SSE.SSE2 ...

  5. 反汇编引擎Capstone

    反汇编引擎Capstone Capstone是Kali Linux自带的一款轻量级反汇编引擎.它可以支持多种硬件构架,如ARM.ARM64.MIPS.X86.该框架使用C语言实现,但支持C++.Pyt ...

  6. C/C++ BeaEngine 反汇编引擎

    反汇编引擎有很多,这个引擎没有Dll,是纯静态链接库,适合r3-r0环境,你可以将其编译为DLL文件,驱动强制注入到游戏进程中,让其快速反汇编,读取出反汇编代码并保存为txt文本,本地分析. 地址:G ...

  7. Linux capstone 反汇编引擎使用方法

    最进在研究反汇编引擎,在google看到capstone反汇编工具,据说是IDA都使用了capstone的引擎,记录一下: capstone 官方网站:http://www.capstone-engi ...

  8. 游戏插件开发之防插件剥离的简单保护壳工具设计(利用反汇编引擎实现自动化代码保护)上

    前言: 感谢:易道云学院 tiger老师指导: 前提:需要导入小蜜蜂反汇编引擎,不会的可以回复问,这里不做介绍,MFC页面的搭建也不做详细介绍.因为任务量太大本贴只写代码保护工具的制作,后续解析在下个 ...

  9. Linux x8664汇编,Linux Udis86 反汇编引擎使用

    前两篇说了capstone/beaengine,这节一起用一用经典的udis86; github:https://github.com/vmt/udis86 0x01:udis86相比于前面两个,用起 ...

最新文章

  1. 安装IE8不能调试VS2005的解决办法
  2. (转)如何在一台电脑上开启多个tomcat 和配置让系统识别哪个具体的tomcat
  3. 3_10 MediaMode 中介者模式
  4. 今晚直播丨全新MySQL OLAP实时分析解决方案HeatWave详解
  5. java中的greeting_JAVA基础知识——字符串
  6. git上传项目 openssh_GitHub上传项目
  7. 视频课程:高等数学考研辅导讲座-上
  8. 一分钟快速了解内网穿透软件有哪些
  9. 十、Robotium测试入门
  10. 电脑快捷键横屏变竖屏_电脑屏幕横屏与竖屏之间怎么来回切换?
  11. duilib入门简明教程(1)
  12. 使用scrapy爬取豆瓣上面《战狼2》影评
  13. 快捷方式图标 html,界面上的应用程序快捷方式图标都变成了IE图标
  14. 统计分析/PCA,PCoA,NMDS等的区别
  15. 20180619 Linux 软件包安装和卸载 7.1 - 7.9
  16. 60个实用Android框架排行榜
  17. csdn新手上路,多多关照
  18. C# CAD视图操作之缩放
  19. 网络安全领域专家(更新中...)
  20. 玩转RT-Thread之荔枝派Nano(全志F1C100S) 新手上路

热门文章

  1. 人手上的斗代表什么?
  2. 【科普】什么是“F5G”?
  3. 使用elementui美化原生input file
  4. M6:中文多模态预训练模型
  5. 区块链技术用解决拜占庭将军问题_什么是拜占庭将军问题
  6. pycharm 代码截长图插件 code screenshots 使用记录
  7. workon: command not found处理方法
  8. ajax的验证码发送api接口(侵删)
  9. C++实现爬取网页源代码并下载至本地文件(可直接运行)
  10. 利用css实现元素水平垂直居中的方法(分情况讨论)