stdarg(3) variable argument lists 可變參數列表

1. 可變參數列表的實現

GCC 編譯器在彙編過程中,調用 C 語言函數傳遞參數有兩種方法:

  1. 通過堆棧
  2. 通過寄存器(默認)

若想通過堆棧傳遞參數,需在定義 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. 宏的簡要處理過程

對可變參數列表的處理過程一般爲:

  1. 用 va_list 定義一個可變參數列表
  2. 用 va_start 獲取函數可變參數列表
  3. 用 va_arg 循環處理可變參數列表中的各個可變參數
  4. 用 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() 可以是宏或者函數。

10. 參考

C語言函數之可變參數原理:va_start、va_arg及va_end

va_start va_arg va_end 的原理與實例

printf,sprintf,vsprintf 區別

關於va_list的_INTSIZEOF(n)的疑問

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