1. 程式人生 > >LINUX平臺可以用GDB進行反彙編和除錯。

LINUX平臺可以用GDB進行反彙編和除錯。

如果在Linux平臺可以用gdb進行反彙編和除錯。(轉)
2. 最簡C程式碼分析

    為簡化問題,來分析一下最簡的c程式碼生成的彙編程式碼:
    # vi test1.c
      
    int main()
    {
        return 0;
    }   
    
    編譯該程式,產生二進位制檔案:
    # gcc test1.c -o test1
    # file test1  
    test1: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped 

    test1是一個ELF格式32位小端(Little Endian)的可執行檔案,動態連結並且符號表沒有去除。
    這正是Unix/Linux平臺典型的可執行檔案格式。
    用mdb反彙編可以觀察生成的彙編程式碼:

    # mdb test1
    Loading modules: [ libc.so.1 ]
    > main::dis                       ; 反彙編main函式,mdb的命令一般格式為  <地址>::dis
    main:          pushl   %ebp       ; ebp暫存器內容壓棧,即儲存main函式的上級呼叫函式的棧基地址
    main+1:        movl    %esp,%ebp  ; esp值賦給ebp,設定main函式的棧基址
    main+3:          subl    $8,%esp
    main+6:          andl    $0xf0,%esp
    main+9:          movl    $0,%eax
    main+0xe:        subl    %eax,%esp
    main+0x10:     movl    $0,%eax    ; 設定函式返回值0
    main+0x15:     leave              ; 將ebp值賦給esp,pop先前棧內的上級函式棧的基地址給ebp,恢復原棧基址
    main+0x16:     ret                ; main函式返回,回到上級呼叫
    > 

    注:這裡得到的組合語言語法格式與Intel的手冊有很大不同,Unix/Linux採用AT&T彙編格式作為組合語言的語法格式
         如果想了解AT&T彙編可以參考文章:
Linux AT&T 組合語言開發指南
 

    問題:誰呼叫了 main函式?
     
     在C語言的層面來看,main函式是一個程式的起始入口點,而實際上,ELF可執行檔案的入口點並不是main而是_start。
     mdb也可以反彙編_start:
       
    > _start::dis                       ;從_start 的地址開始反彙編
    _start:              pushl   $0
    _start+2:            pushl   $0
    _start+4:            movl    %esp,%ebp
    _start+6:            pushl   %edx
    _start+7:            movl    $0x80504b0,%eax
    _start+0xc:          testl   %eax,%eax
    _start+0xe:          je      +0xf            <_start+0x1d>
    _start+0x10:         pushl   $0x80504b0
    _start+0x15:         call    -0x75           <atexit>
    _start+0x1a:         addl    $4,%esp
    _start+0x1d:         movl    $0x8060710,%eax
    _start+0x22:         testl   %eax,%eax
    _start+0x24:         je      +7              <_start+0x2b>
    _start+0x26:         call    -0x86           <atexit>
    _start+0x2b:         pushl   $0x80506cd
    _start+0x30:         call    -0x90           <atexit>
    _start+0x35:         movl    +8(%ebp),%eax
    _start+0x38:         leal    +0x10(%ebp,%eax,4),%edx
    _start+0x3c:         movl    %edx,0x8060804
    _start+0x42:         andl    $0xf0,%esp
    _start+0x45:         subl    $4,%esp
    _start+0x48:         pushl   %edx
    _start+0x49:         leal    +0xc(%ebp),%edx
    _start+0x4c:         pushl   %edx
    _start+0x4d:         pushl   %eax
    _start+0x4e:         call    +0x152          <_init>
    _start+0x53:         call    -0xa3           <__fpstart>
    _start+0x58:        call    +0xfb        <main>              ;在這裡呼叫了main函式
    _start+0x5d:         addl    $0xc,%esp
    _start+0x60:         pushl   %eax
    _start+0x61:         call    -0xa1           <exit>
    _start+0x66:         pushl   $0
    _start+0x68:         movl    $1,%eax
    _start+0x6d:         lcall   $7,$0
    _start+0x74:         hlt
    > 

    問題:為什麼用EAX暫存器儲存函式返回值?
    實際上IA32並沒有規定用哪個暫存器來儲存返回值。但如果反彙編Solaris/Linux的二進位制檔案,就會發現,都用EAX儲存函式返回值。
    這不是偶然現象,是作業系統的ABI(Application Binary Interface)來決定的。
    Solaris/Linux作業系統的ABI就是Sytem V ABI。


    概念:SFP (Stack Frame Pointer) 棧框架指標 

    正確理解SFP必須瞭解:
        IA32 的棧的概念
        CPU 中32位暫存器ESP/EBP的作用
        PUSH/POP 指令是如何影響棧的
        CALL/RET/LEAVE 等指令是如何影響棧的

    如我們所知:
    1)IA32的棧是用來存放臨時資料,而且是LIFO,即後進先出的。棧的增長方向是從高地址向低地址增長,按位元組為單位編址。
    2) EBP是棧基址的指標,永遠指向棧底(高地址),ESP是棧指標,永遠指向棧頂(低地址)。
    3) PUSH一個long型資料時,以位元組為單位將資料壓入棧,從高到低按位元組依次將資料存入ESP-1、ESP-2、ESP-3、ESP-4的地址單元。
    4) POP一個long型資料,過程與PUSH相反,依次將ESP-4、ESP-3、ESP-2、ESP-1從棧內彈出,放入一個32位暫存器。
    5) CALL指令用來呼叫一個函式或過程,此時,下一條指令地址會被壓入堆疊,以備返回時能恢復執行下條指令。
    6) RET指令用來從一個函式或過程返回,之前CALL儲存的下條指令地址會從棧內彈出到EIP暫存器中,程式轉到CALL之前下條指令處執行
    7) ENTER是建立當前函式的棧框架,即相當於以下兩條指令:
        pushl   %ebp
        movl    %esp,%ebp
    8) LEAVE是釋放當前函式或者過程的棧框架,即相當於以下兩條指令:
        movl ebp esp
        popl  ebp

    如果反彙編一個函式,很多時候會在函式進入和返回處,發現有類似如下形式的彙編語句: 
        
        pushl   %ebp            ; ebp暫存器內容壓棧,即儲存main函式的上級呼叫函式的棧基地址
        movl    %esp,%ebp       ; esp值賦給ebp,設定 main函式的棧基址
        ...........             ; 以上兩條指令相當於 enter 0,0
        ...........
        leave                   ; 將ebp值賦給esp,pop先前棧內的上級函式棧的基地址給ebp,恢復原棧基址
        ret                     ; main函式返回,回到上級呼叫

    這些語句就是用來建立和釋放一個函式或者過程的棧框架的。
    原來編譯器會自動在函式入口和出口處插入建立和釋放棧框架的語句。
    函式被呼叫時:
    1) EIP/EBP成為新函式棧的邊界
    函式被呼叫時,返回時的EIP首先被壓入堆疊;建立棧框架時,上級函式棧的EBP被壓入堆疊,與EIP一道行成新函式棧框架的邊界
    2) EBP成為棧框架指標SFP,用來指示新函式棧的邊界
    棧框架建立後,EBP指向的棧的內容就是上一級函式棧的EBP,可以想象,通過EBP就可以把層層呼叫函式的棧都回朔遍歷一遍,偵錯程式就是利用這個特性實現 backtrace功能的
    3) ESP總是作為棧指標指向棧頂,用來分配棧空間
    棧分配空間給函式區域性變數時的語句通常就是給ESP減去一個常數值,例如,分配一個整型資料就是 ESP-4
    4) 函式的引數傳遞和區域性變數訪問可以通過SFP即EBP來實現 
    由於棧框架指標永遠指向當前函式的棧基地址,引數和區域性變數訪問通常為如下形式:
        +8+xx(%ebp)         ; 函式入口引數的的訪問
        -xx(%ebp)           ; 函式區域性變數訪問
            
    假如函式A呼叫函式B,函式B呼叫函式C ,則函式棧框架及呼叫關係如下圖所示:
   	+-------------------------+----> 高地址
   	| EIP (上級函式返回地址)    | 
   	+-------------------------+ 
 +-->   | EBP (上級函式的EBP)      | --+ <------當前函式A的EBP (即SFP框架指標) 
 | 	+-------------------------+   +-->偏移量A 
 | 	| Local Variables         |   |
 | 	| ..........              | --+  <------ESP指向函式A新分配的區域性變數,區域性變數可以通過A的ebp-偏移量A訪問 
 | f 	+-------------------------+
 | r 	| Arg n(函式B的第n個引數)   | 
 | a 	+-------------------------+
 | m 	| Arg .(函式B的第.個引數)   |
 | e 	+-------------------------+
 | 	| Arg 1(函式B的第1個引數)   |
 | o 	+-------------------------+
 | f 	| Arg 0(函式B的第0個引數)   | --+ <------ B函式的引數可以由B的ebp+偏移量B訪問
 | 	+-------------------------+   +--> 偏移量B
 | A 	| EIP (A函式的返回地址)     |   | 
 | 	+-------------------------+ --+ 
 +--- 	| EBP (A函式的EBP)         |<--+ <------ 當前函式B的EBP (即SFP框架指標) 
   	+-------------------------+   |
   	| Local Variables         |   |
   	| ..........              |   | <------ ESP指向函式B新分配的區域性變數
   	+-------------------------+   |
   	| Arg n(函式C的第n個引數)   |   |
   	+-------------------------+   |
   	| Arg .(函式C的第.個引數)   |   |
   	+-------------------------+   +--> frame of B
   	| Arg 1(函式C的第1個引數)   |   |
   	+-------------------------+   |
   	| Arg 0(函式C的第0個引數)   |   |
   	+-------------------------+   |
   	| EIP (B函式的返回地址)     |   |
   	+-------------------------+   |
 +-->   | EBP (B函式的EBP)         | --+ <------ 當前函式C的EBP (即SFP框架指標) 
 |      +-------------------------+
 | 	| Local Variables         |
 | 	| ..........              | <------ ESP指向函式C新分配的區域性變數
 | 	+-------------------------+----> 低地址
frame of C
	
		圖 1-1 
       
    再分析test1反彙編結果中剩餘部分語句的含義:
        
    # mdb test1
    Loading modules: [ libc.so.1 ]
    > main::dis                        ; 反彙編main函式
    main:          pushl   %ebp                            
    main+1:        movl    %esp,%ebp        ; 建立Stack Frame(棧框架)
    main+3:       subl    $8,%esp       ; 通過ESP-8來分配8位元組堆疊空間
    main+6:       andl    $0xf0,%esp    ; 使棧地址16位元組對齊
    main+9:       movl    $0,%eax       ; 無意義
    main+0xe:     subl    %eax,%esp     ; 無意義
    main+0x10:     movl    $0,%eax          ; 設定main函式返回值
    main+0x15:     leave                    ; 撤銷Stack Frame(棧框架)
    main+0x16:     ret                      ; main 函式返回
    >

    以下兩句似乎是沒有意義的,果真是這樣嗎?
        movl    $0,%eax 
        subl     %eax,%esp
       
    用gcc的O2級優化來重新編譯test1.c:
    # gcc -O2 test1.c -o test1
    # mdb test1
    > main::dis
    main:         pushl   %ebp
    main+1:       movl    %esp,%ebp
    main+3:       subl    $8,%esp
    main+6:       andl    $0xf0,%esp
    main+9:       xorl    %eax,%eax      ; 設定main返回值,使用xorl異或指令來使eax為0
    main+0xb:     leave
    main+0xc:     ret
    > 
    新的反彙編結果比最初的結果要簡潔一些,果然之前被認為無用的語句被優化掉了,進一步驗證了之前的猜測。
    提示:編譯器產生的某些語句可能在程式實際語義上沒有用處,可以用優化選項去掉這些語句。

    問題:為什麼用xorl來設定eax的值?
    注意到優化後的程式碼中,eax返回值的設定由 movl $0,%eax 變為 xorl %eax,%eax ,這是因為IA32指令中,xorl比movl有更高的執行速度。

    概念:Stack aligned 棧對齊
    那麼,以下語句到底是和作用呢?
        subl    $8,%esp
       andl    $0xf0,%esp     ; 通過andl使低4位為0,保證棧地址16位元組對齊
       
    表面來看,這條語句最直接的後果是使ESP的地址後4位為0,即16位元組對齊,那麼為什麼這麼做呢?
    原來,IA32 系列CPU的一些指令分別在4、8、16位元組對齊時會有更快的執行速度,因此gcc編譯器為提高生成程式碼在IA32上的執行速度,預設對產生的程式碼進行16位元組對齊

        andl $0xf0,%esp 的意義很明顯,那麼 subl $8,%esp 呢,是必須的嗎?
    這裡假設在進入main函式之前,棧是16位元組對齊的話,那麼,進入main函式後,EIP和EBP被壓入堆疊後,棧地址最末4位二進位制位必定是1000,esp -8則恰好使後4位地址二進位制位為0000。看來,這也是為保證棧16位元組對齊的。

    如果查一下gcc的手冊,就會發現關於棧對齊的引數設定:
    -mpreferred-stack-boundary=n    ; 希望棧按照2的n次的位元組邊界對齊, n的取值範圍是2-12

    預設情況下,n是等於4的,也就是說,預設情況下,gcc是16位元組對齊,以適應IA32大多數指令的要求。

    讓我們利用-mpreferred-stack-boundary=2來去除棧對齊指令:
      
    # gcc -mpreferred-stack-boundary=2 test1.c -o test1
       
    > main::dis
    main:       pushl   %ebp
    main+1:     movl    %esp,%ebp
    main+3:     movl    $0,%eax
    main+8:     leave
    main+9:     ret
    > 

    可以看到,棧對齊指令沒有了,因為,IA32的棧本身就是4位元組對齊的,不需要用額外指令進行對齊。
    那麼,棧框架指標SFP是不是必須的呢?
    # gcc -mpreferred-stack-boundary=2 -fomit-frame-pointer test1.c -o test
    > main::dis
    main:       movl    $0,%eax
    main+5:     ret
    > 

    由此可知,-fomit-frame-pointer 可以去除SFP。
       
    問題:去除SFP後有什麼缺點呢?
       
    1)增加調式難度
        由於SFP在偵錯程式backtrace的指令中被使用到,因此沒有SFP該除錯指令就無法使用。
    2)降低彙編程式碼可讀性
        函式引數和區域性變數的訪問,在沒有ebp的情況下,都只能通過+xx(esp)的方式訪問,而很難區分兩種方式,降低了程式的可讀性。
       
    問題:去除SFP有什麼優點呢?
       
    1)節省棧空間
    2)減少建立和撤銷棧框架的指令後,簡化了程式碼
    3)使ebp空閒出來,使之作為通用暫存器使用,增加通用暫存器的數量
    4)以上3點使得程式執行速度更快

    概念:Calling Convention  呼叫約定和 ABI (Application Binary Interface) 應用程式二進位制介面
         
        函式如何找到它的引數?
        函式如何返回結果?
        函式在哪裡存放區域性變數?
        那一個硬體暫存器是起始空間?
        那一個硬體暫存器必須預先保留?

    Calling Convention  呼叫約定對以上問題作出了規定。Calling Convention也是ABI的一部分。
    因此,遵守相同ABI規範的作業系統,使其相互間實現二進位制程式碼的互操作成為了可能。
    例如:由於Solaris、Linux都遵守System V的ABI,Solaris 10就提供了直接執行Linux二進位制程式的功能。
    詳見文章:
關注: Solaris 10的10大新變化 
             
3. 小結
    本文通過最簡的C程式,引入以下概念:
        SFP 棧框架指標
        Stack aligned 棧對齊
        Calling Convention  呼叫約定 和 ABI (Application Binary Interface) 應用程式二進位制介面
    今後,將通過進一步的實驗,來深入瞭解這些概念。通過掌握這些概念,使在彙編級除錯程式產生的core dump、掌握C語言高階除錯技巧成為了可能。

相關推薦

LINUX平臺可以GDB進行彙編除錯

如果在Linux平臺可以用gdb進行反彙編和除錯。(轉) 2. 最簡C程式碼分析     為簡化問題,來分析一下最簡的c程式碼生成的彙編程式碼:     # vi test1.c            int main()     {         return 0;     }            

在windowslinux之間SecureCRT來上傳下載文件

命令行工具 鏈接 關閉 默認目錄 usr 按鈕 eas add 編譯安裝 SecureCRT可以使用linux下的zmodem協議來快速的傳送文件,使用非常方便.具體步驟:一.在使用SecureCRT上傳下載之前需要給服務器安裝lrzsz:A:CentOS中使用yum安裝即

linuxgdb實現程式宕機時自動列印呼叫堆疊

linux下程式執行幾天莫名其妙宕機了,不能還原現場,找到宕機原因就很無語了。 一個解決辦法是使用core檔案,但是對於大型伺服器檔案,動輒幾百M的core檔案是在有點傷不起,於是想到程式宕機時自動列印呼叫堆疊。簡單實用。

Ubuntu18.04/Linux下安裝DosBox進行8086彙編

筆者由於學習需求,最近需要使用DosBox進行彙編。無奈網上教程均是複製貼上,答非所問,筆者特寫下這篇教程,希望能幫到大家。 軟體準備 DosBox:此次使用虛擬環境的是DosBox,DosBox是

在arm-linuxgdb除錯程式,出現“Program received signal SIGPIPE, Broken pipe”

        出現這種情況大多是因為程式採用CS架構(伺服器/客戶端)在讀寫操作時出現,我第一次也是在這樣的情況下遇到的。首先我們都知道套接字的通訊方式是雙工的,同端即可寫也可讀。而出現Broken pipe這種情況的原因是寫段正在寫入時,另一端已關閉套接字,這樣程序就會向

Amber進行能量分解計算結合自由能——MMPBSA工具

用Amber進行能量分解和計算結合自由能 First release:2018-01-13  Last update: 2018-03-17 Amber是一款適用於生物大分子的動力學模擬的軟體,其官方網站是http://ambermd.org/index.html,目

python進行圖片處理特徵提取

原文來自:http://www.analyticsvidhya.com/blog/2015/01/basics-image-processing-feature-extraction-python/ 毫無疑問,上面的那副圖畫看起來像一幅電腦背景圖片。這些都歸功於我的

LinuxMakefile製作動態庫靜態庫並編譯生成可執行程式

Makefile 一個工程中的原始檔不計其數,其按型別、功能、模組分別放在若干個目錄中,makefile定義了一系列的規則來指定,哪些檔案需要先編譯,哪些檔案需要後編譯,哪些檔案需要重新

Linux平臺C++實現訊號量,同步執行緒

    使用Linux平臺上現有的訊號量sem_t相關的一組API,可以方便地進行執行緒同步。現在用pthread_mutex_t和pthread_cond_t相關的一組API實現訊號量機制。這組API包括:pthread_mutex_init,pthread_cond_init,pthread_mu

linuxudp進行本地通訊

Linux下有協議域af_unix專門用於本機跨程序通訊,在af_unix協議域下通訊地址由傳統的ip:埠號變成一個特殊的檔案。 並且在本地環境下udp不可能出現丟包情況,udp協議快速簡單的特點也適合非常適合本地IPC。 python程式碼: 服務端:

linuxpython進行opencv開發----簡單的圖片操作

初學opencv做的例子程式,儲存一下。 之所以選擇用python,是因為python上手快,開發快。 #!/usr/bin/python2 # coding: utf-8 import cv2 import numpy as np #原始圖片 image = cv

linux下把.so檔案彙編

如果是arm架構的可以這樣。arm-linux-objdump -d libxxx.so > libxxx.S 其中arm-Linux-objdump換成相應的工具字首就行。Android的編譯器都存在 prebuild目錄下, prebuilt\linux-x8

Linux下使用gcc進行靜態編譯使用動態連結庫編譯

/home/plus/demo下有main.c和func.c兩個檔案: func.c: int func(int a) { return a+1; } main.c: #i

linux-windows 通過SecureCRT進行遠端訪問檔案(包含資料夾)傳輸

檔案傳輸 使用SecureCRT自帶的SFTP連線: securecrt 按下ALT+P就開啟新的會話 進行ftp操作。   輸入:help命令,顯示該FTP提供所有的命令   pwd:  查詢Linux主機所在目錄(也就是遠端主機目錄)   lpwd: 查詢本

gdb 檢視,執行彙編程式碼

用gdb 檢視彙編程式碼, 採用disassemble 和 x 命令。 nexti, stepi 可以單步指令執行 如下例: ------------------------------------------------------------ 原始碼: --------

告別 USB, wifi 進行 Android 真機除錯

--------------------- 本文來自 wdeo3601 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/captive_rainbow_/article/details/81012704?utm_source=copy 先看

S3C2440 windows下使用jlink gdbserver,arm-none-eabi-gdb進行裸機程式編寫除錯

一開始是學stm32的,一直用MDK下載除錯程式,非常方便。後來轉學嵌入式Linux,在Linux下進行u-boot和Linux核心的移植,一直沒有用到硬體除錯功能,都是通過列印串列埠資訊或者led來除錯,大部分情況下能夠奏效,這也是因為u-boot和linux核心本身的程

Chrome進行JavaScript的各種除錯詳解

Sources Panel 的左邊是內容源,包括頁面中的各種資源。其中,又分 Sources 和 Content scripts。Sources 就是頁面本身包含的各種資源,它是按照頁面中出現的域來組織的,這是我們要關注的。非同步載入的 js 檔案,在載入後也會出現在這裡的。Content script

STM32CubeMX生成基於Eclipse的GCC工程(一)(工程模板Jlink進行SWD單步除錯

首先,新建STM32Cube工程,在SYS選單下選擇 Serial Wire。 進入工程選單下的設定介面更改Toolchain/IDE為TrueSTUDIO 點選工程-生成程式碼,可以看到生成的檔案 接下來新建一個eclipse空的C工程 新建工程成功後,把STM3

使用Android Studio進行NDK開發除錯(gradle-experimental之官方文件的翻譯說明)

版本更新 環境要求 Gradle(參照三裡邊的版本要求) Android NDK r10e Build Tool在19.0.0以上的SDK Gradle版本要求 不同版本的Experimental Plugin需要不同版本的gradle