浅谈栈和栈帧(一)
一個碼農要是沒遇見過coredump,那他就是神仙了。core file(coredump的轉儲文件)中保存的最重要內容之一,就是函數的call trace。還原這部分內容(棧回溯),并與原代碼對應上,盡快找出程序崩潰的位置和原因,是碼農們一生的責任。當然,你如果有良好的開發環境和開發習慣,保留了現場環境(core file and lib file等)和unstrip的原程序,那么恭喜,也許你不用太費神,直接用GDB的backtrace功能,就可以找到癥結所在。當然如果棧被沖掉了一部分,backtrace出來的就是一堆問號,要找出call trace就不容易了。這在緩沖區溢出時經常碰到。
? ? 好了廢話少說,切入正題,先談與call trace密切相關的棧和棧幀概念。
1. ?棧和棧幀
? ??棧(stack)相對整個系統而言,調用棧(Call stack)相對某個進程而言,棧幀(stack frame)則是相對某個函數而言,調用棧就是正在使用的棧空間,由多個嵌套調用函數所使用的棧幀組成。具體來說,Call stack就是指存放某個程序的正在運行的函數的信息的棧。Call stack 由 stack frames 組成,每個 stack frame 對應于一個未完成運行的函數。 ? ? 在當今多數計算機體系架構中,函數的參數傳遞、局部變量的分配和釋放都是通過操縱棧來實現的。棧還用來存儲返回值信息、保存寄存器以供恢復調用前處理機狀態。每次調用一個函數,都要為該次調用的函數實例分配棧空間。為單個函數分配的那部分棧空間就叫做 stack frame,或者這樣說,stack frame 這個說法主要是為了描述函數調用關系的。 ? ? Stack frame 組織方式的重要性和作用體現在兩個方面: ? ? 第一,它使調用者和被調用者達成某種約定。這個約定定義了函數調用時函數參數的傳遞方式,函數返回值的返回方式,寄存器如何在調用者和被調用者之間進行共享; ? ? 第二,它定義了被調用者如何使用它自己的 stack frame 來完成局部變量的存儲和使用。
2. ? ?壓棧和出棧?
? ? 在 RISC 計算機中主要參與計算的是寄存器,saved registers 就是指在進入一個函數后,如果某個保存原函數信息的寄存器會在當前函數中被使用,就應該將此寄存器保存到堆棧上,當函數返回時恢復此寄存器值。而且由于 RISC 計算機大部分采用定長指令或者定變長指令,一般指令長度不會超過32個位。而現代計算機的內存地址范圍已經擴展到 32 位甚至64位,這樣在一條指令里就不足以包含有效的內存地址,所以RISC計算機一般借助于一個返回地址寄存器 RA(return address) 來實現函數的返回。幾乎在每個函數調用中都會使用到這個寄存器,所以在很多情況下 RA 寄存器會被保存在堆棧上以避免被后面的函數調用修改,當函數需要返回時,從堆棧上取回 RA 然后跳轉。
? ? 移動 SP 和保存寄存器的動作一般處在函數的開頭,這個壓棧過程也叫做 function prologue;恢復這些寄存器狀態的動作一般放在函數的最后,出棧過程也叫做 function epilogue。壓棧出棧指令各個CPU也不相同。
? ??Stack Frame 中所存放的內容和存放順序,則由目標體系架構的調用約定(calling convention)定義。下面,我們看看圖形化的函數棧幀,以棧向下增長為例來了解幾種常用CPU的stack frame 組織方式。
?
3. ?MIPS棧幀
? ??先看MIPS的棧幀布局圖: ? ?? ? ? 此圖描述的是一種典型的MIPS stack frame 組織方式。在這張圖中,sp(stack pointer)/s8(棧基址,又稱fp)?就是當前函數的棧指針,它指向棧頂的位置。Callee Frame 所示即為當前函數(被調用者)的 frame,Caller Frame 是當前函數的調用者的 frame。general register area根據需要保存ra、gp、s8等caller的寄存器信息,保存位置和順序暫時沒有發現明確的規律。 ? ? 在MIPS這種沒有BP(base pointer) 寄存器的目標架構中,進入一個函數時需要將當前棧指針向下移動 n個byte(字節),這個大小為n個byte?的存儲空間就是此函數的 stack frame 的存儲區域。此后棧指針便不再移動,只能在函數返回時再將棧指針加上這個偏移量恢復棧現場。由于不能隨便移動棧指針,所以寄存器壓棧和出棧都必須指定偏移量,這與 x86 架構的計算機對棧的使用方式有著明顯的不同。所以,MIPS一般是sp跟s8一致,只有調用alloca或動態數組后fp才會移到新的frame邊界。 ? ? 另外提一下兩個重要的寄存器。MIPS有個寄存器t9,專門用來跳轉子函數時預裝子函數地址。跳轉子函數的匯編命令是:jalr t9。這條命令后面8個byte的地址就是子函數棧幀中保留的ra值。MIPS的寄存器gp則用來存放某些變量或GOT信息,它的獲取是在函數的開頭三條語句完成,計算公式:gp = x <<16 + y?+ t9。三條語句是: ? ? ? ? lui ? ? ? ?gp, x
? ? ? ??addiu ? gp, gp, y?
? ? ? ??addu ? ?gp, gp, t9 ? ? ? ? ?#t9就是當前函數的地址,它永遠保留著最后一個被調用函數的地址 ? ??MIPS的壓棧出棧指令分別是ld和sd。?
? ? 今天先說到這,后面補上x86、ARM、PowerPC的圖形化棧幀解說。
1. ?棧和棧幀
? ??棧(stack)相對整個系統而言,調用棧(Call stack)相對某個進程而言,棧幀(stack frame)則是相對某個函數而言,調用棧就是正在使用的棧空間,由多個嵌套調用函數所使用的棧幀組成。具體來說,Call stack就是指存放某個程序的正在運行的函數的信息的棧。Call stack 由 stack frames 組成,每個 stack frame 對應于一個未完成運行的函數。 ? ? 在當今多數計算機體系架構中,函數的參數傳遞、局部變量的分配和釋放都是通過操縱棧來實現的。棧還用來存儲返回值信息、保存寄存器以供恢復調用前處理機狀態。每次調用一個函數,都要為該次調用的函數實例分配棧空間。為單個函數分配的那部分棧空間就叫做 stack frame,或者這樣說,stack frame 這個說法主要是為了描述函數調用關系的。 ? ? Stack frame 組織方式的重要性和作用體現在兩個方面: ? ? 第一,它使調用者和被調用者達成某種約定。這個約定定義了函數調用時函數參數的傳遞方式,函數返回值的返回方式,寄存器如何在調用者和被調用者之間進行共享; ? ? 第二,它定義了被調用者如何使用它自己的 stack frame 來完成局部變量的存儲和使用。
2. ? ?壓棧和出棧?
? ? 在 RISC 計算機中主要參與計算的是寄存器,saved registers 就是指在進入一個函數后,如果某個保存原函數信息的寄存器會在當前函數中被使用,就應該將此寄存器保存到堆棧上,當函數返回時恢復此寄存器值。而且由于 RISC 計算機大部分采用定長指令或者定變長指令,一般指令長度不會超過32個位。而現代計算機的內存地址范圍已經擴展到 32 位甚至64位,這樣在一條指令里就不足以包含有效的內存地址,所以RISC計算機一般借助于一個返回地址寄存器 RA(return address) 來實現函數的返回。幾乎在每個函數調用中都會使用到這個寄存器,所以在很多情況下 RA 寄存器會被保存在堆棧上以避免被后面的函數調用修改,當函數需要返回時,從堆棧上取回 RA 然后跳轉。
? ? 移動 SP 和保存寄存器的動作一般處在函數的開頭,這個壓棧過程也叫做 function prologue;恢復這些寄存器狀態的動作一般放在函數的最后,出棧過程也叫做 function epilogue。壓棧出棧指令各個CPU也不相同。
? ??Stack Frame 中所存放的內容和存放順序,則由目標體系架構的調用約定(calling convention)定義。下面,我們看看圖形化的函數棧幀,以棧向下增長為例來了解幾種常用CPU的stack frame 組織方式。
?
3. ?MIPS棧幀
? ??先看MIPS的棧幀布局圖: ? ?? ? ? 此圖描述的是一種典型的MIPS stack frame 組織方式。在這張圖中,sp(stack pointer)/s8(棧基址,又稱fp)?就是當前函數的棧指針,它指向棧頂的位置。Callee Frame 所示即為當前函數(被調用者)的 frame,Caller Frame 是當前函數的調用者的 frame。general register area根據需要保存ra、gp、s8等caller的寄存器信息,保存位置和順序暫時沒有發現明確的規律。 ? ? 在MIPS這種沒有BP(base pointer) 寄存器的目標架構中,進入一個函數時需要將當前棧指針向下移動 n個byte(字節),這個大小為n個byte?的存儲空間就是此函數的 stack frame 的存儲區域。此后棧指針便不再移動,只能在函數返回時再將棧指針加上這個偏移量恢復棧現場。由于不能隨便移動棧指針,所以寄存器壓棧和出棧都必須指定偏移量,這與 x86 架構的計算機對棧的使用方式有著明顯的不同。所以,MIPS一般是sp跟s8一致,只有調用alloca或動態數組后fp才會移到新的frame邊界。 ? ? 另外提一下兩個重要的寄存器。MIPS有個寄存器t9,專門用來跳轉子函數時預裝子函數地址。跳轉子函數的匯編命令是:jalr t9。這條命令后面8個byte的地址就是子函數棧幀中保留的ra值。MIPS的寄存器gp則用來存放某些變量或GOT信息,它的獲取是在函數的開頭三條語句完成,計算公式:gp = x <<16 + y?+ t9。三條語句是: ? ? ? ? lui ? ? ? ?gp, x
? ? ? ??addiu ? gp, gp, y?
? ? ? ??addu ? ?gp, gp, t9 ? ? ? ? ?#t9就是當前函數的地址,它永遠保留著最后一個被調用函數的地址 ? ??MIPS的壓棧出棧指令分別是ld和sd。?
? ? 今天先說到這,后面補上x86、ARM、PowerPC的圖形化棧幀解說。
總結
- 上一篇: 编译乱序(Compiler Reorde
- 下一篇: 也谈栈和栈帧(二)