(轉)詳解匯編系統調用過程(以printf為例)


本文以printf為例,詳細解析一個簡單的printf調用里頭,系統究竟做了什么,各寄存器究竟如何變化。

 

環境:

linux + gnu as assembler + ld linker

 

如何在匯編調用glibc的函數?其實也很簡單,根據c convention call的規則,參數反向壓棧,call,然后結果保存在eax里頭。注意,保存的是地址。

在匯編里頭,一切皆地址。(別糾結這個,別告訴我還有立即數……主要是要有一切皆地址的思想)

 

例如這個printf,在C里頭,我們用得很多

int printf(const char *format, ...) 這里值得一提的是這個“...”是不定參數,也就是說后面有多少個參數,函數定義里頭沒有規定,感興趣的可以google一下va_list相關的知識,這里就不展開了。

 

但是匯編怎么知道處理這個的呢?這里給個簡單的解釋,感興趣的可以google一下“c convention call”了解更詳細跟專業的解釋。

例如當我們調用 result = printf( "%d %d", 12, a )的時候,編譯器默認是這樣處理的(除非函數定義聲明了pascal call)。

在棧里頭,先一次push a的地址,還有12這個立即數,再push "%d %d"這個字符串的地址,內存模型如下,x86的esp是往下增長的。

(這里是buttom,往下增長的是top)

&a

12

address of "%d %d"

-------------------------------------------(esp 指着這里 ,我們假設地址是4字節,12這個數也是4字節)

當call printf的時候,首先,push當前的eip入esp,解析esp+4所指的"%d %d",因為%d這樣的特定字符都定義了后面每個參數的大小,所以只要解析“%d %d”,我們就可以知道棧里頭參數的情況,例如esp+4+4就是一個int,esp+4+4+4是另外一個int。

當返回的時候,先pop到eip,也就是把eip還原到call之后馬上要執行的機器碼,這時,esp就指着“%d %d”,esp+4指着12,esp+8指着a的地址。esp里頭的內容怎么處理,看需要吧,你也可以pop出來,也可以不pop。但為了效率着想,如果空間夠用,通常不pop,直接用mov指令把下一次要用的參數move進去。返回指儲存在eax里頭。

 

這也一定程度上解釋了為什么c convention call是反向壓棧,這樣編譯器處理起來方便,特別對於這些va_list,因為va_list后面不能繼續跟參數,va_list一定出現在函數的末尾,如果是對printf這類的函數使用pascal call,也就是參數正向壓棧,匯編級別處理起來就特別麻煩了。

 

眼見為實,下面就用匯編寫一個調用printf的,並用gdb跟蹤寄存器,看看是否是上述的一樣。

文件:test_printf.s

 

[plain]  view plain copy
 
  1. .section .data  
  2.         format: .asciz "%d\n"  
  3. .section .text  
  4. .global _start  
  5. _start:  
  6.         pushl $12  
  7.         pushl $format  
  8.         call printf  
  9.         movl $0, (%esp)  
  10.         call exit  



使用如下命令編譯,鏈接

$ as -g test_printf.s -o test_printf.o

$ ld -lc -I /lib/ld-linux.so.2 test_printf.o -o test_printf

as加入-g是要加入調試信息,ld的-lc是鏈接libc.a,-I是--dynamic-linker,/lib/ld-linux.so.2這個要看各人系統情況。鏈接libc跟ld庫之后,生成test_printf

執行

$ ./test_printf

12

輸出12,正常退出。

 

 先用objdump看看test_printf里頭的.text section

$ objdump -d test_printf

 

[plain]  view plain copy
 
  1. Disassembly of section .text:  
  2.   
  3.   
  4. 080481c0 <_start>:  
  5.  80481c0:   6a 0c                   push   $0xc  
  6.  80481c2:   68 cc 92 04 08          push   $0x80492cc  
  7.  80481c7:   e8 d4 ff ff ff          call   80481a0 <printf@plt>  
  8.  80481cc:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)  
  9.  80481d3:   e8 d8 ff ff ff          call   80481b0 <exit@plt>  



 

 

下面使用gdb跟蹤一下,看看上述是否正確。

$ gdb test_printf

 

(gdb) b _start  //設置斷點到_start,主函數入口
Breakpoint 1 at 0x80481c0: file test_printf.s, line 7.
(gdb) run  //執行,遇到斷點,停下,eip指着第7行,也就是第一條要執行的push指令
Starting program: /home/fengzh/research/c_and_asm/printf/test_printf 


Breakpoint 1, _start () at test_printf.s:7
warning: Source file is more recent than executable.

7 pushl $12

(gdb) info reg   //察看寄存器狀況,這里只顯示需要注意的寄存器
esp            0xbffff870 0xbffff870
eip            0x80481c0 0x80481c0 <_start>  //指着第一條指令地址

 


(gdb) s  //執行一步,eip指着下一條指令地址
8 pushl $format
(gdb) info reg
esp            0xbffff86c 0xbffff86c  // 86c = 870 - 4,對比上一條的esp,小了4,也就是stack增長了4個字節
eip            0x80481c2 0x80481c2 <_start+2>

 


(gdb) s //執行一步,下一條就是printf系統調用
9 call printf
(gdb) info reg
esp            0xbffff868 0xbffff868  // 868 = 86c - 4,增長了4個字節
eip            0x80481c7 0x80481c7 <_start+7>

 

//////////重點來了

(gdb) s
0xb7e91110 in printf () from /lib/libc.so.6  //執行一步,正式進入printf
(gdb) info reg
esp            0xbffff864 0xbffff864  // 864 = 868 - c,新push進去4個字節
eip            0xb7e91110 0xb7e91110 <printf> 

(gdb) x /1x $esp
0xbffff864: 0x080481cc  // esp的棧頂保存的是下一條要執行的代碼的位置,movl的位置,(參考上面objdump的結果)

 

(gdb) s  //執行一步,printf已經執行完畢,
Single stepping until exit from function printf,
which has no line number information.
12   //這個是printf的輸出
_start () at test_printf.s:10
10 movl $0, (%esp)
(gdb) info reg
eax            0x3                 3   // eax保存着這次printf的返回值,也就是被打印的字符數量,12\n,一共3個字符。
esp            0xbffff868 0xbffff868  // esp恢復到call printf之前的狀態
eip            0x80481cc 0x80481cc <_start+12>   //恢復eip

 

(gdb) s  //執行movl指令,下一條是call exit
11 call exit
eax            0x3 3
esp            0xbffff868 0xbffff868
eip            0x80481d3 0x80481d3 <_start+19>

(gdb) x /1x $esp   
0xbffff868: 0x00000000   //esp並沒有增長,因為printf之前的數據已經沒用了,我沒有把他們pop出來,而是直接用新的數據刷寫esp所指的內存

 

(gdb) s
0xb7e77c80 in exit () from /lib/libc.so.6
(gdb) s
Single stepping until exit from function exit,
which has no line number information.
[Inferior 1 (process 1609) exited normally]

 

正常退出。一切都如上述。

 

經過這個簡單的printf,我們可以清楚知道在一個glibc調用里頭,匯編層面究竟是怎么做的,具體都做了些什么。

有了這個基礎,如果各位想開發一門新語言,需要處理multiple return value的情況,就知道怎么做了。

例如,我需要處理這個函數[ a, b ] = function()

這個函數需要返回a跟b兩個值。在c語言里頭,構造一個struct,或者構造一個array,都是可行的。但是代碼上看着就比較惡心,處理起來也麻煩。c語言返回值就只有一個,所以用一個eax就足夠了,要么一個int,要么一個double,要么就一個地址,無論哪種情況,就1個寄存器就足夠了(浮點型使用專門的st寄存器)

而如果是新的編譯器需要處理這中語言,怎么做呢?在push參數之前,先push return value的address進去esp

例如

push a

push b

push parameter

在轉跳函數里頭,計算出參數a跟b的地址,之后把返回之存儲到a跟b里頭。就可以了。或者用eax,ebx之類的構造一個stack(這個我不大清楚是否可以,不過按照esp的思路,邏輯上應該是行得通的。)

希望對大家有用。

 


免責聲明!

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



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