1. Introduction. p( g ^4 B& o
2 q( w, @) z$ j" K5 M' E, }# ]可变参数其实是标准C语言一个内建的功能,它和EFI本身并没有太多关系。但是它在EDK中有重新实现和使用,而且我们家的code base使用频繁,很多oem callback都使用了可变参数以此获得函数格式的统一以及参数传递的灵活性。所以我就提一下可变参数的实现,希望对Legacy BIOS转过来的以及对C不是很熟悉的朋友有所帮助。
) Q8 P7 s& l% Z' I. z+ I7 L- ]
% M5 D$ B& n! ]8 T* A# R2. Function Call Impl
8 ?7 L0 ~3 r0 f
- C- T) {" H( `- \8 Y要想搞明白可变参数的实现,那就肯定不能不提C的运行所必需的一个核心部件stack。离开了stack,C是没法活J,这也是为什么EFI code 只能99%而无法100%用C实现的其中一个原因(sec阶段需要准备好stack然后才可以交棒给C的code)。先看一看函数调用过程中stack的变化状况:
! i0 T( W8 W3 A0 Y# s# H9 B4 I' q& u, m1 d) h
a = a; A. O5 C5 r2 Z5 }% J/ q
b = b; 4 m! ~' \( w p, D E& [' B
testr(); , s) s( n4 N, y/ h6 b0 U1 x
testq(1,2); testp调用testq,这时stack的状况如下图1所示
: d/ S3 Q3 q- g) P- V# [$ P- G3 m4 u9 l, F% z' j8 y3 q
+ ~0 D( x- g s4 S8 y; R- g' q" C. N0 z8 E+ E1 p0 x9 D: @5 d
通常情况下stack由高地址向低地址增长,压进去一个参数esp就会减小,弹出当然就会增加而且通常会以机器字对齐。一个函数保存局部变量以及调用下一级函数所需要的stack空间被称作一个frame。如上图1所示以ebp所指向的地址为界,ebp上方的为一个frame,下方包括保存的testp的ebp为另一个frame。ebp的存在也方便了函数参数,和局部变量的存取。ebp+n即可取出参数,ebp-n取出局部变量。函数调用参数进栈的顺序与平台和编译器有关,但通常都是从右向左进栈,所以testp会将b先进栈,然后是a接下来保存返回地址(从testq返回时继续执行的位置)。了解了这些知识,就足以揭开可变参数的面纱了,下面就来看看可变参数的实现。
: l! b% u! G% J' Q: u( x* f
9 b6 v- ]2 {+ E [3. VA_START, VA_ARG,VA_END; l) X {, }! J
- j/ b- e8 ?5 G! i) h
; y8 A& Y0 A: I2 w& g这三个宏就是可变参数的所有秘密所在了,所有的代码一共不超过十行,可是如果不清楚前面所提到到stack的布局,想搞明白这几行代码也不是很容易哦。翠花上codeJ,edk中的实现如下所示:
- m+ Z9 u+ I; u7 f' ]8 P" t; M+ U#define _EFI_INT_SIZE_OF(n) ((sizeof (n) + sizeof (UINTN) - 1) &~(sizeof (UINTN) - 1)) // Also support coding convention rules for var arg macros #define VA_START(ap, v) (ap = (VA_LIST) & (v) + _EFI_INT_SIZE_OF (v)) #define VA_ARG(ap, t)4 M& N* b" r3 s
(*(t *) ((ap += _EFI_INT_SIZE_OF (t)) - _EFI_INT_SIZE_OF (t))) #define VA_END(ap)
* n( O# ?, K# I. K$ Z! u# D1 Y(ap = (VA_LIST) 0) 用一段测试sample code,演示和讲解一下可变参数的使用和原理
. ~9 z) B; D: ]7 W2 M" N
# _ f0 P6 L7 \; dIN OEMCALLBACK
# j$ R' E" E0 n: P*this,
2 b% t6 p' L4 C N/ f
IN UINT32) ^: Z# Z8 q8 n% z6 z1 {5 U$ E
NumOfArgs, 2 t* q& ?# r+ R: ?$ O; @
...
( I4 f! t; L" E- C! b; d)
& I. t1 v8 d8 xVA_LIST) T/ M1 v3 c. _( F
Marker;
2 ~( N* P$ m/ Z8 F( c1 J
UINT32
& H& Q: J- a* l I. wTmp;
9 E& d* a0 V$ j; ^+ S s& \1 @UINT32
$ ^$ [' W& H4 YCont;
+ E1 ~! m) E) e. t3 F
VA_START (Marker, NumOfArgs);
$ |# I5 V- q! Z8 ~+ S- W4 H0 d4 `
for(Cont = 0x00; Cont < NumOfArgs; ++Cont) % o) [ f9 D9 F
Tmp = VA_ARG (Marker, UINT32); ) A: Q: T$ q( T4 T7 D7 l4 `
printf("The value is :%d,",Tmp);
7 K2 V* d! c( n2 t$ E+ @7 m}
5 ~, J# \. {9 R4 a& w o- pprintf("\n");
6 F& k$ |8 ?# O. T% I n4 ^VA_END (Marker);
int main (int argc,char** argv)
( \/ K6 @% x9 C7 R! M# VOemCallBack(NULL,3,5,10,33);
}
6 N( q# c0 Q2 T$ l先来看调用栈长的什么模样,再来分析实现原理吧,调用栈如下图2所示:0 U1 I+ D6 q- u
) s5 `& [( N- _8 @VA_START展开以后就是(Marker = (VA_LIST) & (NumOfArgs) + _EFI_INT_SIZE_OF (NumOfArgs))也即求出NumOfArgs之后的参数的地址,图中红色部分,也就是可变参数列的首地址。VA_ARG展开以后就有点意思了:(*(UINT32 *) ((Marker += _EFI_INT_SIZE_OF (UINT32)) - _EFI_INT_SIZE_OF (UINT32)))这里就是defrence出当前Marker指向的地址的t类型的值,并且移动Marker指针为下一轮做准备,这就是“Marker += _EFI_INT_SIZE_OF (UINT32)”奥妙所在。这样逐次移动Marker指针就可以遍历出所有的可变参数了。VA_END就没什么好说的了,防止出现野指针:Marker = (VA_LIST) 0。最后一个_EFI_INT_SIZE_OF它是为了特定平台的内存对齐的需要,因为这个UINTN在不同的平台下大小不同,所以使用这个宏会将内存对齐到一个机器字。关于可变参数还有要特别强调的地方就是:一定要有结束标识,否则程序无法识别参数的个数,OemCallBack中的NumOfArgs就给出了参数的个数,另外就是至少要有一个不变的参数J,否则无法获得可变参数的首地址。 9 ?# Y" ?/ S$ G1 k' b
7 j& |- R# L6 K+ P6 j+ Y以上就是可变参数的所有内容了,希望有人能够从中获得帮助,也不枉我一番辛苦。再写要吐血了,闪!' J# j7 ], c" e: L
: a9 Z, v6 ^' ~ t, L Z. l! `Peter
# T& ` E" w5 Q/ X) X2009-10-22 |