汇编语言系统调用过程
以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
總結
以上是生活随笔為你收集整理的汇编语言系统调用过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GDB调试--以汇编语言为例
- 下一篇: GDB查看栈信息