2015-11-30 49 views
3

我已經編寫了一些代碼來了解調用堆棧。我用一些內聯程序集來完成這個任務,以便將參數傳遞給堆棧。我用gcc 4.1.2編譯了它(在CentOS5.4上),它運行良好,然後使用gcc 4.8.4(在Ubuntu14.04.3上)編譯並運行程序,但它總是崩潰。基於ebp的尋址和esp尋址的區別

我發現在引用變量的方式上存在差異。局部變量使用gcc 4.1.2(CentOS5.4)中的EBP寄存器尋址,而局部變量使用gcc 4.8.4(Ubuntu14.04.3)中的ESP寄存器尋址。這似乎是它崩潰的原因。

我的問題是,我該如何控制gcc是否使用EBP或ESP?另外,他們之間有什麼區別?

這裏是C代碼:

double fun(double d) { 
    return d; 
} 

int main(void) { 
    double a = 1.6; 
    double (*myfun)() = fun; 
    asm volatile("subl $8, %esp\n" 
       "fstpl (%esp)\n"); 
    myfun(); 
    asm volatile("addl $8, %esp\n"); 
    return 0; 
} 

這裏是GCC 4.1.2大會,和它的作品

int main(void) { 
    **......** 

double a = 1.6; 
    0x080483bf <+17>:  fldl 0x80484d0 
    0x080483c5 <+23>:  fstpl -0x18(%ebp) 

double (*myfun)() = fun; 
    0x080483c8 <+26>:  movl $0x8048384,-0xc(%ebp) 

asm volatile("subl $8, %esp\n" 
      "fstpl (%esp)\n");     
    0x080483cf <+33>:  sub $0x8,%esp 
    0x080483d2 <+36>:  fstpl (%esp)   

myfun(); 
    0x080483d5 <+39>:  mov -0xc(%ebp),%eax 
    0x080483d8 <+42>:  call *%eax 
    0x080483da <+44>:  fstp %st(0) 

asm volatile("addl $8, %esp\n"); 
    0x080483dc <+46>:  add $0x8,%esp 

    **......** 

這裏是組件的gcc 4.8.4。這是崩潰了:

int main(void) { 
    **......** 

double a = 1.6; 
    0x0804840d <+9>: fldl 0x80484d0 
    0x08048413 <+15>: fstpl 0x8(%esp) 

double (*myfun)() = fun; 
    0x08048417 <+19>: movl $0x80483ed,0x4(%esp) 

asm volatile("subl $8,%esp\n" 
      "fstpl (%esp)\n"); 
    0x0804841f <+27>: sub $0x8,%esp 
    0x08048422 <+30>: fstpl (%esp)  

myfun(); 
    0x08048425 <+33>: mov 0x4(%esp),%eax 
    0x08048429 <+37>: call *%eax 
    0x0804842b <+39>: fstp %st(0) 

asm volatile("addl $8,%esp\n"); 
    0x0804842d <+41>: add $0x8,%esp 
    **......** 
+0

昨天我粘貼了錯誤的代碼,我很抱歉浪費你所有的時間。代碼現在正在更新。 –

回答

2

有使用espebp,除了esp變化與pushpopcallret,這有時使得很難知道某一局部變量或參數位於堆棧之間沒有真正的區別。這就是爲什麼ebp得到加載esp,所以有一個穩定的參考點來引用函數參數和局部變量。

對於這樣一個函數:

int foo(int arg) { 
    int a, b, c, d; 
    .... 
} 

以下組件通常產生:

# using Intel syntax, where `mov eax, ebx` puts the value in `ebx` into `eax` 
.intel_syntax noprefix 

foo:               
    push ebp   # preserve 
    mov ebp, esp  # remember stack 
    sub esp, 16  # allocate local variables a, b, c, d 

    ... 

    mov esp, ebp  # de-allocate the 16 bytes 
    pop ebp   # restore ebp 
    ret 

調用此方法(foo(0))將產生這樣的事情:

pushd 0   # the value for arg; esp becomes esp-4 
    call foo   
    add esp, 4  # free the 4 bytes of the argument 'arg'. 

call指令執行後立即執行,在執行foo方法的第一條指令之前,[esp]將保存返回地址,並且[esp+4]0值爲arg

在方法foo,如果我們想加載argeax(在...) 我們可以使用:

mov eax, [ebp + 4 + 4] 

因爲[ebp + 0]持有ebp以前的值(從push ebp), 和[ebp + 4] (原始值爲esp),保留返回地址。

但是,我們也可以參考使用esp參數:

mov eax, [esp + 16 + 4 + 4] 

我們加入16因爲sub esp, 16,那麼4因爲push ebp,另有4跳過的返回地址,以arg到達。

同樣訪問四個局部變量可以通過兩種方式來完成:

mov eax, [ebp - 4] 
    mov eax, [ebp - 8] 
    mov eax, [ebp - 12] 
    mov eax, [ebp - 16] 

mov eax, [esp + 12] 
mov eax, [esp + 8] 
mov eax, [esp + 4] 
mov eax, [esp + 0] 

但是,每當esp變化,這些指令必須藏漢改變。所以,最後,使用esp還是ebp並不重要。由於您不必push ebp; mov ebp, esp; ... mov esp, ebp; pop ebp,因此使用esp可能會更有效。


UPDATE

據我所知,有沒有辦法保證你的內聯彙編將工作:在Ubunty在gcc 4.8.4優化了使用ebp和參考用一切esp。它不知道你的內聯彙編修改了esp,所以當它試圖調用myfun()時,它從[esp + 4]中獲取它,但它應該從[esp + 4 + 8]中獲取它。

這是一個解決方法:不要在使用內聯彙編的函數中使用局部變量(或參數),而內聯彙編不會執行堆棧操作。要直接繞過鑄造double fun(double)的問題double fn()通話功能組件:

void my_call() {  
    asm volatile("subl $8, %esp\n" 
       "fstpl (%esp)\n" 
       "call fun\n" 
       "addl $8, %esp\n"); 
} 

int main(void) { 
    my_call(); 
    return 0; 
} 

你也可以放置my_call功能在一個單獨的.s(或.S)文件:

.text 
.global my_call 
my_call: 
    subl $8, %esp 
    fstpl (%esp) 
    call fun 
    addl $8, %esp 
    ret 

,並用C :

extern double my_call(); 

您還可以通過fun作爲ar gument:

extern double my_call(double (*myfun)()); 
... 
    my_call(fun); 

.text 
.global my_call 
my_call: 
    sub $8, %esp 
    fstp (%esp) 
    call *12(%esp) 
    add $8, %esp 
    ret 
+0

感謝您的回答,我昨天粘貼了錯誤的代碼,我很抱歉浪費您的時間。我只是想通過內聯彙編傳遞參數,所以我在調用函數之前展開了堆棧,之後我還原了堆棧。我在內聯程序集中更改了** esp **,因此如果** myfun **由** esp **尋址,則它必須是錯誤的地址,但是如果** ebp **是基址寄存器,則一切正常! –

+0

是的,我想知道'sub,fstp,add' :-)我添加了一個更新的答案,我希望你覺得有用! – Kenney

+0

它真的幫助我,感謝很多:-) –

1

如果您想了解棧和參數傳遞約定(ABI),我建議你看一下由編譯器生成的程序集。爲此,您可以交互地在這個網站:http://gcc.godbolt.org/#

嘗試不同的參數類型,varadic功能,傳遞和返回花車,雙打,不同尺寸的結構......

使用內聯彙編與堆棧梅辛太難並且不可預測。這很可能會以很多方式失敗,你不會學到任何有用的東西。

+0

謝謝,這個網站很有幫助 –

+0

@MartinDong:你可以請upvote並檢查答案已被接受,所以這個問題被認爲是解決? – chqrlie

0

ebp通常用於幀指針。用於使用幀指針的函數的第一指令是

 push ebp   ;save ebp 
     mov  ebp,esp  ;ebp = esp 
     sub  esp,...  ;allocate space for local variables 

然後參數和局部變量是從EBP +/-偏移

大多數編譯器有一個選項,不使用幀指針,在此情況下尤被用作基指針。如果非幀指針代碼使用ebp作爲通用寄存器,則仍然需要保存。

1

大多數編譯器創建基於EBP堆棧幀。或者,至少他們曾經這樣做過。這是大多數人教授的使用EBP作爲固定基準幀指針的方法。

一些編譯器創建基於ESP的堆棧幀。原因很簡單。它釋放EBP以用於其他用途,並消除了設置和恢復堆棧幀的開銷。由於堆棧指針可以不斷變化,因此顯然難以看清。

你遇到的問題可能是因爲你正在調用使用stdcall調用約定的API,當它們返回給調用者時,它會無意中拋棄你的堆棧。 EBP必須由被調用者通過cdecl和stdcall函數來保存。但是,stdcall例程將清理堆棧,從而縮小其大小。被叫方必須對這些類型的不幸事件進行補償,並在呼叫返回後重新分配堆棧上的空間。

GCC具有選項-fomit-frame-pointer,它將關閉基於EBP的幀。相反,我不知道是什麼選擇。