簡單學習看機器碼的方法


  我們知道,用C、C++、Java等高級編程語言寫的程序,最終都要經過編譯鏈接成本機可執行的程序。這個可執行程序究竟是什么呢?

  在Linux上,我們可以用objdump命令很方便地查看一個可執行程序的機器碼。

  好,現在從一個簡單的示例開始,說一說怎么理解機器碼。

  我們編一個簡單的c程序,如下:

#include <stdio.h>
void f1()
{
        int i;
        for(i = 0; i < 10; i++)
        {
                printf("%d\n", i);
        }
}

int main()
{
        printf("start\n");
        f1();
        printf("end\n");
        return 0;
}

  Makefile的內容如下:

all : test.c
        gcc -o test test.c
        gcc -S test.c
        objdump -D test > dumpresult.txt
clean :
        rm test test.s dumpresult.txt

  程序很簡單,我們就不去關心運行結果了。

  首先看生成的test.s,里面main的匯編代碼為:

main:
        pushl   %ebp
        movl    %esp, %ebp
        andl    $-16, %esp
        subl    $16, %esp
        movl    $.LC1, (%esp)
        call    puts
        call    f1
        movl    $.LC2, (%esp)
        call    puts
        movl    $0, %eax
        leave
        ret

  函數f1的匯編代碼為:

f1:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $40, %esp
        movl    $0, -12(%ebp)
        jmp     .L2
.L3:
        movl    $.LC0, %eax
        movl    -12(%ebp), %edx
        movl    %edx, 4(%esp)
        movl    %eax, (%esp)
        call    printf
        addl    $1, -12(%ebp)
.L2:
        cmpl    $9, -12(%ebp)
        jle     .L3
        leave
        ret

  其實作者在寫本文的時候,匯編方面相關的基礎素養也不高,免強看得懂啦。看這匯編代碼確實讓人暈得很哈,不像C程序代碼那樣接近人的思維。

  這里的匯編代碼是AT&T語法的,跟部分學校里面開設的匯編課程中所采用的intel語法是不一樣的。以intel語法中有mov指令為例,它在AT&T中可能對應movl,而且操作數的方法不一樣,intel的是第一個操作數是目的操作數,第二個是源操作數,而AT&T的剛好相反。這里就簡單提一點,有興趣的請谷歌找詳細內容。

  這里看過匯編代碼之后,再下層就是機器碼了,讓我們一步一步揭開其真實面紗。

  在Makefile中,我們通過objdump命令將生成的可執行程序進行了反匯編,生成的結果在dumpresult.txt文件中。我們在這個文件中找到咱們main函數,如下:

08048423 <main>:
 8048423:       55                      push   %ebp
 8048424:       89 e5                   mov    %esp,%ebp
 8048426:       83 e4 f0                and    $0xfffffff0,%esp
 8048429:       83 ec 10                sub    $0x10,%esp
 804842c:       c7 04 24 14 85 04 08    movl   $0x8048514,(%esp)
 8048433:       e8 ec fe ff ff          call   8048324 <puts@plt>
 8048438:       e8 b7 ff ff ff          call   80483f4 <f1>
 804843d:       c7 04 24 1a 85 04 08    movl   $0x804851a,(%esp)
 8048444:       e8 db fe ff ff          call   8048324 <puts@plt>
 8048449:       b8 00 00 00 00          mov    $0x0,%eax
 804844e:       c9                      leave
 804844f:       c3                      ret

  而f1函數的反匯編如下:

080483f4 <f1>:
 80483f4:       55                      push   %ebp
 80483f5:       89 e5                   mov    %esp,%ebp
 80483f7:       83 ec 28                sub    $0x28,%esp
 80483fa:       c7 45 f4 00 00 00 00    movl   $0x0,-0xc(%ebp)
 8048401:       eb 18                   jmp    804841b <f1+0x27>
 8048403:       b8 10 85 04 08          mov    $0x8048510,%eax
 8048408:       8b 55 f4                mov    -0xc(%ebp),%edx
 804840b:       89 54 24 04             mov    %edx,0x4(%esp)
 804840f:       89 04 24                mov    %eax,(%esp)
 8048412:       e8 fd fe ff ff          call   8048314 <printf@plt>
 8048417:       83 45 f4 01             addl   $0x1,-0xc(%ebp)
 804841b:       83 7d f4 09             cmpl   $0x9,-0xc(%ebp)
 804841f:       7e e2                   jle    8048403 <f1+0xf>
 8048421:       c9                      leave
 8048422:       c3                      ret

  值得說明的是,在test這個可執行程序中,咱們可以用諸如ghex這樣的十六進制查看軟件進行查看。如在main的起點,它的數據是55 89 e5 83 e4 f0 83 ec 10……這樣的數值,如下:

clip_image001

  這就是機器碼,只有機器知道是什么意思,要人來看,估計搞一上午也不定能看懂幾行。但咱們的目標是理解它,是要知道為什么f1函數中80483f5行的mov %esp, %ebp是89 e5,而804840f 行的mov %eax,(%esp)卻是89 04 24了呢?為什么同樣是mov指令,有的以89開頭,有的以b8開頭,有的以8b開頭,等等。

  這得說,這就是人家這樣定義的。我在最前面忘了說了,我的電腦CPU是Intel® Celero® E1500的,簡單點就是intel32位的。要找到這些機器碼為什么是這樣的,得從http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html上找人家的手冊。請在名為“Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes:1, 2A, 2B, 2C, 3A, 3B, and 3C”的這個鏈接上下載相應的手冊,這個上面就說人這些機器碼是怎么定義得來的。這個文檔有3020頁,暈菜了。

  為了快速找到答案,我們直接看第Vol2A 2.1頁,講指令格式的,截圖如下:

clip_image002

  手冊上對這樣的格式的說明,大意是,一條機器碼,Opcode是必須的,其它五個域都是可選的。在本文中所舉的例子中大部分都是沒有第一個域的,所以這里就不提第一個域了。而第二個域,opcode,是每條指令都有的,它是用哈夫曼算法進行編碼的,所以域的長度分為1 2 3字節不等。而這每種編碼究竟對應什么指令呢,這個請參考手冊上第二卷相關章節的描述。

  以mov指令為例,對mov指令機器碼的定義在Vol.2B 4-29頁,部分截圖如下:

clip_image003

  可以看出,就光一個mov指令,針對被操作對象的不同也分不同的機器碼。所以在本示例情況下,以89開頭的機器碼,表示Move r32 to r/m32。

  但以mov %esp, %ebp(89 e5)和mov %eax,(%esp)(89 04 24)又是怎么個原理呢?這就得看第三個域ModR/M了,即是對比e5與04的區別。ModR/M分為三個字段,它將一個8比特的字節按2:3:3分開,Mod和R/M域結合着表示指令操作數的尋址方式,Reg部分表示要用到的寄存器。要理解這三個域的意思,得結合Vol.2A 2-5頁的表2-2來看了,截圖如下:

clip_image004

  我們將十六進制的e5和04按比特位2:3:3分開,它們所表示的數分別是(11 100 101:3、4、5)和(00 000 100:0、0、4)。對照上表,Mod為11,R/M為101,Reg為100所對應的剛好是E5。而表中E5在r32下所表示的意思是將ESP中的值移到EBP中去。由此反匯編出了mov %esp, %ebp。同理04所表示的意思是將EAX中的值移到某個地方,下面的注釋說詳情請見SIB域,也就是要在04后面所跟的24上找答案了。

  SIB占一個字節,它所有取值所對應的意思可以在Vol2A 2-6頁的表2-3中找到,如下:

clip_image005

  我們將十六進制的24按2:3:3分開,它所表示的數是(00100100:0、4、4)。對照上表,Scale為00,Index為100,Base為100,剛好對應的值是24,它所表示的意思是數據不做任何處理直接存放在ESP寄存器中。這就反匯編出了mov %eax,(%esp)指令。

 

  好了,其它的機器碼也是按同樣的思路去理解。怎么樣,有了官方的手冊,理解起來容易多了吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM