2015-10-24 141 views
0

在研究C代碼的拆卸時,這讓我感到震驚。通常,在保存幀指針之後的函數組裝中,我們推送被保存的寄存器並在返回之前將其恢復。 x86 ABI告訴我們哪些寄存器是被調用者/調用者保存的。然而,當我看到編譯器在組裝這些函數時行爲不同時,我的問題就開始了。例如:x86彙編中的序言和推送被調用者保存寄存器

Case 1 
    (gdb) disassemble EVP_CipherInit_ex 
    Dump of assembler code for function EVP_CipherInit_ex: 
     0xb1258044 <+0>:  push %ebp 
     0xb1258045 <+1>:  mov %esp,%ebp 
     0xb1258047 <+3>:  push %edi 
     0xb1258048 <+4>:  push %esi 
     0xb1258049 <+5>:  push %ebx 

Case 2 
    (gdb) disassemble FIPS_mode 
    Dump of assembler code for function FIPS_mode: 
     0xb12614c4 <+0>:  push %ebp 
     0xb12614c5 <+1>:  mov %esp,%ebp 
     0xb12614c7 <+3>:  push %ebx 
     0xb12614c8 <+4>:  sub $0x4,%esp 

Case 3 
    (gdb) disassemble OPENSSL_init 
    Dump of assembler code for function OPENSSL_init: 
     0xb124fae4 <+0>:  push %ebp 
     0xb124fae5 <+1>:  mov %esp,%ebp 
     0xb124fae7 <+3>:  push %ebx 
     0xb124fae8 <+4>:  sub $0x4,%esp 

Case 4 
    (gdb) disassemble FIPS_module_mode 
    Dump of assembler code for function FIPS_module_mode: 
     0xb117dfdc <+0>:  push %edi 
     0xb117dfdd <+1>:  push %esi 
     0xb117dfde <+2>:  push %ebx 
     0xb117dfdf <+3>:  sub $0x10,%esp 

Q1。在前三種情況下,我們保存了幀指針ebp,以及另一個常用寄存器ebx但其餘部分不同。編譯器如何識別要推送哪些文件以及要避免哪些文件?這是一種優化遊戲嗎?任何關於此的指針都會非常有幫助。 Q2302。在拆卸FIPS_module_mode我們甚至沒有保存幀指針ebp。我知道我們可以通過使用編譯器選項優化來節省空間。我的興趣在於理解這種幀指針部分的缺失是由於顯式編譯器優化還是由於某些其他參數有助於決定這一點。

Q3。像gdb這樣的調試器如何在特定的函數中檢測到情況4,在覈心轉儲中省略了幀指針?

的張貼的函數的原型:

int FIPS_module_mode(void); 
void OPENSSL_init(void); 
int EVP_CipherInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, 
         ENGINE *impl, const unsigned char *key, 
         const unsigned char *iv, int enc); 
int FIPS_mode(void); 

這上NetBSD5運行和信息轉儲由GDB

+1

我們可以看到C嗎?特別是,啓用優化時,靜態函數可能會忽略ABI。 –

+0

也許還需要知道該平臺 - gdb暗示linux,但可能是mingw,而windows和linux調用約定不同 –

+0

@Jon Chesterfield - 所討論的函數都不是靜態的 –

回答

3

Q1進行分析。 gcc(與其他優化編譯器一樣)編譯整個函數,使用盡可能多的被保存的寄存器,但只有需要時纔有用。在gcc完成優化整個函數(或編譯單元或程序)之前,不會生成asm,因此gcc知道它在發佈序言時需要多少個寄存器。

它使用的任何被保存的被保存的寄存器被推入序言中並彈出結尾。在某些函數中,它只使用被調用方保存的寄存器,因爲它沒有保存的調用方保存的寄存器(因此,僅用於寄存器總數)。在非葉函數中,被調用方保存的寄存器也可用於保存call中的某個寄存器,該代碼必須假設所有調用方保存的寄存器都是clobbers。

它看起來像是gcc只需要一個調用保存寄存器,它選擇ebx。不過,它可能會使用(保存/恢復)esi/edi,如果它想使用rep movs或其他東西。

有時gcc的行爲是次優的:一些函數有一個不使用許多本地的快速路徑,但是gcc發出在檢查之前推送的代碼,因此必須再次彈出。 Linux內核提示某些函數爲noinline,以便儘可能快地保持快速路徑,但要以慢速路徑中的額外函數調用爲代價。據我瞭解,這是Linux中noinline的主要原因,而不是代碼大小的膨脹。

Q2。是的,它看起來像FIPS_module_mode編譯爲(這是新gcc中的默認值)。如果您正在查看庫,Makefile(或任何構建系統)可以輕鬆構建具有不同選項的不同文件。或者,即使使用,具有可變大小局部變量的函數也會構建堆棧幀。例如
int func(int c) { int tmp[c]; ...; }

Q3。我很好奇現代調試器如何在沒有幀指針的情況下進行堆棧回溯。 This blog post sheds some light.eh_frame_hdr數據部分有調試信息(未標記爲「調試」信息,因此通常不會被剝離,所以當調用堆棧經過剝離庫中的某個函數時可以回溯)。使用objdump -h查看該部分的大小。該數據還用於在引發運行時異常時解除堆棧,這是不剝離它的另一個原因。

在正常情況下(禁止使堆棧崩潰或編譯器/ asm編程錯誤混淆堆棧指針),它無需幀指針,所以是gcc自4.6以來的默認值,即使是x86。我認爲這是x86-64更長的默認設置。

如果沒有這些信息,您可以掃描堆棧中正確範圍內的值作爲返回地址。

+0

看起來我需要在理解你的答案之前學習一些東西,但是我沒有得到第一個答案的第一行。 「gcc使用盡可能多的被調用者保存的寄存器,因爲它是有用的,但只在需要時......」。我認爲這正是我的問題。 gcc如何知道哪個被保存的寄存器被丟棄,哪些被保存?因爲,非葉函數可以被許多其他函數調用。爲你的努力+1。謝謝。 –

+1

它根據調用約定保留所有內容。但是,如果函數本身不使用它,它不必被推入,因爲它不會改變。 – Jester

+1

@RIPUNJAY:當我說「使用」時,我的意思是它編譯整個函數,使用盡可能多的寄存器,有助於保存所有需要一次生成的值,以便快速生成代碼而不會將本地數據溢出到內存中。如果這個寄存器比寄存器保存的寄存器集合中可用的寄存器更多(即可以不保存地使用),則它在整個功能中保存/恢復一些寄存器。此外,非葉子意味着它*調用*其他功能。所有功能可以有多個呼叫者。單呼叫者功能的主要優化是內聯他們。 –