跟我一起写操作系统

    返回首页    发表留言
本文作者:李德强
          第三节 实现printf
 
 

        学习了C语言的读者都知道想要使用printf函数就要引入一个头文件叫作stdio.h,也就是说printf这个函数被声明在stdio.h中,它的声明形式是这样的:

int printf(char *fmt, ...);

        第一个参数是显示字符串的格式定义,的参数型为“...”也就是动态参数,说明printf在实现时可以接收不同数量的参数处理。返回值是int类型,表示它一共显示了多少个字节的字符。好了,现在我们就一起来学习printf函数的原理和实现方法。

        C语言是采用栈的方式来存放变量和函数参数,我们来看一下下面的C语言函数:

void myfunc() 
{ 
        int a; 
        int b; 
        int c; 
        a = 0x1111; 
        b = 0x2222; 
        c = 0x3333; 
        myfunc2(a, b, c); 
} 

void myfunc2(int A, int B, int C) 
{ 
        A = 0x4444; 
        B = 0x5555; 
        C = 0x6666; 
}

        为了更好的理解C语言中函数参数的传递方式,我们把这两个函数的反汇编代码和内存使用情况说明一下:

<myfunc>: 
push   %ebp 
mov    %esp,%ebp 
sub    $0x28,%esp 
movl   $0x1111,-0xc(%ebp) 
movl   $0x2222,-0x10(%ebp) 
movl   $0x3333,-0x14(%ebp) 
mov    -0x14(%ebp),%eax 
mov    %eax,0x8(%esp) 
mov    -0x10(%ebp),%eax 
mov    %eax,0x4(%esp) 
mov    -0xc(%ebp),%eax 
mov    %eax,(%esp) 
call   <myfunc2> 
leave  
ret    

<myfunc2>: 
push   %ebp 
mov    %esp,%ebp 
sub    $0x10,%esp 
movl   $0x4444,-0x4(%ebp) 
movl   $0x5555,-0x8(%ebp) 
movl   $0x6666,-0xc(%ebp) 
leave  
ret

        可以看到C语言在参数传递过程中是采用栈寄存器来指示变量和参数的相对位置,我们来看一下内存的情况:



 

        从上图可以看出:在函数调用过程中,主调用函数采用逆向压栈,而被调用函数则正向出栈。具体来说就是myfunc中定义变量的顺序为a、b、c,并将其按定义的顺序压栈,那么变量a则在栈底,变量c在栈顶。而在调用myfunc2时,直接按栈顶到栈底的顺序为myfunc传递参数,并压栈也就是c、b、a的顺序,于是在函数myfunc2中取得参数的顺序从栈顶向下取值就可以按参数传递的正确顺序得到相应的参数值。值得注意的是:在被调用函数里,每个参数之间的地址差总是4个字节(32位操作数和寻址模式)。也就是说我们只要知道了第1个参数的地址,再将这个地址加上4,就可以得到第2个参数的地址;将第2个参数的地址加上4,就可以得到第3个参数地址,以此类推。这就为我们实现动态不定参数提供了一个很好的办法。于是我们来定义两个宏命令,来动态根据一个参数的地址来取得下一个参数的值,无论这个函数有多少个参数:

//定义动态参数地址号 
typedef u32 va_list; 
/*** 
 * 初始化动态参数地址 
 * v: 动态参数地址号 
 * a: 前一个参数变量 
 */ 
#define va_init(v, a)                        \ 
        ({                                   \ 
                v = (va_list)(&a);           \ 
        }) 
/*** 
 * 取得下一个参数的值 
 * v: 动态参数地址号 
 * t: 下一个参数的类型 
 * return: 返回下一个参数的值 
 */ 
#define va_arg(v, t)                         \ 
        ({                                   \ 
                v += 4;                      \ 
                (t)(*((t*)(v)));             \ 
        })

        以上两个就实现了动态参数的取值。对于(t)(*((t*)(v)));这一行语句做一下说明。v是一个u32类型,(t*)(v)就是把v强制类型转换为(t*)的指针类型,再将这个指针类型做取值运算(*((t*)(v)))即是取得这个参数的实际值,为了在赋值时进行安全的值传递,还要将这个结果进行一次强制类型转换(t)(*((t*)(v)))。我们来看一下在使用这个宏时它的宏展开:

char ch = va_arg(args, char); 
char ch = ({args += 4; (char)(*((char*)(args)));});

        在能够取得动态参数之后,就可以根据printf的第1个参数的格式化字符串来处理显示程序了。C语言标准输出printf函数的格式有很多,我们不打算实现过多的复杂功能,在这里只实现它显示字符(char: %c)、字符串(const char*: %s)、整数(int %d)和无符号16进制整数(u32: %x)。另外,还有两种非常重要的数据类型浮点型float和double,这两种类型我们会在后继编写在多任务下的shell程序中来实现。在printf里读入格式化字符串:

//读到\0为结束 
while (*fmt != '\0') 
{ 
        //格式化标记% 
        if (*fmt == '%') 
        { 
                //显示一个字符 
                if ('c' == *(fmt + 1)) 
                { 
                        ch = va_arg(args, char); 
                        putchar(ch); 
                        count++; 
                        fmt += 2; 
                } 
                //显示字符串 
                else if ('s' == *(fmt + 1)) 
                { 
                        str = va_arg(args, char*); 
                        count += puts(str); 
                        fmt += 2; 
                } 
                //显示整数 
                else if ('d' == *(fmt + 1)) 
                { 
                        number_to_str(buff, va_arg(args, int), 10); 
                        count += puts(buff); 
                        fmt += 2; 
                } 
                //显示无符号16进制整数 
                else if ('x' == *(fmt + 1)) 
                { 
                        number_to_str(buff, va_arg(args, u32), 16); 
                        count += puts(buff); 
                        fmt += 2; 
                } 
        } 
        //显示普通字符 
        else 
        { 
                putchar(*fmt++); 
                count++; 
        } 
}

        其中还用到了两个函数:显示字符串puts和数字转字符串number_to_str。它们的实现如下:

/*
 * number_to_str : 将整数转为字符串
 *  - int tty_id : tty编号
 *  - char *buff : 数据地址
 *  - int number : 整数
 *  - int hex : 10进制或16进制
 * return : void
 */
void number_to_str(char *buff, int number, int hex)
{
	char temp[0x800];
	char num[0x20] = "0123456789ABCDEFG";

	int i = 0;
	int length = 0;
	int rem;
	char sign = '+';

	//反向加入temp
	temp[i++] = '\0';
	if (number < 0)
	{
		sign = '-';
		number = 0 - number;
	}
	else if (number == 0)
	{
		temp[i++] = '0';
	}

	//将数字转为字符串
	while (number > 0)
	{
		rem = number % hex;
		temp[i++] = num[rem];
		number = number / hex;
	}
	//处理符号
	if (sign == '-')
	{
		temp[i++] = sign;
	}
	length = i;

	//返向拷贝到buff缓冲区
	for (i = length - 1; i >= 0; i--)
	{
		*buff++ = temp[i];
	}
}

/*
 * puts : 显示字符串
 *  - int tty_id : tty编号
 *  - char *str : 字符串
 * return : void
 */
int puts(int tty_id, char *str)
{
	int count = 0;
	while (*str != '\0')
	{
		putchar(tty_id, *str++);
		count++;
	}
	return count;
}

        在start_kernel中加入调用printf函数来显示我们想要的内容:

#include <kernel/kernel.h> 
#include <kernel/printf.h> 

//全局字符串指针变量 
char *str = "Hello World!"; 

//内核启动程序入口 
int start_kernel(int argc, char **args) 
{ 
       //显示Hello World! 
       printf("%s\n", str); 
       u32 no = 0x12051204; 
       int age = 33; 
       char ch = 'B'; 
       char *msg = "Lidq school number is %x.\nThis year he is %d years old.\nHe got an %c on his test points.\n"; 
       printf(msg, no, age, ch); 

       //永无休止的循环 
       for (;;) 
       { 
       } 
       return 0; 
}

        执行make all命令,并运行lidqos虚拟机,查看结果:



 

        源代码的下载地址为:

https       https://github.com/magicworldos/lidqos.git 
git         git@github.com:magicworldos/lidqos.git 
subverion   https://github.com/magicworldos/lidqos 
branch      v0.9

 

    返回首页    返回顶部
#1楼  雷锋  于 2017年11月21日21:02:31 发表
 
格式化字符串您粘贴两遍,我猜下边大概是想number_to_str和puts方法吧。。
#2楼  李德强  于 2017年11月23日19:45:13 发表
 
非常感谢您的提醒,代码内容已经修正。
  看不清?点击刷新

 

  Copyright © 2015-2018 问渠网 辽ICP备15013245号