1. Introduction
1 @( X u& g8 m2 D: o0 q R2 C" u
l/ ^/ p+ ~/ ]1 w可变参数其实是标准C语言一个内建的功能,它和EFI本身并没有太多关系。但是它在EDK中有重新实现和使用,而且我们家的code base使用频繁,很多oem callback都使用了可变参数以此获得函数格式的统一以及参数传递的灵活性。所以我就提一下可变参数的实现,希望对Legacy BIOS转过来的以及对C不是很熟悉的朋友有所帮助。/ @% U' \( X: e4 t
; z( L. b! s! Y5 Z2. Function Call Impl
- ?# w0 ]2 D0 p: `9 v8 l
" Q% \! D: P# {要想搞明白可变参数的实现,那就肯定不能不提C的运行所必需的一个核心部件stack。离开了stack,C是没法活J,这也是为什么EFI code 只能99%而无法100%用C实现的其中一个原因(sec阶段需要准备好stack然后才可以交棒给C的code)。先看一看函数调用过程中stack的变化状况:
. f, m; t5 O. K8 Q$ L: B) q l/ }" }: O9 C/ W7 k- k
a = a; % ]# X2 A% |; g& ?2 O
b = b; 0 t$ ^! L( q/ H p& I- e
testr(); , l/ q& p9 _$ i/ Q
testq(1,2); testp调用testq,这时stack的状况如下图1所示
7 ^ e* T7 ]7 d g% I! z+ p: @
: H& D9 }) \; m+ f Z- C' Q
6 h4 J. H A* E* ]% m4 z& u1 i通常情况下stack由高地址向低地址增长,压进去一个参数esp就会减小,弹出当然就会增加而且通常会以机器字对齐。一个函数保存局部变量以及调用下一级函数所需要的stack空间被称作一个frame。如上图1所示以ebp所指向的地址为界,ebp上方的为一个frame,下方包括保存的testp的ebp为另一个frame。ebp的存在也方便了函数参数,和局部变量的存取。ebp+n即可取出参数,ebp-n取出局部变量。函数调用参数进栈的顺序与平台和编译器有关,但通常都是从右向左进栈,所以testp会将b先进栈,然后是a接下来保存返回地址(从testq返回时继续执行的位置)。了解了这些知识,就足以揭开可变参数的面纱了,下面就来看看可变参数的实现。# B/ \9 I( ?4 l# }; s5 L' @/ Q
" K* W5 t$ Q4 M# u O; h
3. VA_START, VA_ARG,VA_END
+ S+ L! q' u! P4 C4 O, q' V9 U n5 M: X% D' m
! q/ Y# I1 W9 \$ W8 }9 X
这三个宏就是可变参数的所有秘密所在了,所有的代码一共不超过十行,可是如果不清楚前面所提到到stack的布局,想搞明白这几行代码也不是很容易哦。翠花上codeJ,edk中的实现如下所示:
" _2 o1 E( M8 ?- N9 @# ^#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). R5 g. P6 S4 V' A7 f+ F
(*(t *) ((ap += _EFI_INT_SIZE_OF (t)) - _EFI_INT_SIZE_OF (t))) #define VA_END(ap) X5 h5 U r( Y% X, G
(ap = (VA_LIST) 0) 用一段测试sample code,演示和讲解一下可变参数的使用和原理
$ W( M6 l0 H. |
) o4 \8 l/ X0 |1 a, L! G% k" |# g9 YIN OEMCALLBACK
- F X1 a) G4 f9 k& b! K*this,
4 e- _# c( R! m' iIN UINT32" A" _5 T$ w, v" m" {/ J
NumOfArgs,
0 {! [! k p/ q, N7 e- y* _
...
2 ?1 w2 Y- l8 ^* C0 |' q9 v' {! }+ q- V)
/ H! V+ Z& o, K, n2 L$ A6 X
VA_LIST: n8 W* q& _- Z4 h
Marker; % f3 I- i- G& P! R
UINT32& d% j, ], w" i) X
Tmp; ) n: q. F& l' d: c2 R
UINT32! {. q2 ^; J8 P4 o+ i1 |! C. Z! z
Cont;
9 w3 S# X5 Y5 t$ w* lVA_START (Marker, NumOfArgs);
for(Cont = 0x00; Cont < NumOfArgs; ++Cont) 4 D4 U5 e. H" Y$ V Y) [
{
/ r. r( S @, W' z" QTmp = VA_ARG (Marker, UINT32);
$ l6 N. I/ ]% `/ A/ yprintf("The value is :%d,",Tmp);
% H. W/ \8 X: I) a5 N' b. {}
0 [; R2 W+ l- i8 ^" e+ ~7 a6 L. d
printf("\n");
5 @/ A _2 X5 t s" j4 SVA_END (Marker);
int main (int argc,char** argv)
8 }( e" t: W: l% v i) VOemCallBack(NULL,3,5,10,33);
}/ K& @' J! H0 k6 y/ J
先来看调用栈长的什么模样,再来分析实现原理吧,调用栈如下图2所示:' m6 q- u4 Y0 ^ |, V6 w4 m
( j {, C& m0 y/ Q9 A9 R. P
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,否则无法获得可变参数的首地址。 $ L# z) {8 f! Z( @# h
9 |* M; `! G4 S) A
以上就是可变参数的所有内容了,希望有人能够从中获得帮助,也不枉我一番辛苦。再写要吐血了,闪!+ R; j( o) K y5 @
9 w. Q8 |/ M2 O7 tPeter% g# N$ O+ S* p$ h
2009-10-22 |