linux0.12之内核代码之『深入追踪变参函数的实现』

在内核段要显示,需要printk.c函数集
看看printk这个函数

int printk(const char *fmt, ...)
{
 va_list args;
 int i;

 va_start(args, fmt);
 i=vsprintf(buf,fmt,args);
 va_end(args);
 console_print(buf);
 return i;
}

看看其中的变量以及函数
首先printk是一个变参函数,这里深入理解变参函数的构成
这里涉及一个头文件stdarg.h
1、
typedef char *va_list;
这个类型的定义
2、

#ifndef __sparc__
#define va_start(AP, LASTARG) \
 (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#else
#define va_start(AP, LASTARG) \
 (__builtin_saveregs (),     \
  AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#endif

看一看最核心的

#define va_start(AP, LASTARG) \
 (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))

是一个宏,
里面又有一个宏

#define __va_rounded_size(TYPE)  \
  (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))

不管这个函数是什么实践一下
看一个测试代码

#include <iostream>
using namespace std;

#define __va_rounded_size(TYPE)  \
 (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))

#define va_start(AP, LASTARG) \
 (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))

int test(const char* fmt,char a)
{
 char* args;
 char buff;
 int cnt;
 int size;
 va_start(args,fmt);
 cnt=__va_rounded_size(fmt);
 buff=*args;
 size=sizeof(fmt);

 return 0;
}
int main()
{
 test("test fmt",'a');

 return 0;
}
  • fmt 0x01087800 “test fmt” const char *
    fmt里面是存放的字符串的地址
  • &a 0x0025f694 “a” char *
  • &fmt 0x0025f690 const char * *
    这两个变量是压栈的地址
    经过va_start之后
  • args 0x0025f694 “a” char *
    指向函数下一个参数a的入栈地址
    cnt 4 int
    那么我们的这个就明白了
    实际就是计算参数fmt占用的栈的大小,然后计算后指针指向下一个参数
    对于这个宏
    __va_rounded_size的作用,
    是一个对齐,这里是4字节对齐,不足4字节的按照4字节对齐
    打个比方,对于十进制,我们要求值10对齐,也就是
    1 ->10 《==(1+9)/10×10
    11->20 《== (11+9)/10*10
    45->50 《== (45+9)/10×10
    如何计算,根据十进制加法满10进1
    将这个数加9 然后除10取模,再乘以10.这样就就可以了
    这里写图片描述
    同样在这段代码中也是可以的这么去实现的。

由上面的可知
va_start(args, fmt);
就是将指针args指向第二个参数。

下面看下一句代码
i=vsprintf(buf,fmt,args);
首先在printf.c中
static char buf[1024];
一个静态变量
然后用到vsprintf函数,这个函数在vsprintf.c中,很长一段函数代码。一步一步看。
这个函数主体代码块就是一个for循环,对传进来的参数进行扫描一次
这里写图片描述
展开for循环
第一个代码块
if (*fmt != ‘%’) {
*str++ = *fmt;
continue;
}
寻找第一个%的位置,之前的字符都保存到str指向的缓冲区中,
continue是为了满足if条件时,只执行if语句。%字符不保存到缓冲区里。
检测到%继续向下执行。可想而知,if下面的代码都是对%后面的字符串操作
第二个代码块

/* process flags */
  flags = 0;
  repeat:
   ++fmt;    /* this also skips first '%' */
   switch (*fmt) {
    case '-': flags |= LEFT; goto repeat;
    case '+': flags |= PLUS; goto repeat;
    case ' ': flags |= SPACE; goto repeat;
    case '#': flags |= SPECIAL; goto repeat;
    case '0': flags |= ZEROPAD; goto repeat;
    }

到这是停一下,需要学习一下%格式输出的知识
见笔记【%格式输出】
这里写图片描述
大致知道其完整格式是这样的,所以猜测linus会分5个代码块来解决这个问题
上面的switch代码块就是检测flags的,只要有这些字符,会repeat执行,然后将这些属性与操作。
属性宏定义为

#define ZEROPAD  1   /* pad with zero */
#define SIGN           2     /* unsigned/signed long */
#define PLUS          4  /* show plus */
#define SPACE        8   /* space if plus */
#define LEFT           16    /* left justified */
#define SPECIAL     32   /* 0x */

第三个代码块,解决width的问题

/* get field width */
  field_width = -1;
  if (is_digit(*fmt))
   field_width = skip_atoi(&fmt);
  else if (*fmt == '*') {
   /* it's the next argument */
   field_width = va_arg(args, int);
   if (field_width < 0) {
    field_width = -field_width;
    flags |= LEFT;
   }
  }

其中field_width = -1;是将宽度初始化未设置
再分析,
1、#define is_digit(c) ((c) >= '0' && (c) <= '9')
检测是否在字符‘0’到‘9’
2、

static int skip_atoi(const char **s)
{
 int i=0;

 while (is_digit(**s))
  i = i*10 + *((*s)++) - '0';
 return i;
}

这是一个静态函数,不允许其他文件访问
这就是一个将字符串数,转换成整数的过程。这个写法也是很规范的,学习一下

#include <iostream>
#include <algorithm>
using namespace std;

#define d(c) ((c>='0')&&(c<='9'))

int a_i(const char* ai)
{
 int back=0;
 while(d(*ai))
 {
  back=back*10+((*ai++)-'0');
 }
 return back;
}

int main()
{
 char *b="1890an";
 a_i(b);
}

上面是本人利用一级指针写的,当初linus为什么用二级指针写呢
在oldlinux中有人提到过这个问题
http://www.oldlinux.org/oldlinux/viewthread.php?tid=5228
一级指针传递时,原指针没有变化,只是复制了一个指针来操作字符串。而二级指针是直接对原指针操作,在函数结束时,原指针也是向前推进的。这两种方法都能实现同样的字符转整数的功能,但是差别就是在那个地方,
3、

#define va_arg(AP, TYPE)     \
 (AP += __va_rounded_size (TYPE),    \
  *((TYPE *) (AP - __va_rounded_size (TYPE))))

调用这个宏时,是检测到‘’,就是宽度由下一个整数参数指明,就是args指向的。
这是一个逗号操作,

下一个代码块

/* get the precision */
  precision = -1;
  if (*fmt == '.') {
   ++fmt;
   if (is_digit(*fmt))
    precision = skip_atoi(&fmt);
   else if (*fmt == '*') {
    /* it's the next argument */
    precision = va_arg(args, int);
   }
   if (precision < 0)
    precision = 0;
  }

和上一个模块差不多
下一个代码块

/* get the conversion qualifier */
  qualifier = -1;
  if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L') {
   qualifier = *fmt;
   ++fmt;
  }
  switch (*fmt) {
  case 'c':
   if (!(flags & LEFT))
    while (--field_width > 0)
     *str++ = ' ';
   *str++ = (unsigned char) va_arg(args, int);
   while (--field_width > 0)
    *str++ = ' ';
   break;
  case 's':
   s = va_arg(args, char *);
   len = strlen(s);
   if (precision < 0)
    precision = len;
   else if (len > precision)
    len = precision;
   if (!(flags & LEFT))
    while (len < field_width--)
     *str++ = ' ';
   for (i = 0; i < len; ++i)
    *str++ = *s++;
   while (len < field_width--)
    *str++ = ' ';
   break;
  case 'o':
   str = number(str, va_arg(args, unsigned long), 8,
    field_width, precision, flags);
   break;
  case 'p':
   if (field_width == -1) {
    field_width = 8;
    flags |= ZEROPAD;
   }
   str = number(str,
    (unsigned long) va_arg(args, void *), 16,
    field_width, precision, flags);
   break;
  case 'x':
   flags |= SMALL;
  case 'X':
   str = number(str, va_arg(args, unsigned long), 16,
    field_width, precision, flags);
   break;
  case 'd':
  case 'i':
   flags |= SIGN;
  case 'u':
   str = number(str, va_arg(args, unsigned long), 10,
    field_width, precision, flags);
   break;
  case 'n':
   ip = va_arg(args, int *);
   *ip = (str - buf);
   break;
  default:
   if (*fmt != '%')
    *str++ = '%';
   if (*fmt)
    *str++ = *fmt;
   else
    --fmt;
   break;
  }
} 

这里就是常见的输出值了
挑一个c看看
其输出代码为

switch (*fmt) {
  case 'c':
   if (!(flags & LEFT))
    while (--field_width > 0)
     *str++ = ' ';
   *str++ = (unsigned char) va_arg(args, int);
   while (--field_width > 0)
    *str++ = ' ';
   break;

第一句
if (!(flags & LEFT))
while (–field_width > 0)
*str++ = ’ ‘;
检测是否左对齐,如果不是,根据数据宽度,将对应的数存到缓冲区间里面
第二句
*str++ = (unsigned char) va_arg(args, int);
将变参后面的参数缓存到缓冲区中,这里有一个问题
就是args需要每次缓存后需要指向下一个参数,什么时候args变化的?
搜索之后args只是在va_arg函数中使用。
重新看看这个宏

#define va_arg(AP, TYPE)     \
 (AP += __va_rounded_size (TYPE),    \
  *((TYPE *) (AP - __va_rounded_size (TYPE))))

现将AP加一个地址(和TYPE相关),返回原地址取值,最终AP的值是变化的。

vprintf还有最后一个问题
return str-buf;
两个地址相减 ,实际就是相对地址,str为缓冲区的现在指向的指针,buff是基地址。得到缓存的大小(字节)

到此vprintf简单分析完成

printk下一句代码
va_end(args);
没有看到源码。
谷歌上找到

#define va_end(ap) ap = (va_list)0  /* 将ap置空 */

实际就是一个指针释放的作用。

printk最后一个调用函数
console_print(buf);
传入参数就是要显示的缓存内存地址,进入这个函数
console_print函数源代码如下

void console_print(const char * b)
{
 int currcons = fg_console;
 char c;
 while (c = *(b++)) {
  if (c == 10) {
   cr(currcons);
   lf(currcons);
   continue;
  }
  if (c == 13) {
   cr(currcons);
   continue;
  }
  if (x>=video_num_columns) {
   x -= video_num_columns;
   pos -= video_size_row;
   lf(currcons);
  }
  __asm__("movb %2,%%ah\n\t"
   "movw %%ax,%1\n\t"
   ::"a" (c),
   "m" (*(short *)pos),
   "m" (attr)
   :"ax");
  pos += 2;
  x++;
 }
 set_cursor(currcons);
}

对此分析分析
看看了关于vga等显示操作,暂不讨论吧。

到此 printk这个变参函数简单讨论结束

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值