新聞中心

        EEPW首頁 > 嵌入式系統 > 設計應用 > 匯編技術內幕(2)

        匯編技術內幕(2)

        作者: 時間:2016-11-24 來源:網絡 收藏
        問題:為什么用EAX寄存器保存函數返回值?

        實際上IA32并沒有規定用哪個寄存器來保存返回值。但如果反匯編Solaris/Linux的二進制文件,就會發現,都用EAX保存函數返回值。這不是偶然現象,是操作系統的ABI(Application Binary Interface)來決定的。Solaris/Linux操作系統的ABI就是Sytem V ABI。

        本文引用地址:http://www.104case.com/article/201611/320808.htm


        概念: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二進制程序的功能。



        關鍵詞: 匯編技術內

        評論


        技術專區

        關閉
        主站蜘蛛池模板: 青川县| 浠水县| 凤翔县| 济阳县| 汝阳县| 高雄市| 张家港市| 天镇县| 安顺市| 集安市| 池州市| 荔浦县| 若尔盖县| 仁怀市| 阳春市| 富顺县| 咸宁市| 新田县| 上思县| 太和县| 沭阳县| 石景山区| 洪洞县| 黔西县| 寿光市| 景东| 巴塘县| 三门峡市| 福清市| 谢通门县| 恩平市| 昌江| 邹城市| 旅游| 许昌市| 什邡市| 体育| 监利县| 罗田县| 纳雍县| 田林县|