C51中的函数指针
概述
函數指針是C編程語言眾多難懂的特性之一。由于C編譯器對關于8051架構的獨特要求,函數指針和可重入函數需要克服更大的挑戰。這主要是因為函數參數傳遞的方式。
通常,(對于大多數非8051的芯片),函數參數是在棧上以壓入和彈出的匯編指令來完成。由于8051的棧大小有限(僅128字節,某些設備上更低至64字節),函數參數傳遞必須用不同的技術來傳遞。
英特爾為8051推出PL/ML-51編譯器時,他們引入了將參數存儲在固定內存位置的技術。當鏈接器被調用時,它會建立程序的調用樹,找出哪些函數參數是相互獨立的,然后覆蓋它們。這就是鏈接器OVERLAY指令的開始。
由于PL/M-51不支持函數指針,所以從未出現間接函數調用的問題。但是,關于C,問題更多。鏈接器如何“知道”哪些內存被間接函數使用?你又如何添加間接調用的函數進入調用樹?
這篇文檔解釋如何在C51程序中有效地使用函數指針。一些示例被用來闡釋討論的問題和解決方案。
具體來說,就是這下面這些被討論的主題:
- 轉換常量為一個指針
- 聲明函數指針
- C51中函數指針的問題
- 用OVERLAY指令修改調用樹
- 可重入函數指針
固定地址的指針
你可以輕松地將數字地址轉換成函數指針。這樣去做有很多原因。例如,你可能需要不用觸發CPU的復位線就復位目標和應用程序。你能夠用地址為0000h的函數指針來實現這。
你可能會用標準C特性來轉換0x0000為一個指向0地址函數的指針。例如,當你編譯下面的C代碼時…
((void (code *) (void)) 0x0000) ();…編譯器產生如下信息:
; FUNCTION main (BEGIN) ; SOURCE LINE # 3 0000 120000 LCALL 00H ; SOURCE LINE # 4 0003 22 RET ; FUNCTION main (END)這確實是我們期待的:LCALL 0。
轉換數字常量為函數指針是一件有技巧的事。下列關于上面函數調用的各部分的描述將幫助你理解如何更好地使用它們。
在上述函數調用中,(void(*)void))是一個數據類型:一個指向函數的指針,這個函數不帶參數,返回void。
0x0000是轉換的地址。在類型轉換之后,函數指針指向0x0000地址。注意,我們使用圓括號包圍數據類型和0x0000。這不是必須的,如果我們只是想轉換0x000到一個函數指針。但是,由于我們將要調用這個函數,這些括號是必需的。
轉換數字常量至一個指針并不像通過指針調用一個函數。為此,我們必須指定一個參數列表。那就是在這一行末尾的()。
注意,在這個表達式中的所有括號都是必需的。括號分組和優先級也是重要的。上面的指針和指向帶參數函數的指針之間的唯一的不同是數據類型和參數列表。例如,這下面的函數調用…
((long (code *) (int, int, int)) 0x8000) (1, 2, 3);…調用一個地址為0x8000的函數,接收3個int參數并且返回long。
無參數的函數指針
函數指針是一個指向函數的變量。這個變量的值是函數的地址。例如,下列函數指針的聲明…
void (*function_ptr) (void);…是一個調用function_ptr的指針。用下面的代碼來調用function_ptr指向的函數。
(*function_ptr) ();由于function_ptr指向的函數沒有一個參數被傳遞。這也是為什么參數列表為空。function_ptr的地址可以被賦值,當它被聲明的時候。
void (*function_ptr) (void) = another_function;或者,它也可以在程序執行中被賦值。
function_ptr = and_another_function;重要的是要注意,你必須給函數指針賦值一個地址。如果你沒有這樣做,這指針的值可能是0(如果你幸運的話)或者它可能是完全不確定的值,具體取決于你使用數據內存的方式。如果一個指針沒有初始化,當你間接通過它調用函數時,你的程序可能崩潰。
要聲明一個有返回類型的函數指針,你必須在聲明時指明返回類型。例如,下面的聲明將上面的聲明改為一個指向返回float類型的函數。
float (*function_ptr) (void) = another_function;很簡單。只要記住括號在哪里就行了。
帶參數的函數指針
帶參數的函數指針和不帶參數的函數指針類似。例如:
void (*function_ptr) (int, long, char);…是一個帶有int,long和char作為參數的函數指針。用下面的代碼調用function_ptr指向的函數。
(*function_ptr)(12, 34L, 'A');注意,函數指針可能只能指定帶3個或更少參數的函數。這是因為間接調用函數的參數必須駐存寄存器。有關使用多于3個參數的函數指針,參見可重入函數。
使用函數指針的注意事項
如果你在C51程序中使用函數指針,這里有幾個你必須注意的事項。
參數列表限制
通過函數指針傳遞到函數的參數必須全部填充進寄存器。最多3個參數可以自動在在寄存器中傳遞。不要認為任意3個數據類型都可以。
由于C51至少可以通過寄存器傳遞3個參數。除非函數指定了更多參數,否則使用內存空間傳遞參數不是什么問題。如果是這種情況,你可以合并參數進一個結構體,然后傳遞指數結構體的指針。如果這不可接受,你可以使用可重入函數(參見下文)
調用樹的保存
C51工具鏈不會將函數參數壓入堆棧(除非可重入函數被調用)。相反,函數參數和自動變量(局部變量)是存儲在寄存器中或在固定的內存位置。這會防止函數的可重入。例如,如果一個函數調用它自己,它將覆蓋它自己的參數或局部變量。這個重入的問題通過reentrant關鍵字解決(參見下文)。另一個非可重入的副作用是函數指針能夠,而且經常帶來實現的問題。
為了保存盡可能多的數據空間,鏈接器執行調用樹分析來確定一些內存空間是否要以安全地被覆蓋。
例如,如果你的應用程序包括main函數、函數a、函數b和函數c;然后如果main調用a,b,c;并且a,b,c沒有調用其他函數(也沒有調用彼此);然后關于你的應用程序的調用樹如下:
然后,被A,B和C使用的內存可以被安全覆蓋。
當調用樹不能被正確的構建時,關于函數指針的問題就出現了。這原因是鏈接器不能確定函數指針是引用哪個函數。這里沒有自動的方法來解決這個問題。但是這里有一個手動的,盡管有點麻煩。
這下面的兩個源文件幫助說明問題并且使得解決方案更容易理解。這第一個源文件FPCALLER.C,包含一個函數,這個函數通過函數指針(fptr)調用另外一個函數。
void func_caller (long (code *fptr) (unsigned int)) {unsigned char i;for (i = 0; i < 10; i++){(*fptr) (i);} }第二個源文件FPMAIN.C,包含main C函數,也包括通過func_caller(上面定義)間接調用的函數。注意,main調用func_caller并且傳遞一個函數的地址作為參數。
extern void func_caller (long (code *) (unsigned int)); int func (unsigned int count) {long j;long k;k = 0;for (j = 0; j < count; j++){k += j;}return (k); } void main (void) {func_caller (func);while (1) ; }上述兩個文件編譯沒有錯誤。它們鏈接也沒有錯誤。這下面的調用是通過鏈接器在map文件中產生的。
SEGMENT DATA_GROUP +--> CALLED SEGMENT START LENGTH ------------------------------------------------- ?C_C51STARTUP ----- ----- +--> ?PR?MAIN?FPMAIN ?PR?MAIN?FPMAIN ----- ----- +--> ?PR?_FUNC?FPMAIN +--> ?PR?_FUNC_CALLER?FPCALLER ?PR?_FUNC?FPMAIN 0008H 000AH ?PR?_FUNC_CALLER?FPCALLER 0008H 0003H盡管這是個簡單的示例,但是依然可以從調用樹中獲取很多信息。
?C_C51STARTUP段調用MAIN C函數,那是?PR?MAIN?FPMAIN段。這個段名的組成部分可以被解碼為:PR是PRogram內存,MAIN是函數名,FPMAIN是函數定義所在的源文件的名字。
MAIN函數調用FUNC和FUNC_CALLER(通過調用樹)。注意,這不是正確的。MAIN從沒有調用FUNC。但是它的確傳遞了FUNC的地址給FUNC_CALLER。同樣要注意,通過調用樹,FUNC_CALLER并沒有調用FUNC。這是因為它是通過函數指針間接調用的。
在FPMAIN中的FUNC函數用開始于0008h的000Ah個字節數據。在FPCALLER中的FUNC_CALLER用起始于0008h的0003h個字節數據。這是重要的!
FUNC_CALLER用起始于0008h的內存,FUNC同樣用起始于0008h的內存。由于FUNC_CALLER調用FUNC,并且兩個函數都用相同的內存區域,然后我們出現問題了。當FUNC被調用時(被FUNC_CALLER),它會破壞FUNC_CALLER使用的內存。這是如何發生的呢?Keil C51編譯器和鏈接器不起作用了嗎?
這個問題的原因是函數指針。無論你什么時候使用函數指針,你一直會有類似的問題。幸運的是,它們很容易被修復。OVERLAY鏈接指令可以讓你在調用樹中指定函數是如何鏈接到一起的。
為了修正上面的調用樹,針對FUNC的調用必須從MAIN函數移出,并且在FUNC_CALLER中插入對FUNC的調用。這下面的OVERLAY命令正是這樣做的。
OVERLAY (?PR?MAIN?FPMAIN ~ ?PR?_FUNC?FPMAIN,?PR?_FUNC_CALLER?FPCALLER ! ?PR?_FUNC?FPMAIN)為了移除或插入對調用樹的引用,需要首先指定調用者然后是被調用者。波浪線(~)是移除引用或調用,感嘆號(!)是添加引用或調用。例如,?PR?MAIN?FPMAIN ~ ?PR?_FUNC?FPMAIN移除從MAIN中對FUNC的調用。
通過OVERLAY指令修正調用樹的鏈接命令被調整之后,map文件中顯示如下信息:
SEGMENT DATA_GROUP +--> CALLED SEGMENT START LENGTH ------------------------------------------------- ?C_C51STARTUP ----- ----- +--> ?PR?MAIN?FPMAIN ?PR?MAIN?FPMAIN ----- ----- +--> ?PR?_FUNC_CALLER?FPCALLER ?PR?_FUNC_CALLER?FPCALLER 0008H 0003H +--> ?PR?_FUNC?FPMAIN ?PR?_FUNC?FPMAIN 000BH 000AH然后,這調用樹現在被修正了,變量FUNC和FUNC_CALLER被分離在不同的空間(不再覆蓋了)。
函數指針表
下面的內容是典型的函數指針表定義:
long (code *fp_tab []) (void) = { func1, func2, func3 };如果你的main C函數通過fp_tab調用函數,那么這鏈接map將出現如下信息:
SEGMENT DATA_GROUP +--> CALLED SEGMENT START LENGTH ---------------------------------------------- ?C_C51STARTUP ----- ----- +--> ?PR?MAIN?FPT_MAIN +--> ?C_INITSEG ?PR?MAIN?FPT_MAIN 0008H 0001H ?C_INITSEG ----- ----- +--> ?PR?FUNC1?FP_TAB +--> ?PR?FUNC2?FP_TAB +--> ?PR?FUNC3?FP_TAB ?PR?FUNC1?FP_TAB 0008H 0008H ?PR?FUNC2?FP_TAB 0008H 0008H ?PR?FUNC3?FP_TAB 0008H 0008H通過表來調用這3個函數,func1,func2,func3,看起來像通過?C_INITSEG調用。但是,這并不是正確的。?C_INITSEG是初始化你代碼變量的例程。這些函數只是在初始化代碼中被引用,因為函數指針表是通過這些函數的地址初始化的。
注意,這些通過main C函數,同樣還有func1,func2,func3作為變量被用的起始區域都是起始于0008h。這是無法運行的,因為main C函數調用func1、func2和fun3(通過函數指針表)。并且,在func1等函數中被用到的變量會覆蓋那些在main中被用到的。
當你使用函數指針表時,C51編譯器和BL51鏈接器組合工作,讓覆蓋函數變量空間變得很容易。但是,你必須恰當地聲明函數指針表。如果你這樣做,你能夠避免使用OVERLAY指令。這下面的是一個函數指針表定義,C51和BL51可以自動處理。
code long (code *fp_tab []) (void) = { func1, func2, func3 };注意,這唯一不同的是表格存儲在代碼空間。
現在,鏈接map顯示如下:
現在,這里沒有來自從初始化代碼中對func1、func2和func3的引用。相反,這里有個從main到FP_TAB的引用。
這是一個函數指針表,由于函數指針表引用了func1、func2和func3,因此這調用樹是正確的。
只要這函數指針表是存放在獨立的源文件中,C51和BL51可以為你讓所一切在調用樹中正確地鏈接。
函數指針建議和技巧
這里有一些函數指針的技巧,可以使得你讓事情變得更容易。
用指定內存指針
將函數指針從通用指針轉換成指定內存指針。這將為每個指針節省1個字節。到目前為止,示例使用的都是通用函數指針。由于函數僅僅駐留在代碼內存(在8051上),因此可以將函數聲明為Code類型指針來節省1個字節。例如:
void (code *function_ptr) (void) = another_function;如果你在你的函數指針聲明中選擇包含一個code關鍵字,那么請確保所有的地方都這樣用。如果你聲明一個3個字節的函數指針,
并且傳遞指定內存2個字節的函數指針,糟糕的事情就會發生!
可重入函數和指針
Keil C51為可重入的函數提供reentrant關鍵字??芍厝牒瘮灯谕麉凳峭ㄟ^模擬棧來傳遞。
棧是在用于small memory model的IDATA、用于compact memory model的PDATA或用于large memory model的XDATA上來維持的。如果你使用可重入函數,你必須在STARTUP.A51中初始化可重入棧指針。參見下列從啟動代碼中摘取的信息。
你必須設置你使用哪個內存模型棧,并設置棧頂部。當有元素被壓入棧,可重入棧指針減小(向下移動)。一個保存內部數據內存的小技巧是放置所有的可重入函數在獨立的內存模型中,如large或compact。
要聲明可重入函數,用reentrant關鍵字。
void reentrant_func (long arg1, long arg2, long arg3) reentrant { }要聲明一個large model的可重入函數,使用large和reentrant關鍵字。
void reentrant_func (long arg1, long arg2, long arg3) large reentrant { }要聲明一個指定可重入函數的指針,你必須同樣使用reentrant關鍵字。
void (*rfunc_ptr) (long, long, long) reentrant = reentrant_func;聲明可重入函數指針和非可重入函數指針并沒有太多不同。
當使用可重入函數指針時,因為參數必須壓入模擬棧,所以更多的代碼會產生。但是,沒有鏈接控制需要指定,你也不必須混亂地使用OVERLAY指令。
如果你使用間接調用地方式傳遞多于3個參數到函數,那么可重入函數指針是需要的。
結論
如果你注意鏈接器調用樹并且確保使用OVERLAY修正任何不一致的情況,那么函數指針是非常有用的并且也不是特別難用。
總結
- 上一篇: 解决runtime error R602
- 下一篇: Apriori FP-growth 详细