注:本文档为《从0学x86操作系统》课程配套的学习文档,提供相应的辅助学习资料和答疑勘误。 有关该课程的信息,请点击这里访问:https://study.163.com/provider/1017884735/index.htm 在阅读本文档时,如有疑问和建议,欢迎在下方留言或者直接联系我。

本小节实现了%s的格式化输出,重点在可变参数的获取。

主要内容

可变参数的使用

在C语言中,支持一个函数的参数数量没有固定限制,典型的代表就是printf。
在定义这种类型的函数时,使用:函数名(参数1, 参数2, …)的形式,即在最后有名称的参数后面增加一个…的参数。
在函数内部,可以通过一定的方式取出这些没有名称的参数。流程依次为:

  • 定义va_list类型的变量,如ap,用于存放取可变参数的一些信息
  • 使用va_start(ap, 最后一个有名字的参数-参数2):初始化ap
  • 依次使用va_arg(ap, type)获取传入的可变部分的参数
  • 使用va_end(ap)结束可变参数的取参,释放相应的资源

具体的示例代码在课程视频中有展示,也可看以下的代码。

/ va_arg example /

include / printf /

include / va_list, va_start, va_arg, va_end /

int FindMax (int n, …) { int i,val,largest; va_list vl; va_start(vl,n); largest=va_arg(vl,int); for (i=1;ival)?largest:val; } va_end(vl); return largest; }

int main () { int m; m= FindMax (7,702,422,631,834,892,104,772); printf (“The largest value is: %d\n”,m); return 0; }

可变参数的处理分析

注:以下内容有部分内容是我自己分析得出的结论,正确性不知。
前面的章节中已经分析过了GCC对于函数调用时,参数传递是通过栈传递的,并且是先压最右边的参数,再压最左边的参数,即如下图所示,其对应的函数可能为:func(int b, int a)。
而对于一个具备可变参数的函数而言,例如func_arg(int a, ….),在进行func_arg(a, b)调用时,其栈实际上是和下图完全相同的。
image.png
当有更多参数时,如func_arg(a, b, c, d, e),则栈中压入的参数从上(高地址)到下(低地址)依次为e, d, c, b , a。在函数内部,只有func_arg(int a, ….)中的a参数有名字可以引用,而要获取其它参数,则需要借助a找到对应的栈位置,然后再在栈中依次往上(高地址)逐个去取出相应的参数。因此,在获得参数时:

  • 首先,使用va_list定义一个变量,如args,这个变量会被va_start()初始化。我猜测,在va_start()内部,可能维护了一个指针,指向了栈中参数所在的位置。例如,在调用时va_start(args, a),会将args指向了参数a的后一个参数(更高地址)。
  • 然后,要获取这个参数,通过va_arg(args, type)实现。其会将当前args指向的栈单元中的值取出来。取出来后转换成什么类型,则由type指定(内部可能做了强制类型转换)。
  • 取完之后,args内部的指针再继续往一个参数移动。

如此,通过反复地调用va_arg(args, type),args内部的指针在栈中不断地移动,就实现了遍历栈中所有参数,然后将参数依次取出来。

kernel_vsprintf分析

kernel_vsprintf内部定义一个简单的状态机对字符串进行格式化,具体为:

  • NORMAL:表示正在对普通的需要直接显示的字符进行处理,处理方式为直接写入字符串缓存中
  • READ_FMT:表示正在进行参数的转换处理,例如遇到了%s,正在从可变参数列表中取出字符串写入字符串缓存中

该函数功能类似于C库中的vsprintf,有关vsprintf的说明,请见:https://www.cplusplus.com/reference/cstdio/vsprintf/

参考资料