常用汇编指令与C语言反汇编

一、简要复习常用的汇编指令

1.堆栈相关指令

push:把一个32位的操作数压入堆栈中。这个操作导致esp被减4.(32位平台)。esp被形象地称为栈顶。我们认为顶部是地址小的区域,那么,压入堆栈的数据越多,这个堆栈也就越堆越高,esp也就越来越少。

 

pop:相反,esp被加4,一个数据出栈。pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中。

 

sub:减法。第一个参数就是被减数所在的寄存器,第二个参数是减数。

 

add:加法。

 

ret:返回。相当于跳转回调用函数的地方。(对应的call指令来调用函数,返回到call之后的下一条指令。)

 

call:调用函数。

 

call指令会把它的下一条指令的地址压入堆栈中,然后跳转到它调用函数的开头处。而单纯的jmp是不会这样做的。同时,ret会自动弹出返回地址。

call的本质相当于push+jmp。ret的本质相当于pop+jmp。

 

如果我要一次在堆栈中分配4个4字节长整型的空间,那么没有必要4次调用push,很简单地把esp减去4*4=16即可。当然,也可以同样的用add指令来恢复它。这常常用于分配函数局部变量空间,因为C语言函数的局部变量保存在栈里。

 

2.数据传送指令

mov:数据移动。第一个参数是目的,第二个参数是来源。在C语言中相当于赋值号。

 

xor:异或。xor eax,eax这样的操作常常用来代替mov eax,0.好处是速度更快,占用字节数更少。

 

lea:取得地址(第二个参数)后放入到前面的寄存器(第一个参数)中。

lea edi,[ebp-0cch]  //方括号表示存储器,也就是ebp-0cch这个地址所指的存储器内容。但是lea要求取[ebp-0cch]的地址,那么地址就是ebp-0cch,这个地址将被放入到edi中。等同于mov edi,ebp-0cch的效果,但是mov不支持后一个操作数写成寄存器减去数字。但是lea支持。

 

mov  ecx,30h

mov  eax,0CCCCCCCh

rep   stos dword ptr es:[edi]

stos把eax中的数据放入edi所指的地址中,同时,edi加4,。所以上面代码的意思就是对堆栈中30h*4(0c0h)个字节初始化为0cch(也就是int3指令的机器码),这样发生意外时执行堆栈里面的内容会引发调试中断。

 

3.跳转与比较指令

jmp:无条件跳转。

jg:大于的时候跳转。

jl:小于的时候跳转。

jge:大于等于的时候跳转。

cmp:比较。往往是jg、jl、jge之类的条件跳转指令的执行条件。

 

二、C函数的参数传递过程

对于C程序默认的调用方式,堆栈总是调用方把参数反序(从右到左)地压入堆栈中,被调用方把堆栈复原。

函数调用规则:

image

在用C语言所写的程序中,堆栈用于传递函数参数。写一个简单的函数如下:

void myfunction(int a,int b)
{
          int c = a+b;
}

这是标准的C函数调用方式。其过程是:

①调用者把参数反序地压入堆栈中。

②调用函数。

③调用者把堆栈清理复原。

这就是C编译器默认的_cdecl方式,而Windows API一般采用的_stdcall则是被调用者恢复堆栈(可变参数函数调用除外)。

至于返回值都是写入eax中,然后返回的。

在Windows中,不管哪种调用方式都是返回值放在eax中,然后返回。外部从eax中得到返回值。

_cdecl方式下被调用函数需要做以下一些事情。

(1)保存ebp。ebp总是被我们用来保存这个函数执行之前的esp的值。执行完毕之后,我们用ebp恢复esp;同时,调用此函数的上层函数也用ebp做同样的事情。所以先把ebp压入堆栈,返回之前弹出,避免ebp被我们改动。

(2)保存esp到ebp中。

上面两步的代码如下:

;保存ebp,并把esp放入ebp中,此时ebp与esp同都是这次函数调用时的栈顶
push ebp
mov ebp,esp

(3)在堆栈中腾出一个区域用来保存局部变量,这就是常说的所谓局部变量是保存在栈空间中的。方法是:把esp减少一个数值,这样就等于压入了一堆变量。要恢复时,只要把esp恢复成ebp中保存的数据就可以了。

(4)保存ebx、esi、edi到堆栈中,函数调用完后恢复。

对应的代码如下:

;把esp往下移动一个范围,等于在堆栈中放出一片新的空间用来存局部变量
sub esp,0cch
push ebx  ;下面保存三个寄存器:ebx、esi、edi
push esi
push edi

(5)把局部变量区域初始化成全0cccccccch。0cch实际是int 3指令的机器码,这是一个断点中断指令。因为局部变量不可能被执行,如果执行了,必然程序有错,这时发生中断来提示开发者。这是VC编译Debug版本的特有操作。相关代码如下:

lea edi,[ebp-0cch] ;本来是要mov edi,ebp-0cch,但是mov不支持-操作所以对ebp-0cch取内容,而lea把内容的地址,也就是ebp-0cch加载到edi中。目的是把保存局部变量的区域(从ebp-0cch开始的区域)初始化成全部0cccccccch
mov ecx,33h
mov eax,0cccccccch
rep stos dword ptr [edi] ;串写入

(6)然后做函数里应该做的事情。参数的获取是ebp+8字节为第一个参数,ebp+12为第二个参数,依次增加。ebp+4字节处是要返回的地址。

(7)恢复ebx、esi、edi、esp、ebp,最后返回。代码如下:

pop edi   ;恢复edi、esi、ebx
pop esi
pop ebx
mov esp,ebp  ;恢复原来的ebp和esp,让上一个调用的函数正常使用
pop ebp
ret

为了简单起见,我的函数没有返回值。如果要返回值,函数应该在返回之前,把返回值放入eax中。外部通过eax得到返回值。

 

三、C语言的循环反汇编

1、for循环
   下面是一段C语言的代码,我们的目的是来看其反汇编的结果:
   int myfunction(int a,int b)
    {
         int c = a+b;
         int i;
         for(i=0;i<50;i++)
         {
             c = c+i;
         }
         return c;
    } 

前面的反汇编暂时不理它,这里从for的地方开始反汇编,结果如下:
      for(i=0;i<50;i++)
00412BC7  mov        dword ptr [i],0   // i=0; 给循环变量赋初值
00412BCE  jmp         myfunction+39h (412BD9h)// 跳到第一次循环处
  >  00412BD0  mov        eax,dword ptr [i] 
  |   00412BD3  add         eax,1  // i++;修改循环变量
  |   00412BD6  mov        dword ptr [i],eax 
  |   00412BD9  cmp        dword ptr [i],32h //  比较 i 与50的关系, 检查循环条件
  |   00412BDD  jge          myfunction+4Ah (412BEAh)  // 当 i>=50 [即 !(i<50) ] 时则跳出循环
  |   {
  |      c = c+i;
  |   00412BDF  mov         eax,dword ptr [c]  // 变量 c
  |   00412BE2  add         eax,dword ptr [i]   // 变量 i
  |   00412BE5  mov         dword ptr [c],eax  // c=c+i;
  |   }
  <  00412BE8  jmp         myfunction+30h (412BD0h)  // 跳回去修改循环变量
      00412BEA  mov         eax,dword ptr [c] 
     }
      可以看到for循环主要用这么几条指令来实现:mov进行初始化。jmp跳过循环变量改变代码。cmp实现条件判断,jge根据条件跳转。
用jmp回到循环改变代码进行下一次循环。所以for结构有以下的显著特征:
           mov <循环变量>,<初始值>   ; 给循环变量赋初值
       jmp B       ;跳到第一次循环处
    A: (改动循环变量)       ;修改循环变量。
        …
    B: cmp <循环变量>,<限制变量>  ;检查循环条件
       jgp  跳出循环
       (循环体)
          …               
        jmp A       ;跳回去修改循环变量 

2、do循环 
  再看一下do循环,因为 do循环没有修改循环变量的部分,所以比for循环要简单一些。
        do
           {
                  c = c+i;
            00411A55  mov        eax,dword ptr [c] 
            00411A58  add         eax,dword ptr [i] 
            00411A5B  mov         dword ptr [c],eax 
            } while(c< 100);
00411A5E  cmp        dword ptr [c],64h  
            00411A62  jl           myfunction+35h (411A55h) 
            return c;
   do循环就是一个简单的条件跳转回去。只有两条指令:
   cmp <循环变量>,<限制变量>
    jl <循环开始点> 

3、while循环
        while(c<100){
            00411A55  cmp         dword ptr [c],64h
            00411A59  jge         myfunction+46h (411A66h) 
               c = c+i;
            00411A5B  mov         eax,dword ptr [c] 
            00411A5E  add         eax,dword ptr [i] 
            00411A61  mov         dword ptr [c],eax 
              }
       00411A64  jmp         myfunction+35h (411A55h)
            return c;
    很明显,我们会发现while要更复杂一点。因为while除了开始的时候判断循环条件之外,后面还必须有一条无条件跳转回到循环开始的地方,共用三条指令实现:
             A: cmp <循环变量>,<限制变量>
                 jge  B
                ( 循环体)
                …
           jmp A
             B: (循环结束了) 

四、C语言判断与分支反汇编

1、if-else 语句
为了观察其汇编语句,下面是一个简单的if判断结构: 
      if(a>0 && a<10)
         {
               printf(“a>0”);
         }
         else if( a>10 && a<100)
         {
               printf(“a>10 && a<100”);
         }
         else 
         {
               printf(“a>10 && a<100”);
          }
    if 判断都是使用cmp再加上条件跳转指令。对于if( A && B)的情况,一般都是使用否决法。如果A不成立,立刻跳下一个分支。依次,如果 B 不成立,同样跳下一分支。
          cmp 条件
          jle 下一个分支
  所以开始部分的反汇编为:
         if(a>0 && a<10)
         00411A66  cmp       dword ptr [c],0 
         00411A6A  jle         411A81h   ; 跳下一个else if的判断点
      00411A6C  cmp       dword ptr [c],0Ah 
         00411A70  jge        411A81h   ; 跳下一个else if的判断点
      {
            printf(“a>0”);
         00411A72  push      offset string “a>0” (4240DCh) 
         00411A77  call        @ILT+1300(_printf) (411519h) 
         00411A7C  add       esp,4 
         }
    else if 的和 else 的特点是,开始都有一条无条件跳转到判断结束处,阻止前面的分支执行结束后,直接进入这个分支。这个分支能执行到的唯一途径只是,前面的判断条件不满足。
   else 则在jmp之后直接执行操作。而else if则开始重复if之后的操作,用cmp比较,然后用条件跳转指令时行跳转。
       else if( a>10 && a<100)
          00411A7F  jmp          411AA9h     ;直接跳到判断块外
       00411A81  cmp         dword ptr [c],0Ah         ;比较+条件跳转,目标为下一个分支处
       00411A85  jle          411A9Ch 
          00411A87  cmp         dword ptr [c],64h 
          00411A8B  jge          411A9Ch 
          {
                printf(“a>10 && a<100”);
          00411A8D  push        offset string “a>10 && a<100” (424288h) 
          00411A92  call          @ILT+1300(_printf) (411519h) 
          00411A97  add         esp,4 
           }
          else 
          00411A9A  jmp        411AA9h   ;这里是else,所以只有简单的一条跳转。
       {
             printf(“a>10 && a<100”);
          00411A9C  push        offset string “a>10 && a<100” (424288h) 
          00411AA1  call          @ILT+1300(_printf) (411519h) 
          00411AA6  add          esp,4 
           }
          return c;

2、switch-case 语句
   switch 的特点是有多个判断。因为 swtich 显然不用判断大于小于,所以都是je(因此,C语言中switch语句不支持float类型的变量),分别跳到每个case处。最后一个是无条件跳转,直接跳到default处。以下的代码: 
          switch(a)
           {
            case 0:
                 printf(“a>0”);
            case 1:
            {
                 printf(“a>10 && a<100”);
                 break;
            }
           default:
                  printf(“a>10 && a<100”);
           }
反汇编的switch(a)
          00411A66  mov         eax,dword ptr [a] 
          00411A69  mov         dword ptr [ebp-0E8h],eax 
          00411A6F  cmp         dword ptr [ebp-0E8h],0  // case 0:
          00411A76  je            411A83h 
          00411A78  cmp         dword ptr [ebp-0E8h],1  // case 1:
          00411A7F  je            411A90h 
          00411A81  jmp         411A9Fh  // default:
          {
             …
   显然是比较a 是否是0、1这两个数字。汇编指令先把a移动到[ebp-0E8h]这个地址,然后再比较,这是调试版本编译的特点。可能是为了防止直接操作堆栈而导致堆栈破坏?最后一条直接跳转到default处。当然,如果没有default,就会跳到swtich{}之外。
   从这里我们可以发现:switch语句里,完成“比较判断”的指令会与“case”指令的两部分,在汇编中,不是按照C语句逐句翻译的,而是分开为两个指令模块来实现的!
       case 0:
                printf(“a>0”);
          00411A83  push        offset string “a>0”  (4240DCh) 
          00411A88  call          @ILT+1300(_printf) (411519h) 
          00411A8D  add         esp,4 
          case 1:
          {
               printf(“a>10 && a<100”);
          00411A90  push        offset string “a>10 && a<100” (424288h) 
          00411A95  call          @ILT+1300(_printf) (411519h) 
          00411A9A  add         esp,4 
              break;
          00411A9D  jmp         myfunction+8Ch (411AACh) 
          }
          default:
               printf(“a>10 && a<100”);
          00411A9F  push        offset string “a>10 && c<100” (424288h) 
          00411AA4  call          @ILT+1300(_printf) (411519h) 
          00411AA9  add         esp,4 
           }
    至于case 和 default分支中,如果有break,则会增加一个无条件跳转汇编指令。若没有break,则就没有任何循环控制代码。

本文链接:http://www.alonemonkey.com/c-disassembling.html