一、簡要復習常用的匯編指令

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)在堆棧中騰出一個區域用來保存局部變量,這就是常說的所謂局部變量是保存在??臻g中的。方法是:把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]這個地址,然后再比較,這是調試版本編譯的特點??赡苁菫榱朔乐怪苯硬僮鞫褩6鴮е露褩F茐??最后一條直接跳轉到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.blogfshare.com/c-disassembling.html

  哈爾濱品用軟件有限公司致力于為哈爾濱的中小企業制作大氣、美觀的優秀網站,并且能夠搭建符合百度排名規范的網站基底,使您的網站無需額外費用,即可穩步提升排名至首頁。歡迎體驗最佳的哈爾濱網站建設。