為何C語言函數調用要堆棧,而彙編卻不需要?

2020-12-16 電子產品世界

最近,看了很多關於uboot的分析,其中就有說要為C語言的運行,就要準備好堆棧。而在Uboot的start.S彙編代碼中,關於系統初始化,也看到有堆棧指針初始化這個動作。但是,從來只是看到有人說系統初始化要初始化堆棧,即正確給堆棧指針sp賦值,但是卻從來沒有看到有人解釋,為何要初始化堆棧。

本文引用地址:http://www.eepw.com.cn/article/201807/384661.htm

今天,我們就來試圖解釋一下,為何要初始化堆棧,即:

為何C語言的函數調用要用到堆棧,而彙編卻不需要初始化堆棧?

要明白這個問題,首先要了解堆棧的作用。

關於堆棧的作用,要詳細講解的話,要很長的篇幅,所以此處只是做簡略介紹。

總的來說,堆棧的作用就是:保存現場/上下文,傳遞參數。

1

保存現場/上下文

現場,意思就相當於案發現場,總有一些現場的情況,要記錄下來的,否則被別人破壞掉之後,你就無法恢復現場了。而此處說的現場,就是指CPU運行的時候,用到了一些寄存器,比如r0,r1等等,對於這些寄存器的值,如果你不保存而直接跳轉到子函數中去執行,那麼很可能就被其破壞了,因為其函數執行也要用到這些寄存器。

因此,在函數調用之前,應該將這些寄存器等現場,暫時保持起來,等調用函數執行完畢返回後,再恢復現場。這樣CPU就可以正確的繼續執行了。

在計算機中,你常可以看到上下文這個詞,對應的英文是context。那麼:

1.1.什麼叫做上下文context

保存現場,也叫保存上下文。

上下文,英文叫做context,就是上面的文章,和下面的文章,即與你此刻,當前CPU運行有關係的內容,即那些你用到寄存器。所以,和上面的現場,是一個意思。

保存寄存器的值,一般用的是push指令,將對應的某些寄存器的值,一個個放到堆棧中,把對應的值壓入到堆棧裡面,即所謂的壓棧。

然後待被調用的子函數執行完畢的時候,再調用pop,把堆棧中的一個個的值,賦值給對應的那些你剛開始壓棧時用到的寄存器,把對應的值從堆棧中彈出去,即所謂的出棧。

其中保存的寄存器中,也包括lr的值(因為用bl指令進行跳轉的話,那麼之前的pc的值是存在lr中的),然後在子程序執行完畢的時候,再把堆棧中的lr的值pop出來,賦值給pc,這樣就實現了子函數的正確的返回。

2

傳遞參數

C語言進行函數調用的時候,常常會傳遞給被調用的函數一些參數,對於這些C語言級別的參數,被編譯器翻譯成彙編語言的時候,就要找個地方存放一下,並且讓被調用的函數能夠訪問,否則就沒發實現傳遞參數了。對於找個地方放一下,分兩種情況。

一種情況是,本身傳遞的參數就很少,就可以通過寄存器傳送參數。

因為在前面的保存現場的動作中,已經保存好了對應的寄存器的值,那麼此時,這些寄存器就是空閒的,可以供我們使用的了,那就可以放參數,而參數少的情況下,就足夠存放參數了,比如參數有2個,那麼就用r0和r1存放即可。(關於參數1和參數2,具體哪個放在r0,哪個放在r1,就是和APCS中的「在函數調用之間傳遞/返回參數」相關了,APCS中會有詳細的約定。感興趣的自己去研究。)

但是如果參數太多,寄存器不夠用,那麼就得把多餘的參數堆棧中了。

即,可以用堆棧來傳遞所有的或寄存器放不下的那些多餘的參數。

3

舉例分析C語言函數調用是如何使用堆棧的

對於上面的解釋的堆棧的作用顯得有些抽象,此處再用例子來簡單說明一下,就容易明白了:

用:

1. arm-inux-objdump –d u-boot > dump_u-boot.txt

可以得到dump_u-boot.txt文件。該文件就是中,包含了u-boot中的程序的可執行的彙編代碼,其中我們可以看到C語言的函數的原始碼,到底對應著那些彙編代碼。

下面貼出兩個函數的彙編代碼,

一個是clock_init,

另一個是與clock_init在同一C源文件中的,另外一個函數CopyCode2Ram:

1. 33d0091c :

2. 33d0091c: e92d4070 push {r4, r5, r6, lr}

3. 33d00920: e1a06000 mov r6, r0

4. 33d00924: e1a05001 mov r5, r1

5. 33d00928: e1a04002 mov r4, r2

6. 33d0092c: ebffffef bl 33d008f0

7. ... ...

8. 33d00984: ebffff14 bl 33d005dc

9. ... ...

10. 33d009a8: e3a00000 mov r0, #0 ; 0x0

11. 33d009ac: e8bd8070 pop {r4, r5, r6, pc}

12.

13. 33d009b0 :

14. 33d009b0: e3a02313 mov r2, #1275068416 ; 0x4c000000

15. 33d009b4: e3a03005 mov r3, #5 ; 0x5

16. 33d009b8: e5823014 str r3, [r2, #20]

17. ... ...

18. 33d009f8: e1a0f00e mov pc, lr

(1)clock_init部分的代碼

可以看到該函數第一行:

1. 33d009b0: e3a02313 mov r2, #1275068416 ; 0x4c000000

就沒有我們所期望的push指令,沒有去將一些寄存器的值放到堆棧中。這是因為,我們clock_init這部分的內容,所用到的r2,r3等等寄存器,和前面調用clock_init之前所用到的寄存器r0,沒有衝突,所以此處可以不用push去保存這類寄存器的值,不過有個寄存器要注意,那就是r14,即lr,其是在前面調用clock_init的時候,用的是bl指令,所以會自動把跳轉時候的pc的值賦值給lr,所以也不需要push指令去將PC的值保存到堆棧中。

而clock_init的代碼的最後一行:

1. 33d009f8: e1a0f00e mov pc, lr

就是我們常見的mov pc, lr,把lr的值,即之前保存的函數調用時候的PC值,賦值給現在的PC,這樣就實現了函數的正確的返回,即返回到了函數調用時候下一個指令的位置。

這樣CPU就可以繼續執行原先函數內剩下那部分的代碼了。

(2)CopyCode2Ram部分的代碼

1. 33d0091c: e92d4070 push {r4, r5, r6, lr}

就是我們所期望的,用push指令,保存了r4,r5,r以及lr。用push去保存r4,r5,r6,那是因為所謂的保存現場,以後後續函數返回時候再恢復現場,而用push去保存lr,那是因為此函數裡面,還有其他函數調用:

1. 33d0092c: ebffffef bl 33d008f0

2. ... ...

3. 33d00984: ebffff14 bl 33d005dc

4. ... ...

也用到了bl指令,會改變我們最開始進入clock_init時候的lr的值,所以我們要用push也暫時保存起來。而對應地,CopyCode2Ram的最後一行:

1. 33d009ac: e8bd8070 pop {r4, r5, r6, pc}

就是把之前push的值,給pop出來,還給對應的寄存器,其中最後一個是將開始push的lr的值,pop出來給賦給PC,因為實現了函數的返回。另外,我們注意到,在CopyCode2Ram的倒數第二行是:

1. 33d009a8: e3a00000 mov r0, #0 ; 0x0

是把0賦值給r0寄存器,這個就是我們所謂返回值的傳遞,是通過r0寄存器的。

此處的返回值是0,也對應著C語言的源碼中的「return 0」.

對於使用哪個寄存器來傳遞返回值:

當然你也可以用其他暫時空閒沒有用到的寄存器來傳遞返回值,但是這些處理方式,本身是根據ARM的APCS的寄存器的使用的約定而設計的,你最好不要隨便改變使用方式,最好還是按照其約定的來處理,這樣程序更加符合規範。

相關焦點

  • C語言的那些小秘密之函數的調用關係
    顯示函數的調用關係是調試器的必備功能,如果我們在程序的運行中出現了崩潰的情況,通過函數的調用關係可以快速定位問題的根源,懂得函數調用關係的實現原理也可以擴充自己的知識面,在沒有調試器的情況下,我們也可以自己來實現顯示函數的調用關係。
  • C語言函數調用棧(二)
    函數參數按照從右到左的順序入棧,函數調用者負責清除棧中的參數,返回值在EAX中。由於每次函數調用都要產生清除(還原)堆棧的代碼,故使用cdecl方式編譯的程序比使用stdcall方式編譯的程序大(後者僅需在被調函數內產生一份清棧代碼)。但cdecl調用方式支持可變參數函數(即函數帶有可變數目的參數,如printf),且調用時即使實參和形參數目不符也不會導致堆棧錯誤。
  • 每天都在調用函數,Go 中函數調用的原理你知道嗎?
    C 語言如果想要了解 C 語言中的函數調用的原理,我們可以通過 gcc 或者 clang 將 C 語言的代碼編譯成彙編語言,從彙編語言中可以一窺函數調用的具體過程,作者使用的是編譯器和內核的版本如下:$ gcc --versiongcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2Copyright (C) 2013 Free Software
  • C語言函數調用過程中的內存變化解析
    相信很多編程新手村的同學們都會有一個疑問:C 語言如何調用函數的呢?局部變量的作用域為什麼僅限於函數內?這個調用不是指C 語言上的函數調用的語法,而是在內存的視角下,函數的調用過程。本文將從C 語言調用實例,內存視角,反彙編代碼來探討C 語言函數的調用過程,也可以說是C 語言函數調用過程圖解。通過這個C 語言函數調用過程圖解,同學們將會知道,C 語言函數在調用時,內存空間是怎樣變化的。 要想理解這一個過程還好涉及到函數棧幀的概念。
  • Go有GC就不需要掌握內存堆棧知識了嗎?Go 堆棧的理解
    內存分配中的堆和棧 棧(作業系統):由作業系統自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。堆(作業系統):一般由程式設計師分配釋放, 若程式設計師不釋放,程序結束時可能由 OS 回收,分配方式倒是類似於鍊表。堆棧緩存方式棧使用的是一級緩存, 他們通常都是被調用時處於存儲空間中,調用完畢立即釋放。
  • C語言函數調用棧的詳細教程
    程序的執行過程可看作連續的函數調用。當一個函數執行完畢時,程序要回到調用指令的下一條指令(緊接call指令)處繼續執行。函數調用過程通常使用堆棧實現,每個用戶態進程對應一個調用棧結構(call stack)。編譯器使用堆棧傳遞函數參數、保存返回地址、臨時保存寄存器原有值(即函數調用的上下文)以備恢復以及存儲本地局部變量。
  • 搞懂C語言堆棧工作機制,看這篇就夠了【11張圖解】
    包括了:函數的參數,函數的局部變量,寄存器的值(用以恢復寄存器),函數的返回地址以及用於結構化異常處理的數據(當函數中有 try…catch 語句時才有,本文不討論)。這些數據是按照一定的順序組織在一起的, 我們稱之為一個堆棧幀(Stack Frame)。一個堆棧幀對應一次函數的調用。
  • arm:c語言和彙編混合編程
    1.C和彙編可相互調用,彙編子函數格式參考彙編:普通的函數調用的彙編代碼解析http://www.cnblogs.com/mylinux/p/4139972.html本文引用地址:http://www.eepw.com.cn/article/201611/317685.htm  本文演示了 : 彙編嵌入到c語言;
  • 彙編語言中的堆棧是什麼?
    我們把沒執行POP EAXPUSH 4之前的堆棧進行對比,可以說明堆棧中保存的是臨時數據,應用程式在運行時會出現大量向堆棧中讀取數據的操作,若全部保存在作業系統分配的堆棧是遠遠不夠的,所以堆棧中保存的是臨時數據。
  • 堆棧在C語言中的定義(單片機的中堆棧相當於棧)
    數據結構的棧和堆 首先在數據結構上要知道堆棧,儘管我們這麼稱呼它,但實際上堆棧是兩種數據結構:堆和棧。 堆和棧都是一種數據項按序排列的數據結構。 下面就說說C語言程序內存分配中的堆和棧,這裡有必要把內存分配也提一下,大家不要嫌我囉嗦,一般情況下程序存放在Rom或Flash中,運行時需要拷到內存中執行,內存會分別存儲不同的信息,如下圖所示:
  • GDB入門教程之查看函數調用堆棧
    調用堆棧是當前函數之前的所有已調用函數的列表,每個函數及其變量都被分配了一個 "棧幀",使用 GDB 查看函數調用堆棧可清晰地看到各個函數的調用順序以及各函數的輸入形參值,是分析程序的執行流程和輸入依賴的重要手段。
  • ARM中ADS環境下C語言和彙編語言混合編程及示例
    另外在一些對性能非常敏感的代碼塊,基於彙編與機器碼一一對應的關係,這時不能依靠C編譯器的生成代碼,而要手工編寫彙編,從而達到優化的目的。彙編語言是和CPU的指令集緊密相連的,作為涉及底層的嵌入式系統開發,熟練對應彙編語言的使用也是必須的。這裡主要討論C和彙編的混合編程,包括相互之間的函數調用。下面分四種情況來進行討論,不涉及C++語言。
  • DSP編程技巧之24---C/C++與彙編語言的交互之-(2)從C/C++代碼調用...
    本文引用地址:http://www.eepw.com.cn/article/264157.htm  1.從C/C++中調用彙編代碼中的函數  如果一個在彙編代碼中定義的函數需要在C/C++中被調用,那麼這個彙編函數相對於C/C++代碼來說,相當於一個外部的函數,所以需要使用extern "C"關鍵字進行特別聲明
  • 深入剖析 defer 原理篇 —— 函數調用的原理?
    這些數據都需要保存在一個地方,這個地方就是棧空間上。因為這些數據的聲明周期是和函數一體的,函數執行的時候存在,函數執行完立馬就可以銷毀。和堆空間不同,堆上用來分配聲明周期由程式設計師控制的對象。棧的使用規劃負責人是編譯器,堆空間的使用規劃負責人是程式設計師(在有垃圾回收的語言裡,堆空間的使用由語言層面支持)。
  • 01-JavaScript 調用堆棧
    JavaScript 引擎是一個單線程解析器,而單線程解析器由堆和單一調用棧組成。瀏覽器提供 Web APIs,比如:DOM,AJAX 和 定時器。 本文旨在說明什麼是調用堆棧以及為什麼需要調用棧?對調用棧的理解有助於我們更加清晰的知道 函數的的層次結構和執行順序 在 JavaScript 的引擎中工作方式。
  • Keil中C語言與彙編語言混合編程需要注意的幾個地方
    #pragma endasmC語言代碼……}其中紅色為C語言部分,綠色為嵌入的彙編語言部分。彙編部分需要用#pragma asm和#pragma endasm包起來2、Keil提示「asm/endasm」出錯的解決方法如果只是像1中那樣直接加入彙編代碼的話,編譯將會報錯,錯誤如下:compiling sendata.c...
  • 嵌入式系統高級C語言編程
    內容簡介  《嵌入式系統高級C語言編程》將主要介紹針對嵌入式系統的基於C語言的軟體項目開發的流程,較為複雜的c語言編程知識和技巧,編程風格和調試習慣
  • 傳智播客:C語言函數對另外一個源文件函數進行調用(外部函數)
    當一個程序由多個源文件組成的時候,根據函數是否能被其他源文件調用的時候,將函數分為內部函數和外部函數,本文就會圍著這外部函數的特點進行講解,希望每一個在學C語言的小夥伴都能弄懂函數的知識點。外部函數在開發大的項目的時候,為了方便團隊的協同工作,我們需要把一個項目拆分開,分成很多的源文件來實現。最後再將它們整理在一起。為了減少不必要的重複代碼,一個源文件有時候需要調用其他的源文件中定義的函數。
  • C語言程序執行從彙編角度詳解
    tid=2403023011&_trace_c_p_k2_=b007fad39e354f55af484d41391c5783#/learn/announce關鍵字:棧,函數,調用,彙編,寄存器 概述:C語言程序介於彙編語言和高級語言中間面,擁有直接操縱內存入口,同時擁有高級語言易讀性
  • 一篇文章了解C語言函數調用棧——程式設計師進階必備
    函數調用及彙編指令為了理解函數調用棧的細節,有必要了解一下彙編程序中函數調用的實現。函數的調用主要分為2部分,一個是調用,另外一個是返回。在彙編語言中函數調用是通過call指令完成的,返回則是通過ret指令。