簡體   English   中英

x86_64:堆棧幀指針幾乎沒用?

[英]x86_64 : is stack frame pointer almost useless?


  • Linux x86_64。
  • gcc 5.x

我正在研究兩個代碼的輸出,使用-fomit-frame-pointer和without(gcc at“-O3”默認啟用該選項)。

pushq    %rbp
movq     %rsp, %rbp
...
popq     %rbp

我的問題是:

如果我全局禁用該選項,即使是在極端情況下編譯操作系統,是否有一個問題?

我知道中斷使用該信息,那么該選項僅適用於用戶空間嗎?

編譯器總是生成自洽的代碼,因此只要不使用外部/手工制作的代碼(例如依靠rbp的值),禁用幀指針就可以了。

中斷不使用幀指針信息,它們可以使用當前堆棧指針來保存最小上下文,但這取決於中斷和OS的類型(硬件中斷可能使用Ring 0堆棧)。
您可以查看英特爾手冊以獲取更多相關信息。

關於框架指針的用處:
幾年前,在編譯了幾個簡單的例程並查看生成的64位匯編代碼后,我遇到了同樣的問題。
如果你不介意讀我自己為自己寫的很多筆記,那么它們就是。

注意 :詢問某事物的有用性是有點相對的。 編寫當前主要64位ABI的匯編代碼我發現自己使用堆棧幀越來越小。 然而,這只是我的編碼風格和意見。


我喜歡使用框架指針,編寫函數的序言和結尾,但我也喜歡直接不舒服的答案,所以這就是我的看法:

是的,幀指針在x86_64中幾乎沒用

要注意它並非完全無用,特別是對於人類而言,但編譯器不再需要它。 為了更好地理解為什么我們首先有一個幀指針,最好回憶一下歷史。

回到真實模式(16位)

當Intel CPU僅支持“16位模式”時,對如何訪問堆棧有一些限制,特別是這條指令是(並且仍然是)非法的

mov ax, WORD [sp+10h]

因為sp不能用作基址寄存器。 只有少數指定的寄存器可用bx目的,例如bx或更着名的bp
現在不是每個人都注意到的細節,但是bp比其他基址寄存器具有優勢,它隱含地暗示使用ss作為段/選擇器寄存器,就像sp隱式用法(通過pushpop等),以及就像esp在后來的32位處理器上做的那樣。
即使你的程序分散在整個內存中,每個段寄存器指向不同的區域, bpsp也是相同的,畢竟這是設計者的意圖。

因此通常需要堆棧幀,因此需要幀指針。
bp有效地將堆棧分為四個部分: 參數區域, 返回地址舊bp (只是一個WORD)和局部變量區域。 每個區域由用於訪問它的偏移量標識:參數和返回地址為正,舊bp為零,局部變量為負。

擴展有效地址

隨着英特爾CPU的不斷發展,增加了更廣泛的32位尋址模式。
特別是可以使用任何32位通用寄存器作為基址寄存器,這包括使用esp
像這樣的指示

mov eax, DWORD [esp+10h]

現在有效,堆棧框架和框架指針的使用似乎注定要結束。
可能情況並非如此,至少在開始階段。
確實,現在可以完全使用esp但在上述四個區域中堆疊的分離仍然有用,特別是對於人類。

如果沒有幀指針,push或pop將改變相對於esp的參數或局部變量偏移量,從而為初看起來不直觀的代碼提供表單。 考慮如何使用cdecl調用約定實現以下C例程:

void my_routine(int a, int b)
{  
    return my_add(a, b); 
}

沒有和有框架

my_routine:      
  push DWORD [esp+08h]
  push DWORD [esp+08h]
  call my_add
  ret

my_routine:
  push ebp
  mov ebp, esp

  push DWORD [ebp+0Ch]
  push DWORD [ebp+08h]
  call my_add

  pop ebp
  ret 

乍一看似乎第一個版本兩次推送相同的值。 它實際上推動了兩個單獨的參數,因為第一次推送降低了esp,因此相同的有效地址計算將第二次推送指向另一個參數。

如果你添加局部變量(特別是很多),那么情況很快就會變得難以理解: mov eax, [esp+0CAh]是指局部變量還是參數? 使用堆棧幀,我們為參數和局部變量設置了固定的偏移量。

甚至編譯器最初仍然優選使用幀基指針給出的固定偏移量。 我看到這種行為首先用gcc改變。
在調試構建中,堆棧框架有效地增加了代碼的清晰度,使(熟練的)程序員可以輕松地跟蹤正在發生的事情,並且如注釋中所指出的,可以讓他們更容易地恢復堆棧幀。
然而,現代編譯器擅長數學並且可以輕松地保持堆棧指針移動的計數並從esp生成適當的偏移量,省略堆棧幀以便更快地執行。

當CISC需要數據對齊時

在引入SSE指令之前,與RISC兄弟相比,英特爾處理器從未對程序員提出太多要求。
特別是他們從未要求數據對齊,我們可以在不是4的倍數的地址上訪問32位數據而沒有主要的抱怨(取決於DRAM數據寬度,這可能導致延遲增加)。
SSE使用需要在16字節邊界上訪問的16字節操作數,因為SIMD范例在硬件中有效實現並且變得更加流行16字節邊界上的對齊變得重要。

主要的64位ABI現在需要它,堆棧必須在段落上對齊(即16字節)。
現在,我們通常被稱為在序言之后,堆棧是對齊的,但是假設我們沒有幸運的保證,我們需要做其中一個

push rbp                   push rbp
mov rbp, rsp               mov rbp, rsp             

and spl, 0f0h              sub rsp, xxx
sub rsp, 10h*k             and spl, 0f0h

在這些序言之后,堆棧是這樣或那樣的,但是我們不能再使用rbp的負偏移來訪問需要對齊的局部變量,因為幀指針本身沒有對齊。
我們需要使用rsp ,我們可以安排一個序言,其rbp指向局部變量的對齊區域的頂部,但隨后參數將處於未知偏移量。
我們可以安排一個復雜的堆棧幀(可能有多個指針),但舊式幀基指針的關鍵是它的簡單性。

因此,我們可以使用框架指針來訪問堆棧上的參數和局部變量的堆棧指針。
唉,參數傳遞的堆棧的作用已經減少,並且對於少量參數(目前為四個)它甚至沒有被使用,並且將來它可能會被用得更少。

所以我們不使用框架指針(主要是),也不使用參數(大多數),我們使用它的是什么?

  1. 它保存了原始rsp的副本,因此要在函數出口恢復堆棧指針,一個mov就足夠了。 如果堆棧與一個and不可逆的對齊,則需要原始副本。

  2. 實際上,一些ABI保證在標准序言之后堆棧被對齊,從而允許我們像往常一樣使用幀指針。

  3. 某些變量不需要對齊,可以使用未對齊的幀指針訪問,這通常適用於手工制作的代碼。

  4. 某些功能需要四個以上的參數。

摘要

幀指針是16位程序的殘留范例,由於其在訪問局部變量和參數時的簡單性和清晰性,已經證明它本身在32位機器上仍然有用。
然而,在64位機器上,嚴格的要求消除了大部分的簡單性和清晰度,但是幀指針仍然在調試模式中使用。


事實上,幀指針可以用來制作有趣的東西:這是真的,我想,我從來沒有見過這樣的代碼,但我可以想象它是如何工作的。
然而,我專注於幀指針的管家角色,因為這是我一直看到它的方式。
所有瘋狂的事情都可以用任何指針設置為幀指針的相同值來完成,我給后者一個更“特殊”的角色。
例如VS2013有時使用rdi作為“幀指針”,但如果它不使用rbp/ebp/bp ,我不認為它是真正的幀指針。
對我來說,使用rdi意味着幀指針省略優化:)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM