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

news/2024/7/22 5:01:13
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)

http://www.niftyadmin.cn/n/2559817.html

相关文章

进程优先级、环境变量、虚拟地址空间

一&#xff1a;进程优先级 cpu分配资源的先后顺序即为进程的优先级&#xff1b; 优先级高的进程有优先执行权利。配置进程优先级对多任务环境的linux很有用&#xff0c;可以改善系统性能&#xff1b; 可以把进程运行到指定的cpu上&#xff0c;把不重要的进程安排到某个CPU&…

改造PE中的函数为导出函数

改造PE中的函数为导出函数 1、前言&#xff1a;为什么要这么做 很多时候&#xff0c;我们发现一个PE(EXE或DLL)中非常有用且功能独立的函数(call xxxxxxxx)&#xff0c;并且已经知道了这个函数的各个入口参数的类型和具体含义&#xff0c;我们想在其他的软件中使用这个函数。于…

如何控制控件的可见性

在xml布局文件中 android:visibility"visible" android:visibility"invisible" android:visibility"gone" 在代码中 view.setVisibility(View.VISIBLE); view.setVisibility(View.INVISIBLE);//view不可见&#xff0c;占用空间 view.setVisib…

Linux下根目录

在linux下我们一直使用很多命令&#xff0c;比如ls,cd,mkdir等等&#xff0c;那这些命令是在哪个目录里呢?我们可以先看看根目录&#xff0c;所有目录都挂在根目录下&#xff0c;有且只有一个根目录&#xff0c;所有东西都从根目录开始&#xff0c;如果在终端输入/home,其实是…

斐波那契数列与IE9

百度什么都知道&#xff1a;当然也包括斐波那契(Fibonacci)数列 在一般算法教材中&#xff0c;把Fib数列都是当做递归的经典示例来讲解的&#xff1a; javascript的写法如下&#xff1a; //递归法(计算到fib(40)时浏览器就挂掉了) function fib(n){ if (n<2){return 1;} re…

替换GINA.DLL实现自己的登陆界面

发表日期&#xff1a;2004年12月9日 出处&#xff1a;www.xiaozhou.net&#xff08;本站原创&#xff09; 作者&#xff1a;酷狗 【编辑录入&#xff1a;webmaster】 想不想拥有自己个性化的WIN2000登陆界面呢&#xff1f;本文就教你如何实现。 登陆界面示例&#xf…

文件描述符fd和重定向(dpu、dpu2)

一&#xff1a;文件描述符 文件描述符是什么&#xff1f; 文件描述符实际是一个数字&#xff0c;进程如何通过一个数组来操作文件&#xff1f; Linux进程默认情况下会有3个缺省打开的文件描述符&#xff0c;分别是标准输入0&#xff0c;标准输出1&#xff0c;错误标准输出2&am…

Handler的应用场景

如果在一个activity里面有多个线程去更新UI&#xff0c;并且都没有加锁机制&#xff0c;那就会造成更新UI错乱&#xff1b;而如果对更新UI的操作都进行加锁处理&#xff0c;就会造成性能下降。使用消息机制&#xff0c;就不必担心多线程的问题&#xff0c;因为更新UI 的操作&am…