1 程序的內(nèi)存分布
嵌入式系統(tǒng)中,一個函數(shù)調(diào)用時,它的內(nèi)部機(jī)理是什么,執(zhí)行了哪些步驟?如圖1所示,先看 看 一個程序在運(yùn)行時,它的內(nèi)存分布狀況。
*** 圖1 系統(tǒng)中的內(nèi)存分布***
當(dāng)程序運(yùn)行時,它的代碼會被裝入內(nèi)存,保存在代碼區(qū),包括主函數(shù)和其他函數(shù)。主要有三塊內(nèi)存區(qū)域用來存放數(shù)據(jù):
第一塊是全局變量區(qū)域,存放了程序當(dāng)中的所有全局變量。由于全局變量的個數(shù)和大小是已知的,所以這一塊區(qū)域所占用的內(nèi)存大小在開始就確定下來,它們被稱為是靜態(tài)分配。位于此區(qū)域內(nèi)的變量,它們在程序的整個運(yùn)行過程當(dāng)中,都一直存在,只有當(dāng)整個程序運(yùn)行結(jié)束了, 這一塊內(nèi)存區(qū)域才會被釋放。
第二塊區(qū)域是棧(stack)區(qū)域,它包含了所有的棧幀。所謂的棧幀( stack frame),就是在調(diào)用函數(shù)時,系統(tǒng)自動地為該函數(shù)分配一塊內(nèi)存區(qū)域,用來保存它的運(yùn)行上下文、形參和局部變量等信息,這樣的一塊內(nèi)存區(qū)域,就叫做一個棧幀。棧幀是在函數(shù)調(diào)用時分配,當(dāng)函數(shù)調(diào)用結(jié)束,相應(yīng)的棧幀則被釋放。所以,對于一個函數(shù)的局部變量來說,只有當(dāng)函數(shù)調(diào)用發(fā)生時,系統(tǒng)才會給這個函數(shù)的形參和局部變量分配存儲空間;當(dāng)函數(shù)調(diào)用結(jié)束后,這些局部變量就被釋放掉了。另外,棧區(qū)是由系統(tǒng)自動分配,用戶不需要關(guān)心,所以也稱為是自動分配。
第三塊區(qū)域是堆(heap) 區(qū)域,它主要是用作動態(tài)分配的內(nèi)存。
舉個例子對應(yīng)起來看,直觀一些。
*** 圖2 內(nèi)存分布示例***
如圖2所示,程序開始運(yùn)行,demudashu()這個函數(shù)會被裝入到內(nèi)存。它的代碼存放在內(nèi)存的代碼區(qū)域。由于在這段程序中定義了一個全局變量z,所以內(nèi)存的全局變量區(qū)域分配了一個存儲單元給它。
接下來,系統(tǒng)調(diào)用函數(shù)運(yùn)行,當(dāng)這個函數(shù)調(diào)用發(fā)生,系統(tǒng)就會在棧中給它分配一塊內(nèi)存空間,即一個棧幀,用來存放函數(shù)當(dāng)中所定義的局部變量,即x和y。
隨后,程序計數(shù)器PC就跳轉(zhuǎn)到函數(shù)的第一條語句,開始執(zhí)行。
當(dāng)函數(shù)執(zhí)行結(jié)束,首先要把它所占用的棧幀釋放掉。對于任何一次函數(shù)調(diào)用而言,在函數(shù)調(diào)用結(jié)束后,都要把相應(yīng)的棧幀釋放掉,所以x和y這兩個局部變量所占用的存儲空間就被釋放掉了。
當(dāng)一次函數(shù)調(diào)用發(fā)生時,它的執(zhí)行過程可以歸納為以下5個步驟:
- 在內(nèi)存的??臻g當(dāng)中為其分配一個棧幀,用來存放該函數(shù)的形參變量和局部變量。
- 把實參變量的值復(fù)制到相應(yīng)的形參變量中。
- 控制流轉(zhuǎn)移到該函數(shù)的起始位置。
- 該函數(shù)開始執(zhí)行。
- 當(dāng)這個函數(shù)執(zhí)行完以后,控制流和返回值返回到函數(shù)調(diào)用點(diǎn)。
下面用一個例子來總結(jié)下變量的存儲與作用域。
/* 全局變量,固定地址,其他源文件可見*/
int demu_global_static;
/* 靜態(tài)全局變量,固定地址,但只在本文件可見*/
static int demu_static;
/* 函數(shù)參數(shù):位于棧幀中,動態(tài)創(chuàng)建,動態(tài)釋放*/
int foo(int auto_parameter)
{
/* 靜態(tài)局部變量 ,固定地址,只在本函數(shù)中可見*/
static int func_static;
/* 普通局部變量,位于棧幀中,只在本函數(shù)中可見*/
int auto_i,auto_a[10];
/* 動態(tài)申請的內(nèi)存空間,位于堆中*/
double *auto_d = malloc(sizeof (double)*2020);
return auto_i;
}
2 函數(shù)的調(diào)用
有了上面的內(nèi)存分配理解再來看看函數(shù)的調(diào)用。
函數(shù)調(diào)用過程分五個步驟:
①程序先執(zhí)行函數(shù)調(diào)用之前的語句;
②流程的控制轉(zhuǎn)移到被調(diào)用函數(shù)入口處,同時進(jìn)行參數(shù)傳遞;
③執(zhí)行被調(diào)用函數(shù)中函數(shù)體的語句;
④流程返回調(diào)用函數(shù)的下一條指令處,將函數(shù)返回值帶回;
⑤接著執(zhí)行主調(diào)函數(shù)未執(zhí)行的語句。
*** 圖3 函數(shù)調(diào)用過程***
這樣就要求在轉(zhuǎn)到被調(diào)用函數(shù)之前,要記下當(dāng)時執(zhí)行的指令的地址,還要 “保護(hù)現(xiàn)場” (記下當(dāng)時有關(guān)的信息),方便在函數(shù)調(diào)用之后繼續(xù)執(zhí)行。在函數(shù)調(diào)用之后,流程返回到先前記下的地址處,并且根據(jù)記下的信息 “恢復(fù)現(xiàn)場” ,然后繼續(xù)執(zhí)行。這些過程都會花費(fèi)一定的時間。如果有的函數(shù)需要頻繁的使用,則所用時間會很長,從而降低程序的執(zhí)行效率。有些實用程序?qū)π适怯幸蟮?,要求系統(tǒng)的響應(yīng)世間短,這就需要盡量壓縮調(diào)用過程的時間。
2.1 內(nèi)置函數(shù)
C語言提供了一種提高函數(shù)調(diào)用效率的方法,即在編譯時將所調(diào)用的代碼直接嵌入到主調(diào)函數(shù)中,而不是將流程轉(zhuǎn)出去。這種嵌入到主調(diào)函數(shù)中的函數(shù)稱為 內(nèi)置函數(shù) (inline function),又稱內(nèi)嵌函數(shù)。有些人把它稱為內(nèi)聯(lián)函數(shù)。
用法:在函數(shù)首行的左端加一個關(guān)鍵字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為內(nèi)置函數(shù)
{
if (b > a)
a = b;
if (c > a)
a = c;
return a;
}
由于定義函數(shù)時指定它為內(nèi)置函數(shù),因此編譯系統(tǒng)在遇到函數(shù)調(diào)用“max(i,j,k)”時,就用max函數(shù)體的代碼代替“max(i,j,k)”,同時將實參代替形參。在聲明函數(shù)和定義函數(shù)時可以同時寫inline,也可以只在其中一處聲明inline,效果相同,都能按內(nèi)置函數(shù)處理。
使用內(nèi)置函數(shù)可以節(jié)省運(yùn)行時間,但卻增加了目標(biāo)程序的長度。假設(shè)要調(diào)用10次max函數(shù),則編譯時先后10次將max代碼復(fù)制并插入main函數(shù),這就增加了目標(biāo)文件main函數(shù)的長度。因此一般只將規(guī)模很小而使用頻繁的函數(shù)(如定時采集數(shù)據(jù)的函數(shù)聲明為內(nèi)置函數(shù))。在函數(shù)規(guī)模很小的情況下,函數(shù)調(diào)用的時間可能相當(dāng)于甚至超過執(zhí)行函數(shù)本身的時間,把它定義為內(nèi)置函數(shù),可大大減少程序的運(yùn)行時間。
內(nèi)置函數(shù)中不能包括復(fù)雜的控制語句,如循環(huán)語句和switch語句。
對函數(shù)做inline聲明,只是程序設(shè)計者對編譯系統(tǒng)提出的一個建議,是建議性的,而不是指令性的。并非指定為inline,編譯系統(tǒng)必須這樣做。它是根據(jù)具體情況決定的。例如對前面提到的包含循環(huán)語句和switch語句的函數(shù)或一個遞歸函數(shù)是無法進(jìn)行代碼置換的,又如一個上萬行的函數(shù),也不太可能在調(diào)用點(diǎn)展開。此時編譯系統(tǒng)就會忽略inline聲明,而按普通函數(shù)處理。
所以,只有規(guī)模較小而又頻繁調(diào)用的簡單函數(shù),才適合于聲明為inline函數(shù)。
2.2 函數(shù)調(diào)用過程
前文,如圖3,已經(jīng)描述到,當(dāng)執(zhí)行到某一個函數(shù)時,系統(tǒng)就會跳轉(zhuǎn)過去執(zhí)行該函數(shù),執(zhí)行完畢后接著再去執(zhí)行下一條指令。在執(zhí)行調(diào)用函數(shù)的過程中,系統(tǒng)還要根據(jù)函數(shù)完成一些工作,這些操作通過形成一個棧幀來完成。棧幀是編譯器用來實現(xiàn)函數(shù)調(diào)用過程的一種數(shù)據(jù)結(jié)構(gòu)。C語言中,每個棧幀對應(yīng)著一個未運(yùn)行完的函數(shù)。
下面通過debug,看看Add()函數(shù)的執(zhí)行過程。
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;
}
以下調(diào)試過程大家定性看一下調(diào)用過程,實際過程和嵌入式系統(tǒng)略有差異。
調(diào)用main函數(shù)之前在VC6.0編輯器可以看到main函數(shù)在_tmainCRTStartup 函數(shù)中調(diào)用的,而 _tmainCRTStartup 函數(shù)是在 mainCRTStartup 被調(diào)用的。這個過程要為函數(shù)開辟??臻g, 這塊??臻g我們稱之為函數(shù)棧幀。
棧幀的需要ebp和esp兩個寄存器。在函數(shù)調(diào)用的過程中這兩個寄存器存放了維護(hù)這個棧的棧底和棧頂指針 。ebp指向當(dāng)前位于系統(tǒng)棧最上邊一個棧幀的底部,而不是系統(tǒng)棧的底部。嚴(yán)格說來,“棧幀底部”和“棧底”是不同的概念;ESP所指的棧幀頂部和系統(tǒng)棧的頂部是同一個位置。
開始調(diào)用main函數(shù)
展開main函數(shù)的調(diào)用就得為main函數(shù)創(chuàng)建棧幀,可以看到過程:
執(zhí)行上圖第一條指令:
1.壓棧,把ebp放入棧頂,而esp始終指向棧頂
2.將esp值傳給ebp,也就是讓esp,ebp移在一起
3.sub為減的意思,即將esp-0E4h賦給esp,且函數(shù)調(diào)用分配由高地址向低地址增長,因此esp向上移動,即開辟了新空間,也就是為main函數(shù)開辟空間
4.三個push壓榨分別將ebx,esi,edi按順序壓入棧頂,而esp也會指向棧頂
5.lea指令,加載有效地址;將ebp-0E4h的地址放入edi中,也就是edi指向ebp-0E4h,把39h放到ecx中,把0cccccccch放到eax中,從edi所指向的地址開始向高地址進(jìn)行拷貝,拷貝的次數(shù)為ecx內(nèi)容,拷貝的內(nèi)容為eax內(nèi)。
6.創(chuàng)建變量a與b并初始化10和20.
Add函數(shù)的調(diào)用
1.把b放入eax中,然后對eax壓棧(形參a)
2.把a(bǔ)放入ecx中,然后對ecx壓棧(形參b)
3.call作用:將下一條指令地址壓棧,然后進(jìn)入add函數(shù)里面
注意:call語句push的是下一條指令的地址,為了函數(shù)返回時知道從哪兒接著執(zhí)行
接下來進(jìn)入add函數(shù):
A. 先把main函數(shù)ebp壓棧,保存指向main()函數(shù)棧幀底部的ebp的地址,目的是當(dāng)返回時能找到main函數(shù)棧底,此時esp指向新的棧頂位置。將main函數(shù)的ebp壓棧,也是為了返回時找到main函數(shù)棧底。
B. 將esp的值賦給ebp,產(chǎn)生新的ebp,即Add()函數(shù)棧幀的ebp;
C. 給esp減去一個16進(jìn)制數(shù)0CCh(為Add()函數(shù)預(yù)開辟空間);
D. push ebx、esi、edi;
E. lea指令,加載有效地址;
F. 初始化預(yù)開辟的空間為0xcccccccc;
G. 創(chuàng)建變量z并為其賦值;
H. 把形參a放到eax,即把10,放入eax把形參b加到eax中,即把20加到eax中再把eax放到z的位置,即把兩數(shù)之和放到z中;
I. 把z的值放到寄存器eax中返回,因為z為函數(shù)臨時開辟的變量空間等函數(shù)執(zhí)行完會銷毀,因此放寄存器中返回;
K .接下來執(zhí)行pop出棧操作,edi esi ebx依次從上向下出棧,esp 會向下移動,棧的特點(diǎn):先進(jìn)后出,后進(jìn)先出;
L. 將ebp值賦給esp,也就是esp向下移動指向ebp位置,此時add開辟的棧空間已經(jīng)銷毀;
M. pop將棧頂?shù)脑貜棾龇诺絜bp中,也就是說將main函數(shù)的ebp放入ebp中,即ebp現(xiàn)在指向main函數(shù)ebp;
N. 在執(zhí)行ret后,會把之前push的地址彈出去,這時就要返回main函數(shù),這也就是為什么之前要push這個地址,這樣call指令就完成了,接下來從那個call指令繼續(xù)執(zhí)行;
O. 把esp+8,即esp向下移,把形參銷毀;
最后就是對main函數(shù)棧幀的銷毀,方法類似。
棧幀的總結(jié):
1.堆棧是C語言程序運(yùn)行時必須的一個記錄調(diào)用路徑和參數(shù)的空間:
- 函數(shù)調(diào)用框架;
- 傳遞參數(shù);
- 保存返回地址;
- 提供局部變量空間;
- 堆棧寄存器和堆棧操作
堆棧相關(guān)的寄存器
- esp,堆棧指針(stack pointer)
- ebp,基址指針(base pointer)
堆棧操作
- push 棧頂?shù)刂窚p少4個字節(jié)(32位)
- pop 棧頂?shù)刂吩黾?個字節(jié)
- ebp在C語言中用作記錄當(dāng)前函數(shù)調(diào)用基址
3 AUTOSAR中Runnable
Runnable(可運(yùn)行實體)就是SWC中的函數(shù),而在AUTOSAR架構(gòu)中,使用工具生成時,Runnable是空函數(shù),需要手動添加代碼來實現(xiàn)它的實際功能。Runnable可以被觸發(fā),比如被定時器觸發(fā)、被操作調(diào)用觸發(fā)或者被接受數(shù)據(jù)觸發(fā)等。
這里的函數(shù)就是Send接口,發(fā)送的數(shù)據(jù)由RTE進(jìn)行管理。然而,由于這個SWCn.c文件中并未包含BSW中的.h文件,通過這個方式將AppL和BSW隔離開。所以如果假如必要的.h文件,其實也可以調(diào)用BSW中的函數(shù),但是不建議這么做,該過程 應(yīng)由RTE來完成觸發(fā)和調(diào)度。
RTE給runnables提供觸發(fā)條件,也就是runnable在設(shè)計的時候,需要有觸發(fā)條件,不然無法運(yùn)行,也就沒有意義了。觸發(fā)條件就是一些特定的事件,AUTOSAR中主要規(guī)定了以下一些觸發(fā)條件:
- 初始化事件:初始化自動觸發(fā)
- 定時器事件:給一個周期定時器,時間到了就觸發(fā)
- 接收數(shù)據(jù)事件(S/R):Receiver Port 一旦收到數(shù)據(jù)觸發(fā)
- 接收數(shù)據(jù)錯誤事件(S/R)
- 數(shù)據(jù)發(fā)送完成事件(S/R):Send Port 發(fā)送完成觸發(fā)
- 操作調(diào)用事件(C/S):當(dāng)調(diào)用到了該函數(shù)時觸發(fā)
- 異步服務(wù)返回事件(C/S):C/S可以在異步下運(yùn)行,即當(dāng)異步調(diào)用一個Server函數(shù),那么該被調(diào)函數(shù)作為一個線程和當(dāng)前的運(yùn)行程序并行運(yùn)行,當(dāng)被調(diào)函數(shù)運(yùn)行結(jié)束返回(Return)時,這時觸發(fā)異步服務(wù)返回事件。
- 模式切換事件
- 模式切換應(yīng)答事件
-
嵌入式系統(tǒng)
+關(guān)注
關(guān)注
41文章
3630瀏覽量
129792 -
程序
+關(guān)注
關(guān)注
117文章
3798瀏覽量
81459 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4346瀏覽量
63018
發(fā)布評論請先 登錄
相關(guān)推薦
嵌入式系統(tǒng)U盤實時啟動技術(shù)
嵌入式開發(fā)的優(yōu)缺點(diǎn)是什么?
開發(fā)基本的嵌入式應(yīng)用程序
![開發(fā)基本的<b class='flag-5'>嵌入式</b>應(yīng)用程序](https://file1.elecfans.com//web2/M00/A4/83/wKgZomUMNImAXURKAABS3gpDP9g083.jpg)
汽車電子嵌入式軟件接口庫設(shè)計
![汽車電子<b class='flag-5'>嵌入式</b>軟件接口庫設(shè)計](https://file.elecfans.com/web2/M00/48/D2/pYYBAGKhtCeAcm_PAAAYHPuZP9k145.jpg)
基于Chirp函數(shù)的Nios Ⅱ嵌入式實現(xiàn)
![基于Chirp<b class='flag-5'>函數(shù)</b>的Nios Ⅱ<b class='flag-5'>嵌入式</b>實現(xiàn)](https://file1.elecfans.com//web2/M00/A5/E8/wKgZomUMOriAWynXAAAND19_neY819.jpg)
如何在嵌入式FreeRTOS系統(tǒng)接口調(diào)用API?
![如何在<b class='flag-5'>嵌入式</b>FreeRTOS<b class='flag-5'>系統(tǒng)</b>接口<b class='flag-5'>調(diào)用</b>API?](https://file.elecfans.com/web1/M00/59/40/pIYBAFtlEj6APsTLAAARNvngweo950.png)
為什么中斷處理函數(shù)不能直接調(diào)用不可重入函數(shù)
嵌入式系統(tǒng)
![<b class='flag-5'>嵌入式</b><b class='flag-5'>系統(tǒng)</b>](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
什么是嵌入式系統(tǒng)
![什么是<b class='flag-5'>嵌入式</b><b class='flag-5'>系統(tǒng)</b>](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
嵌入式開發(fā)概述(20190325小結(jié))
![<b class='flag-5'>嵌入式</b>開發(fā)概述(20190325小結(jié))](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
嵌入式軟件中的延時函數(shù)
![<b class='flag-5'>嵌入式</b>軟件<b class='flag-5'>中</b>的延時<b class='flag-5'>函數(shù)</b>](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
函數(shù)調(diào)在嵌入式應(yīng)用設(shè)計中如何實現(xiàn)
嵌入式軟件架構(gòu)設(shè)計之函數(shù)調(diào)用
![<b class='flag-5'>嵌入式</b>軟件架構(gòu)設(shè)計之<b class='flag-5'>函數(shù)</b><b class='flag-5'>調(diào)用</b>](https://file.elecfans.com//web2/M00/90/FA/poYBAGPsgDyAPpd2AACMnI3v9PI987.jpg)
評論