1. 可變參數列表的實現
GCC 編譯器在彙編過程中,調用 C 語言函數傳遞參數有兩種方法:
- 通過堆棧
- 通過寄存器(默認)
若想通過堆棧傳遞參數,需在定義 C 函數時在函數前加上宏 asmlinkage
asmlinkage int printk(const char *fmt, ...)
正常來講,函數原型中具有確定的參數類型和數量,保證了函數調用的準確性。
如果在調用函數時,使用不同類型的不同數量的參數進行調用,參數列表的數量和類型對於被調用函數是未知的。
我們就要想辦法確定各個可變參數的類型,找到這些可變參數的地址。
頭文件 <stdarg.h>
定義了 va_list 類型和用於逐個通過參數列表的三個宏。
被調用函數必須聲明一個 va_list 類型的對象,用 va_list 這個指針指向這個可變參數列表的各個參數。
這個 va_list 對象由 va_start(), va_arg() 和 va_end() 使用。
2. 函數調用時的棧結構
C 函數調用時的棧結構:
棧結構 | 說明 |
---|---|
棧底 | 高地址(入棧方向爲從高地址到低地址) |
…… | …… |
函數返回地址 | …… |
…… | …… |
函數最後一個可變參數 | 入棧順序爲從右向左 |
…… | …… |
函數第一個可變參數 | 調用 va_start 後 ap 指向這裏 |
函數最後一個固定參數 | …… |
…… | …… |
函數第一個固定參數 | …… |
棧頂 | 低地址 |
3. 可變參數列表(第一個可變參數)的地址
我們需要一個基準,這個基準就是可變參數前面固定的那些參數中的最後一個,也即可變參數前面那個參數。
然後以此已知類型的參數爲基準,來確定後面的可變參數。
先確定第一個可變參數的地址,方法是調用void va_start(va_list ap, last);
這樣,基準有了,就是 last 指定的參數,然後由 last 計算 指向當前可變參數的指針 ap
要找到 ap 指向的第一個可變參數的地址,則依據棧結構知道,第一個可變參數的地址等於最後一個固定參數的地址加上偏移
此時,ap 指向可變參數列表中的第一個參數
4. 可變參數列表中隨後的可變參數的獲取
我們需要指定參數的類型,取得指向指定類型的指針,來獲取各個可變參數
方法是調用va_arg(va_list ap, type);
宏,
先得到當前參數地址,返回強制轉換成指向此參數的類型的指針,以後間接訪問即可。
最後,用va_end(ap),初始化 ap 可變參數指針,保持健壯性。
5. 宏的簡要處理過程
對可變參數列表的處理過程一般爲:
- 用 va_list 定義一個可變參數列表
- 用 va_start 獲取函數可變參數列表
- 用 va_arg 循環處理可變參數列表中的各個可變參數
- 用 va_end 結束對可變參數列表的處理
6. 各個宏調用的語法
#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
7. Linux 內核中的宏定義
#ifndef va_arg
#ifndef _VALIST
#define _VALIST
typedef char *va_list;
#endif /* _VALIST */
/*
* Storage alignment properties
*/
#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)
/*
* Variable argument list macro definitions
*/
#define _bnd(X, bnd) ( ((sizeof (X)) + (bnd)) & (~(bnd)) )
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
#define va_arg(ap, T) (*(T *) (((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap) (void) 0
#endif /* va_arg */
8. 宏定義的另一種表達方式
#define _ADDRESSOF(v) ( &(v) )
/* 按 int 的倍數進行字節對齊 */
#define _INTSIZEOF(n) (( sizeof(n) + sizeof(int) - 1 ) & ~(sizeof(int) - 1))
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)( (ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
9. 宏的詳細說明
1. va_list
va_list表示可變參數列表類型,實際上就是一個 char 指針
2. va_start( va_list ap, last )
va_start 用於獲取函數參數列表中可變參數的首指針,即獲取函數可變參數列表
參數 | 作用 |
---|---|
va_list ap | 保存函數參數列表中可變參數的首指針(即,可變參數列表 |
char *last | 函數參數列表中可變參數列表前最後一個固定參數的名字,也即,調用函數的知道其類型的最後一個參數 |
va_start()宏初始化 ap 供 va_arg() 和 va_end() 隨後使用,並且 va_start() 必須首先被調用
由於此參數的地址可能在 va_start 宏中使用,它不應被聲明爲寄存器變量或者作爲一個函數或者數組類型。
3. va_arg( va_list ap, type )
va_arg 用於獲取當前 ap 所指的可變參數並將並將ap指針移向下一可變參數
參數 | 作用 |
---|---|
va_list ap | 指向當前正要處理的可變參數 |
type | 正要處理的可變參數的類型 |
返回值 | 當前可變參數的值 |
在C/C++中,默認調用方式_cdecl是由調用者管理參數入棧操作,且
入棧順序爲從右至左
入棧方向爲從高地址到低地址
因此,第1個參數到第n個參數被存放在地址遞增的堆棧裏。
函數的可變參數列表的地址 = 函數參數列表中最後一個固定參數的地址 + 第一個可變參數對其的偏移量(va_start的實現);
下一可變參數的地址 = 當前可變參數的地址 + 下一可變參數對其的偏移量(va_arg的實現)。
這裏提到的偏移量並不一定等於參數所佔的字節數,
而是爲參數所佔的字節數再擴展爲機器字長(acpi_native_int)倍數後所佔的字節數(因爲入棧操作針對的是一個機器字),
這也就是爲什麼_bnd那麼定義的原因。
va_arg() 宏展開爲一個表達式,這個表達式具有調用中的下一個參數的類型和值。
這裏的 ap 參數即是由 va_start() 初始化過的 va_list ap.
每次對 va_arg() 的調用都修改 ap 參數,以便下次調用能夠返回下個參數。
參數 type 即是指定的類型名,這樣,指向特定類型對象的指針的類型可以簡單的通過在類型後添加 * 操作符得到。
va_start() 宏使用後的 va_arg() 宏的第一次使用返回 last 後面的參數
連續的調用返回剩餘參數的值。
如果沒有下一個參數,或者如果類型和實際的下一個參數的類型不匹配(按照默認參數提升進行的提升),則會產生隨機錯誤。
如果 ap 被傳遞給使用 va_arg(ap, type) 的函數,那麼在這個函數返回後,ap 的值是未定義的。
4. va_end( va_list ap )
va_end 用於結束對可變參數的處理。實際上,va_end被定義爲空.它只是爲實現與va_start配對(實現代碼對稱和”代碼自注釋”功能)
每個 va_start() 調用必須匹配同一個函數中的 va_end() 的相關調用。
調用 va_end() 後,變量 ap 是未定義的。
列表的多重遍歷是可能的,每個列表用 va_start() 和 va_end() 括起來。
va_end() 可以是宏或者函數。