c語言函數調用過程

 原文地址在 http://m.blog.chinaunix.net/uid-7390305-id-2057207.html


這是從別處看到的一篇文章,感覺寫的非常深入細緻,是作者學習彙編語言的筆記,但是我覺得這篇文章對理解C函數調用非常有幫助,於是參考作者的步驟在自己的linux機器上實現了一下, 並對文章做了一些細小的更改,在此對原作者致謝,如果作者覺得這樣有些冒犯的話,請通知我我會立即撤掉。


X86彙編語言學習手記(1)

 

作者: Badcoffee

Email: [email protected]

2004年10月

 

原文出處: http://blog.csdn.net/yayong

版權所有: 轉載時請務必以超鏈接形式標明文章原始出處、作者信息及本聲明.

這是作者在學習X86彙編過程中的學習筆記,難免有錯誤和疏漏之處,歡迎指正。

 

1. 編譯環境

 

   OS: Axianux 1.0
   Compiler: gcc 3..2.3

   Linker: Solaris Link Editors 5.x
   Debug Tool: gdb
   Editor: vi

2. 最簡C代碼分析

爲簡化問題,來分析一下最簡的c代碼生成的彙編代碼:
    # vi test1.c
     
    int main()
    {
        return 0;
    }  
   
     編譯該程序,產生二進制文件:
    # gcc -o start start.c

# file start    

start: ELF 32-bit LSB executable, Intel 80386, version 1(SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), notstripped
     start是一個ELF格式32位小端(Little Endian)的可執行文件,動態鏈接並且符號表沒有去除。這正是Unix/Linux平臺典型的可執行文件格式。

     用gdb反彙編可以觀察生成的彙編代碼:

[wqf@15h166 attack]$ gdb start

GNU gdb Asianux (6.0post-0.20040223.17.1AX)

Copyright 2004 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General PublicLicense, and you are welcome to change it and/or distribute copies of it undercertain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB.  Type "show warranty" for details.

This GDB was configured as"i386-asianux-linux-gnu"...(no debugging symbols found)...Using hostlibthread_db library "/lib/tls/libthread_db.so.1".

 

(gdb) disassemble main          --->反彙編main函數

Dump of assembler code for function main:

0x08048310 <main+0>:    push  %ebp   --->ebp寄存器內容壓棧,即保存main函數的上級調用函數的棧基地址

0x08048311 <main+1>:    mov   %esp,%ebp  ---> esp值賦給ebp,設置main函數的棧基址

0x08048313 <main+3>:    sub   $0x8,%esp  --->通過ESP-8來分配8字節堆棧空間

0x08048316 <main+6>:    and   $0xfffffff0,%esp --->使棧地址16字節對齊

0x08048319 <main+9>:    mov   $0x0,%eax  --->  無意義

0x0804831e <main+14>:   sub   %eax,%esp  --->  無意義

0x08048320 <main+16>:   mov   $0x0,%eax   ---> 設置函數返回值0

0x08048325 <main+21>:   leave    --->將ebp值賦給esp,pop先前棧內的上級函數棧的基地址給ebp,恢復原棧基址.

0x08048326 <main+22>:   ret   --->main函數返回,回到上級調用.

0x08048327 <main+23>:   nop

End of assembler dump.

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

 

問題一:誰調用了 main函數?

在C語言的層面來看,main函數是一個程序的起始入口點,而實際上,ELF可執行文件的入口點並不是main而是_start。

gdb也可以反彙編_start:

 

(gdb)disass _start          --->從_start的地址開始反彙編

Dump of assembler code for function _start:

0x08048264 <_start+0>: xor    %ebp,%ebp

0x08048266 <_start+2>:  pop    %esi

0x08048267 <_start+3>: mov    %esp,%ecx

0x08048269 <_start+5>: and    $0xfffffff0,%esp

0x0804826c <_start+8>: push   %eax

0x0804826d <_start+9>: push   %esp

0x0804826e <_start+10>: push  %edx

0x0804826f <_start+11>: push  $0x8048370

0x08048274 <_start+16>: push  $0x8048328

0x08048279 <_start+21>: push  %ecx

0x0804827a <_start+22>: push  %esi

0x0804827b <_start+23>: push  $0x8048310

0x08048280<_start+28>: call   0x8048254<__libc_start_main>

--->在這裏調用了main函數

0x08048285 <_start+33>: hlt

0x08048286 <_start+34>: nop

0x08048287 <_start+35>: nop

End of assembler dump.  

問題二:爲什麼用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

 

原來編譯器會自動在函數入口和出口處插入創建和釋放棧框架的語句。

        函數被調用時:

        1) EIP/EBP成爲新函數棧的邊界

           函數被調用時,返回時的EIP首先被壓入堆棧;創建棧框架時,上級函數棧的EBP被壓入堆棧,與EIP一道行成新函數棧框架的邊界。        

        2) EBP成爲棧幀指針STP,用來指示新函數棧的邊界
           
棧幀建立後,EBP指向的棧的內容就是上一級函數棧的EBP,可以想象,通過EBP就可以把層層調用函數的棧都回朔遍歷一遍,調試器就是利用這個特性實現backtrace功能的。

        3) ESP總是作爲棧指針指向棧頂,用來分配棧空間
            棧分配空間給函數局部變量時的語句通常就是給ESP減去一個常數值,例如,分配一個整型數據就是 ESP-4。

        4) 函數的參數傳遞和局部變量訪問可以通過STP即EBP來實現
           
由於棧框架指針永遠指向當前函數的棧基地址,參數和局部變量訪問通常爲如下形式:
            +8+xx(%ebp)         :函數入口參數的的訪問

            -xx(%ebp)             :函數局部變量訪問

假如函數A調用函數B,函數B調用函數C ,則函數棧幀及調用關係如下圖所示:

       +----------------------+----> 高地址
       | EIP (上級函數返回地址)  |   
       +----------------------+   
  +--> | EBP (上級函數的EBP)    | --+     <------ 當前函數A的EBP (即STP框架指針) 
  |    +----------------------+   +-->偏移量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 (即STP框架指針) 
       +----------------------+   |
       | 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 (即STP框架指針) 
 |     +----------------------+
 |     | Local Variables      |
 |     |    ..........        |      <------ ESP指向函數C新分配的局部變量
 |     +----------------------+----> 低地址
frame of C

 

 

概念四Stack aligned 棧對齊

      那麼,以下語句到底是和作用呢?

      subl    $8,%esp

      andl    $0xfffffff0,%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被壓入堆棧後,棧地址最末4位必定是0100,esp-8則恰好使後4位地址爲0。看來,這也是爲保證棧16字節對齊的。

如果查一下gcc的手冊,就會發現關於棧對齊的參數設置:

       -mpreferred-stack-boundary=n  ---> 希望棧按照2的n次的字節邊界對齊, n的取值範圍是2-12.

默認情況下,n是等於4的,也就是說,默認情況下,gcc是16字節對齊,以適應IA32大多數指令的要求。

讓我們利用-mpreferred-stack-boundary=2來去除棧對齊指令:

     
(gdb) disass main

Dump of assembler code for function main:

0x08048310 <main+0>:   push   %ebp

0x08048311 <main+1>:   mov    %esp,%ebp

0x08048313 <main+3>:   mov    $0x0,%eax

0x08048318 <main+8>:    leave

0x08048319 <main+9>:    ret

0x0804831a <main+10>:   nop

0x0804831b <main+11>:   nop

End of assembler dump.

可以看到,棧對齊指令沒有了,因爲,IA32的棧本身就是4字節對齊的,不需要用額外指令進行對齊。

 

問題五:棧框架指針STP是不是必須的呢?

[wqf@15h166attack]$ gcc -mpreferred-stack-boundary=2-fomit-frame-pointer start.c -o start

[wqf@15h166attack]$ gdb  start

(gdb) disass main

Dump of assembler code forfunction main:

0x08048310 <main+0>:    mov   $0x0,%eax

0x08048315 <main+5>:    ret

0x08048316 <main+6>:    nop

0x08048317 <main+7>:    nop

End of assembler dump.

由此可知,-fomit-frame-pointer 可以去除STP。

 

去除STP後有什麼缺點呢?

      1)增加調式難度

          由於STP在調試器backtrace的指令中被使用到,因此沒有STP該調試指令就無法使用。

       2)降低彙編代碼可讀性

          函數參數和局部變量的訪問,在沒有ebp的情況下,都只能通過+xx(esp)的方式訪問,而很難區分兩種方式,降低了程序的可讀性。

去除STP有什麼優點呢?

       1)節省棧空間。

      2)減少建立和撤銷棧框架的指令後,簡化了代碼。

      3)使ebp空閒出來,使之作爲通用寄存器使用,增加通用寄存器的數量
       4)以上3點使得程序運行速度更快。

 

概念六:Calling Convention 調用約定和ABI (Application Binary Interface) 應用程序二進制接口。

       函數如何找到它的參數?

       函數如何返回結果?

       函數在哪裏存放局部變量?

        哪一個硬件寄存器是起始空間?

        哪一個硬件寄存器必須預先保留?

Calling Convention 調用約定對以上問題作出了規定。CallingConvention也是ABI的一部分。因此,遵守相同ABI規範的操作系統,使其相互間實現二進制代碼的互操作成爲了可能。

例如:由於Solaris、Linux都遵守System V的ABI,Solaris10就提供了直接運行Linux二進制程序的功能。

3. 小結

本文通過最簡的C程序,引出以下概念:

STP 棧框架指針

Stack aligned 棧對齊

Calling Convention  調用約定 和 ABI (Application Binary Interface) 應用程序二進制接口

今後,將通過進一步的實驗,來深入瞭解這些概念。通過掌握這些概念,使在彙編級調試程序產生的core dump、掌握C語言高級調試技巧成爲了可能。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章