嵌入式系統中函數的調用

瞭解下函數的調用過程,對AUTOSAR分層架構的實現有好處。

1 程序的內存分佈

嵌入式系統中,一個函數調用時,它的內部機理是什麼,執行了哪些步驟?如圖1所示,先看 看 一個程序在運行時,它的內存分佈狀況。
在這裏插入圖片描述
圖1 系統中的內存分佈
當程序運行時,它的代碼會被裝入內存,保存在代碼區,包括主函數和其他函數。主要有三塊內存區域用來存放數據:
第一塊是全局變量區域,存放了程序當中的所有全局變量。由於全局變量的個數和大小是已知的,所以這一塊區域所佔用的內存大小在開始就確定下來,它們被稱爲是靜態分配。位於此區域內的變量,它們在程序的整個運行過程當中,都一直存在,只有當整個程序運行結束了, 這一塊內存區域纔會被釋放。
第二塊區域是棧(stack)區域,它包含了所有的棧幀。所謂的棧幀( stack frame),就是在調用函數時,系統自動地爲該函數分配一塊內存區域,用來保存它的運行上下文、形參和局部變量等信息,這樣的一塊內存區域,就叫做一個棧幀。棧幀是在函數調用時分配,當函數調用結束,相應的棧幀則被釋放。所以,對於一個函數的局部變量來說,只有當函數調用發生時,系統纔會給這個函數的形參和局部變量分配存儲空間;當函數調用結束後,這些局部變量就被釋放掉了。另外,棧區是由系統自動分配,用戶不需要關心,所以也稱爲是自動分配。
第三塊區域是堆(heap) 區域,它主要是用作動態分配的內存。
舉個例子對應起來看,直觀一些。
 圖2    內存分佈示例
圖2 內存分佈示例
如圖2所示,程序開始運行,demudashu()這個函數會被裝入到內存。它的代碼存放在內存的代碼區域。由於在這段程序中定義了一個全局變量z,所以內存的全局變量區域分配了一個存儲單元給它。

接下來,系統調用函數運行,當這個函數調用發生,系統就會在棧中給它分配一塊內存空間,即一個棧幀,用來存放函數當中所定義的局部變量,即x和y。

隨後,程序計數器PC就跳轉到函數的第一條語句,開始執行。

當函數執行結束,首先要把它所佔用的棧幀釋放掉。對於任何一次函數調用而言,在函數調用結束後,都要把相應的棧幀釋放掉,所以x和y這兩個局部變量所佔用的存儲空間就被釋放掉了。

當一次函數調用發生時,它的執行過程可以歸納爲以下5個步驟:

在內存的棧空間當中爲其分配一個棧幀,用來存放該函數的形參變量和局部變量。

把實參變量的值複製到相應的形參變量中。

控制流轉移到該函數的起始位置。

該函數開始執行。

當這個函數執行完以後,控制流和返回值返回到函數調用點。

下面用一個例子來總結下變量的存儲與作用域。


/* 全局變量,固定地址,其他源文件可見*/
int  demu_global_static;
/* 靜態全局變量,固定地址,但只在本文件可見*/
static int demu_static;
/* 函數參數:位於棧幀中,動態創建,動態釋放*/
int foo(int auto_parameter)
{   
  /* 靜態局部變量 ,固定地址,只在本函數中可見*/   
  static int func_static;
  /* 普通局部變量,位於棧幀中,只在本函數中可見*/   
  int auto_i,auto_a[10];   
  /* 動態申請的內存空間,位於堆中*/   
  double *auto_d = malloc(sizeof (double)*2020);   
  return auto_i; 
}

2 函數的調用

有了上面的內存分配理解再來看看函數的調用。

函數調用過程分五個步驟:
①程序先執行函數調用之前的語句;
②流程的控制轉移到被調用函數入口處,同時進行參數傳遞;
③執行被調用函數中函數體的語句;
④流程返回調用函數的下一條指令處,將函數返回值帶回;
⑤接着執行主調函數未執行的語句。
在這裏插入圖片描述
圖3 函數調用過程
這樣就要求在轉到被調用函數之前,要記下當時執行的指令的地址,還要“保護現場”(記下當時有關的信息),方便在函數調用之後繼續執行。在函數調用之後,流程返回到先前記下的地址處,並且根據記下的信息“恢復現場”,然後繼續執行。這些過程都會花費一定的時間。如果有的函數需要頻繁的使用,則所用時間會很長,從而降低程序的執行效率。有些實用程序對效率是有要求的,要求系統的響應世間短,這就需要儘量壓縮調用過程的時間。

2.1 內置函數

C語言提供了一種提高函數調用效率的方法,即在編譯時將所調用的代碼直接嵌入到主調函數中,而不是將流程轉出去。這種嵌入到主調函數中的函數稱爲內置函數(inline function),又稱內嵌函數。有些人把它稱爲內聯函數。

用法:在函數首行的左端加一個關鍵字inline即可。

還是舉個例子來看,明晰一些。


int main()
{
  int i = 3, j = 5, k =8, m;
  m = max(i, j, k);
  cout << "max=" << m = endl;
  return 0;
}
inline int max(int a, int b, int c);//定義max爲內置函數
{
  if (b > a)
  a = b;
  if (c > a)
  a = c;
  return a;
}

由於定義函數時指定它爲內置函數,因此編譯系統在遇到函數調用“max(i,j,k)”時,就用max函數體的代碼代替“max(i,j,k)”,同時將實參代替形參。在聲明函數和定義函數時可以同時寫inline,也可以只在其中一處聲明inline,效果相同,都能按內置函數處理。

使用內置函數可以節省運行時間,但卻增加了目標程序的長度。假設要調用10次max函數,則編譯時先後10次將max代碼複製並插入main函數,這就增加了目標文件main函數的長度。因此一般只將規模很小而使用頻繁的函數(如定時採集數據的函數聲明爲內置函數)。在函數規模很小的情況下,函數調用的時間可能相當於甚至超過執行函數本身的時間,把它定義爲內置函數,可大大減少程序的運行時間。

內置函數中不能包括複雜的控制語句,如循環語句和switch語句。

對函數做inline聲明,只是程序設計者對編譯系統提出的一個建議,是建議性的,而不是指令性的。並非指定爲inline,編譯系統必須這樣做。它是根據具體情況決定的。例如對前面提到的包含循環語句和switch語句的函數或一個遞歸函數是無法進行代碼置換的,又如一個上萬行的函數,也不太可能在調用點展開。此時編譯系統就會忽略inline聲明,而按普通函數處理。

所以,只有規模較小而又頻繁調用的簡單函數,才適合於聲明爲inline函數。

2.2 函數調用過程

前文,如圖3,已經描述到,當執行到某一個函數時,系統就會跳轉過去執行該函數,執行完畢後接着再去執行下一條指令。在執行調用函數的過程中,系統還要根據函數完成一些工作,這些操作通過形成一個棧幀來完成。棧幀是編譯器用來實現函數調用過程的一種數據結構。C語言中,每個棧幀對應着一個未運行完的函數。

下面通過debug,看看Add()函數的執行過程。


int Add(int a, int b)
{
  int z = 0;
  z = a + b;
  return z;
}
int main()
{
  int a = 10;
  int b = 20;
  int ret;
  ret = Add(a, b);
  printf("%d", ret);
  system("pause");
  return 0;
}

以下調試過程大家定性看一下調用過程,實際過程和嵌入式系統略有差異。

調用main函數之前在VC6.0編輯器可以看到main函數在_tmainCRTStartup 函數中調用的,而 _tmainCRTStartup 函數是在 mainCRTStartup 被調用的。這個過程要爲函數開闢棧空間, 這塊棧空間我們稱之爲函數棧幀。
在這裏插入圖片描述
棧幀的需要ebp和esp兩個寄存器。在函數調用的過程中這兩個寄存器存放了維護這個棧的棧底和棧頂指針 。ebp指向當前位於系統棧最上邊一個棧幀的底部,而不是系統棧的底部。嚴格說來,“棧幀底部”和“棧底”是不同的概念;ESP所指的棧幀頂部和系統棧的頂部是同一個位置。
調用main函數
展開main函數的調用就得爲main函數創建棧幀,可以看到過程:
在這裏插入圖片描述
執行上圖第一條指令:
在這裏插入圖片描述
1.壓棧,把ebp放入棧頂,而esp始終指向棧頂
2.將esp值傳給ebp,也就是讓esp,ebp移在一起
3.sub爲減的意思,即將esp-0E4h賦給esp,且函數調用分配由高地址向低地址增長,因此esp向上移動,即開闢了新空間,也就是爲main函數開闢空間
4.三個push壓榨分別將ebx,esi,edi按順序壓入棧頂,而esp也會指向棧頂
5.lea指令,加載有效地址;將ebp-0E4h的地址放入edi中,也就是edi指向ebp-0E4h,把39h放到ecx中,把0cccccccch放到eax中,從edi所指向的地址開始向高地址進行拷貝,拷貝的次數爲ecx內容,拷貝的內容爲eax內。
在這裏插入圖片描述
6.創建變量a與b並初始化10和20.
在這裏插入圖片描述
Add函數的調用
1.把b放入eax中,然後對eax壓棧(形參a)
2.把a放入ecx中,然後對ecx壓棧(形參b)
3.call作用:將下一條指令地址壓棧,然後進入add函數裏面
在這裏插入圖片描述
注意:call語句push的是下一條指令的地址,爲了函數返回時知道從哪兒接着執行
接下來進入add函數:
A. 先把main函數ebp壓棧,保存指向main()函數棧幀底部的ebp的地址,目的是當返回時能找到main函數棧底,此時esp指向新的棧頂位置。將main函數的ebp壓棧,也是爲了返回時找到main函數棧底。
B. 將esp的值賦給ebp,產生新的ebp,即Add()函數棧幀的ebp;
C. 給esp減去一個16進制數0CCh(爲Add()函數預開闢空間);
D. push ebx、esi、edi;
E. lea指令,加載有效地址;
F. 初始化預開闢的空間爲0xcccccccc;
在這裏插入圖片描述
G. 創建變量z併爲其賦值;
H. 把形參a放到eax,即把10,放入eax把形參b加到eax中,即把20加到eax中再把eax放到z的位置,即把兩數之和放到z中;
I. 把z的值放到寄存器eax中返回,因爲z爲函數臨時開闢的變量空間等函數執行完會銷燬,因此放寄存器中返回;
K .接下來執行pop出棧操作,edi esi ebx依次從上向下出棧,esp 會向下移動,棧的特點:先進後出,後進先出;
L. 將ebp值賦給esp,也就是esp向下移動指向ebp位置,此時add開闢的棧空間已經銷燬;
M. pop將棧頂的元素彈出放到ebp中,也就是說將main函數的ebp放入ebp中,即ebp現在指向main函數ebp;
在這裏插入圖片描述
N. 在執行ret後,會把之前push的地址彈出去,這時就要返回main函數,這也就是爲什麼之前要push這個地址,這樣call指令就完成了,接下來從那個call指令繼續執行;
在這裏插入圖片描述
O. 把esp+8,即esp向下移,把形參銷燬;
最後就是對main函數棧幀的銷燬,方法類似。

棧幀的總結:

1.堆棧是C語言程序運行時必須的一個記錄調用路徑和參數的空間:
函數調用框架;
傳遞參數;
保存返回地址;
提供局部變量空間;
2. 堆棧寄存器和堆棧操作
堆棧相關的寄存器

esp,堆棧指針(stack pointer) 
ebp,基址指針(base pointer) 

堆棧操作
push 棧頂地址減少4個字節(32位)
pop 棧頂地址增加4個字節

ebp在C語言中用作記錄當前函數調用基址

3 AUTOSAR中Runnable

Runnable(可運行實體)就是SWC中的函數,而在AUTOSAR架構中,使用工具生成時,Runnable是空函數,需要手動添加代碼來實現它的實際功能。Runnable可以被觸發,比如被定時器觸發、被操作調用觸發或者被接受數據觸發等。
在這裏插入圖片描述
這裏的函數就是Send接口,發送的數據由RTE進行管理。然而,由於這個SWCn.c文件中並未包含BSW中的.h文件,通過這個方式將AppL和BSW隔離開。所以如果假如必要的.h文件,其實也可以調用BSW中的函數,但是不建議這麼做,該過程 應由RTE來完成觸發和調度。
在這裏插入圖片描述
RTE給runnables提供觸發條件,也就是runnable在設計的時候,需要有觸發條件,不然無法運行,也就沒有意義了。觸發條件就是一些特定的事件,AUTOSAR中主要規定了以下一些觸發條件:
初始化事件:初始化自動觸發

定時器事件:給一個週期定時器,時間到了就觸發

接收數據事件(S/R):Receiver Port 一旦收到數據觸發

接收數據錯誤事件(S/R)

數據發送完成事件(S/R):Send Port 發送完成觸發

操作調用事件(C/S):當調用到了該函數時觸發

異步服務返回事件(C/S):C/S可以在異步下運行,即當異步調用一個Server函數,那麼該被調函數作爲一個線程和當前的運行程序並行運行,當被調函數運行結束返回(Return)時,這時觸發異步服務返回事件。

模式切換事件

模式切換應答事件
在這裏插入圖片描述

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章