2015-12-05 25 views
6

我寫了一個簡單的彙編程序:爲什麼在調用printf時覆蓋EDX的值?

section .data 
str_out db "%d ",10,0 
section .text 
extern printf 
extern exit 
global main 
main: 

MOV EDX, ESP 
MOV EAX, EDX 
PUSH EAX 
PUSH str_out 
CALL printf 
SUB ESP, 8 ; cleanup stack 
MOV EAX, EDX 
PUSH EAX 
PUSH str_out 
CALL printf 
SUB ESP, 8 ; cleanup stack 
CALL exit 

我是NASM彙編器和對象文件鏈接到一個可執行Linux上的GCC。

本質上,該程序首先將堆棧指針的值放入寄存器EDX,然後打印該寄存器的內容兩次。但是,在第二次printf調用之後,打印到stdout的值與第一個不匹配。

這種行爲看起來很奇怪。當我用EBX替換該程序中EDX的每個用法時,輸出的整數與預期相同。我只能推斷在printf函數調用期間的某個時刻EDX被覆蓋。

爲什麼會出現這種情況?我如何確保將來使用的寄存器不會與C庫函數衝突?

+2

那也是幾年前的第一次。你接受的答案是正確的,但省略'ebp'和'esp'作爲被調用者保存。這兩個人似乎不言而喻,但你可以從技術上解決這個問題。歡迎大會! – sqykly

+0

@sqykly謝謝。這當然比我習慣的高級語言寬鬆得多。但我不會被它擊敗! :) – Jake

+0

回答儘可能多的javascript問題,你會開始懷疑。 – sqykly

回答

11

按照x86 ABIEBXESIEDI,和EBP是被調用者保存寄存器和EAXECXEDX是呼叫者保存寄存器。

這意味着功能可以自由使用和銷燬以前的值EAX,ECXEDX。 因此,如果您不希望更改其值,請在調用函數之前將值EAX,ECX,EDX保存。這就是「呼叫者保存」的意思。或者更好的辦法是使用其他寄存器來獲得函數調用後仍然需要的值。在函數的開始/結束處推動/彈出EBX比在進行函數調用的循環內部推入/彈出EDX好得多。如果可能,請在呼叫後使用不需要的臨時呼叫的呼叫限制寄存器。已經存在於內存中的值,因此在重新讀取之前不需要寫入,因此泄漏也更便宜。


由於EBXESIEDIEBP都被調用者保存寄存器,功能有恢復值到原來的任何那些他們返回之前修改的。

ESP也被調用保存,但你不能搞砸這件事,除非你在某處複製返回地址。由於現代CPU使用返回地址預測器,所以不匹配的調用/ ret對於性能來說很糟糕。

+2

'EBP'也被稱爲保存! –

+0

這不是*很難。來自沒有參數的函數的'ret 8'會'esp'。圖片任何一種尾部調用優化出錯了。 – sqykly

+0

或!錯誤的cdecl或stdcall。 – sqykly

5

目標平臺(例如32位x86 Linux)的ABI定義了哪些寄存器可以被功能使用而不保存。 (也就是說,如果你想讓他們在一次通話中得到保存,你必須自己做)。

鏈接ABI文檔用於Windows和非窗口,32位和64位,在https://stackoverflow.com/tags/x86/info

有一些寄存器未在調用(可作爲臨時寄存器),保存意味着功能可以更小。簡單的功能通常可以避免進行任何保存/恢復。這減少了指令的數量,導致更快的代碼。

有一些非常重要的:必須跨越調用將所有狀態泄漏到內存中會使非葉子函數的代碼膨脹,並且特別慢。在被調用函數沒有觸及所有寄存器的情況下。

+0

最後一段聽起來很有趣。如果你不得不把所有的狀態保存到內存中,那麼葉函數就是它會膨脹的那些函數。由於它們既是一個調用者又是一個被調用者,所以非葉子函數本質上會膨脹。 –

+0

@DanielStevens:最後一段討論的是所有寄存器被破壞的情況,就像xmm regs在SysV 64bit ABI中一樣。葉函數不需要保存任何東西。另外:非葉函數通常有足夠的被保存寄存器來保存寄存器中的幾個關鍵狀態,並且大多使用調用者保存寄存器作爲暫存空間來計算函數調用參數。如果你在函數調用後仍然需要,你只需要保存/恢復一個reg。通常你需要一對夫婦認爲,像一個循環計數器和一個或兩個指針,但可以重新加載其他東西。 –

+0

但是你在談論把所有的狀態都拋到了記憶中,而不是讓它變得糟糕。 –

相關問題