2.彙編語言系統調用過程

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

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

在彙編裏頭,一切皆地址。

當我們調用 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

.section .data            
    format: .asciz "%d\n" 
.section .text    
.global _start    
_start:            
    pushl $12            
    pushl $format         
    call printf         
    movl $0, (%esp)            
    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

-g是要加入調試信息

ld的-lc是鏈接libc.a,-I是--dynamic-linker,/lib/ld-linux.so.2

運行

#./test_printf

輸出12

調試

用objdump看看test_printf裏頭的.text section,注意Disassembly of section .text

使用gdb跟蹤,看看上述是否正確

#gdb test_printf

設置斷點到_start

(gdb) break _start

(gdb) run

執行,遇到斷點,停下,eip指着第6行,也就是第一條要執行的push指令

(gdb) info reg

察看寄存器狀況

(gdb) s

執行一步,eip指着下一條指令地址

(gdb) info reg

esp 0xbffff6cc 0xbffff6cc

6cc = 6d0 - 4,對比上一條的esp,小了4,也就是stack增長了4個字節

(gdb) s

(gdb) info reg

esp 0xbffff6c8 0xbffff6c8

6c8 = 6cc - 4,對比上一條的esp,小了4,也就是stack增長了4個字節

(gdb) s

in printf () from /lib/libc.so.6

執行一步,正式進入printf

(gdb) info reg

esp 0xbffff6c4 0xbffff6c4

6c4=6c8-4 新push進去4個字節

(gdb) x /1x $esp
0xbffff6c4: 0x080481c4

esp的棧頂保存的是下一條要執行的代碼的位置,movl的位置,(參考上面objdump的結果)

可以使用bt查看棧幀,下面對比棧變化

(gdb) s

printf出12,已經執行完畢

(gdb) info reg

eax保存着這次printf的返回值,也就是被打印的字符數量,12\n,一共3個字符。

esp恢復到call printf之前的狀態

恢復eip

(gdb) s

執行movl指令,下一條是call exit

(gdb) x /1x $esp

esp並沒有增長,因爲printf之前的數據已經沒用了,我沒有把他們pop出來,而是直接用新的數據刷寫esp所指的內存。

(gdb) s
(gdb) s

正常退出

關於EIP、ESP、EBP寄存器

1.EIP寄存器裏存儲的是CPU下次要執行的指令的地址。

也就是調用完fun函數後,讓CPU知道應該執行main函數中的printf("函數調用結束")語句了。

2.EBP寄存器裏存儲的是是棧的棧底指針,通常叫棧基址這個是一開始進行fun()函數調用之前,由ESP傳遞給EBP的。(在函數調用前你可以這麼理解:ESP存儲的是棧頂地址,也是棧底地址。)

3.ESP寄存器裏存儲的是在調用函數fun()之後,棧的棧頂。並且始終指向棧頂。

堆棧是一種簡單的數據結構,是一種只允許在其一端進行插入或刪除的線性表。
允許插入或刪除操作的一端稱爲棧頂,另一端稱爲棧底,對堆棧的插入和刪除操作被稱入棧和出棧。

有一組CPU指令可以實現對進程的內存實現堆棧訪問。其中,POP指令實現出棧操作,PUSH指令實現入棧操作。
CPU的ESP寄存器存放當前線程的棧頂指針,
EBP寄存器中保存當前線程的棧底指針。
CPU的EIP寄存器存放下一個CPU指令存放的內存地址,當CPU執行完當前的指令後,從EIP寄存器中讀取下一條指令的內存地址,然後繼續執行。

 

參考:http://blog.csdn.net/feng_zh/article/details/7075986
————————————————
版權聲明:本文爲CSDN博主「unix21」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/unix21/article/details/8450155

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