你們都知道函數調用是經過棧來實現的,並且知道在棧中存放着該函數的局部變量。可是對於棧的實現細節可能不必定清楚。本文將介紹一下在Linux平臺下函數棧是如何實現的。有些同窗可能以爲不必瞭解這麼深刻,其實非也。根據本號多年的經驗,瞭解系統深層次的原理對分析疑難問題有很好的幫助。 編程
就像熟悉抓包是解決網絡通訊問題的高級武器同樣,熟悉函數調用棧則是分析程序內存問題的高級武器。本文以Linux 64位操做系統下C語言開發爲例,介紹應用程序調用棧的實現原理,並經過一個實例和GDB工具具體分析一下某個程序的調用棧內容。在介紹具體的調用棧以前,咱們先介紹一些基礎知識,這些知識是理解後續函數調用棧的基礎。bash
CPU的寄存器是須要了解的基礎知識,這是由於在X64體系中函數的參數是經過寄存器傳遞的。如圖1是X86 CPU寄存器的列表及功能簡要說明。 網絡
咱們知道Intel的CPU在設計的時候都是向前兼容的,也就是在新一代的CPU上能夠運行老一代CPU上的編譯的程序。爲了保證兼容性,新一代CPU保留了老一代寄存器的別名。以16位寄存器AX爲例,AL表示低8位,AH表示高8位。而32位CPU問世以後,經過名爲EAX的寄存器表示32位寄存器,AX仍然保留。以此類推,RAX表示一個64位寄存器。操做系統經過虛擬內存的方式爲全部應用程序提供了統一的內存映射地址。如圖3所示,從上到下分別是用戶棧、共享庫內存、運行時堆和代碼段。固然這個是一個大概的分段,實際分段比這個可能稍微複雜一些,但整個格局沒有大變化。 函數
從圖中能夠看出用戶棧是從上往下生長的。也就是用戶棧會先佔用高地址的空間,而後佔用低地址空間。目前咱們能夠大致上有個瞭解便可,後面咱們在詳細分析用戶棧的細節。爲了理解函數調用棧的細節,有必要了解一下彙編程序中函數調用的實現。函數的調用主要分爲2部分,一個是調用,另一個是返回。在彙編語言中函數調用是經過call
指令完成的,返回則是經過ret
指令。 彙編語言的call指令至關於執行了2步操做,分別是,1)將當前的IP或CS和IP壓入棧中; 2)跳轉,相似與jmp指令。一樣,ret指令也分2步,分別是,1)將棧中的地址彈出到IP寄存器;2)跳轉執行後續指令。這個基本上就是函數調用的原理。 除了在代碼間的跳動外,函數的調用每每還須要傳遞一個參數,而處理完成後還可能有返回值。這些數據的傳遞都是經過寄存器進行的。在函數調用以前經過上文介紹的寄存器存儲參數,函數返回以前經過RAX寄存器(32位系統爲EAX)存儲返回結果。 另一個比較重要的知識點是函數調用過程當中與堆棧相關的寄存器RSP和RBP,兩個寄存器主要實現對棧位置的記錄,具體做用以下: RSP:棧指針寄存器(reextended stack pointer),其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。 RBP:基址指針寄存器(reextended base pointer),其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的底部。工具
寄存器的名稱跟體系結構是相關的,本文是64位系統,所以寄存器是RSP和RBP。若是是32位系統則寄存器的名稱爲ESP和EBP。spa
咱們先從總體上來看一下函數調用棧的主要內容,如圖4所示。在函數棧中主要包括函數參數表、局部變量表、棧的基址和函數返回地址。這裏棧的基址是上一個棧幀的基址,由於在本函數中須要使用該基址訪問棧中的內容,所以須要首先將上一個棧幀中的基址壓棧。 操作系統
爲了便於理解,咱們以一個具體的程序做爲示例。本程序很是簡單,主要是模擬了多個函數的函數調用關係和參數傳遞。另外,在函數func_2中定義了2個形參,以模擬多參數傳遞的過程。 設計
在本示例中,main函數調用func_1函數。咱們從main函數開始分析,能夠先看一下右側的C語言代碼。 首先是函數參數的準備過程。在main函數調用func_1時依次傳入的參數爲一、二、3和4+g,其中最後一個參數是須要計算的。按照紅色方框的虛線,咱們能夠看到對應的彙編程序,在彙編程序中首先處理最後一個參數,而後是倒數第二個,以此類推(函數參數的處理順序在平常開發中是須要注意的內容重點)。同時,咱們看到存儲參數的寄存器名稱與前文是一致。 當準備完參數以後,就是調用func_1函數,這個在彙編語言中就是call func_1
這一行。雖然只是一行彙編指令,但其實內部作了一些事情,這個咱們在前文介紹call指令的時候有所介紹,你們能夠參考一下前文。 以後就進入func_1
函數的處理邏輯。最一開始是pushq %rbp
彙編程序,這句指令的做用是將RBP壓入函數棧中。這句壓棧及後面的更新RBP的值(moveq %rsp, %rbp)是構建本函數的棧幀頭,後續對本棧幀的內容的訪問都是經過幀頭(RBP)進行的。接下來是對參數壓棧的過程和局部變量初始化的過程,具體分佈參考圖5中的綠色方框和紅色方框。 完成函數內的運算後,最後將運算結果放入寄存器EAX中,而後調用指令leave和ret。這裏面須要說明的是leave指令,該指令至關於下面兩條彙編指令。能夠對比一下函數入口的彙編指令,其實二者是對稱的。leave指令將本幀的棧基址賦值給棧指針(圖6中步驟2),而後將其中的內容彈出到RBP中(圖6中步驟3)。其實就是RBP指向上一個幀(調用者)的棧幀,也便是一個復原的過程。
movl %ebp %esp
popl %ebp
複製代碼
這樣,函數返回後寄存器RBP和RSP從被調用者的棧幀切換到了調用者的棧幀。
上面是經過反彙編的方式分析函數的調用棧和棧幀狀況。咱們還能夠經過gdb動態的分析函數棧和棧幀的使用狀況。咱們依然經過main函數調用func_1函數爲例來分析。咱們這裏在函數func_1的入口處設置一個單點,而後運行程序,程序中止在斷點處。如圖7是咱們逐步執行是函數棧的變化過程,具體細節咱們這裏就再也不贅述,你們能夠實際操做一下。 指針
本文的目的是讓你們對函數調用棧有個總體的瞭解,這樣對之後程序的疑難雜症就有更多的解決思路。由於在實際生產環境中與棧相關的問題也是比較多的,好比局部變量太多致使的棧溢出,或者踩內存問題引發的棧破壞等等。所以,瞭解了函數棧的原理,在遇到所謂的莫名其妙問題的時候就會有新的思路。每每不少問題不是問題自己莫名其妙,而是咱們的知識儲備不夠,本身感受莫名其妙而已。code