研究printf的实现,首先来看看printf函数的函数体
int printf(const char *fmt, ...)
{
int i;
char buf[256];
   
     va_list arg = (va_list)((char*)(&fmt) + 4);
     i = vsprintf(buf, fmt, arg);
     write(buf, i);
   
     return i;
    }
    代码位置:D:/~/funny/kernel/printf.c
   
    在形参列表里有这么一个token:...
    这个是可变形参的一种写法。
    当传递参数的个数不确定时,就可以用这种方式来表示。
    很显然,我们需要一种方法,来让函数体可以知道具体调用时参数的个数。
   
    先来看printf函数的内容:
   
    这句:
   
    va_list arg = (va_list)((char*)(&fmt) + 4);
   
    va_list的定义:
    typedef char *va_list
    这说明它是一个字符指针。
    其中的: (char*)(&fmt) + 4) 表示的是...中的第一个参数。
    如果不懂,我再慢慢的解释:
    C语言中,参数压栈的方向是从右往左。
    也就是说,当调用printf函数的适合,先是最右边的参数入栈。
    fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。
    fmt也是个变量,它的位置,是在栈上分配的,它也有地址。
    对于一个char *类型的变量,它入栈的是指针,而不是这个char *型变量。
    换句话说:
    你sizeof(p) (p是一个指针,假设p=&i,i为任何类型的变量都可以)
    得到的都是一个固定的值。(我的计算机中都是得到的4)
    当然,我还要补充的一点是:栈是从高地址向低地址方向增长的。
    ok!
    现在我想你该明白了:为什么说(char*)(&fmt) + 4) 表示的是...中的第一个参数的地址。
   
    下面我们来看看下一句:
     i = vsprintf(buf, fmt, arg);
   
    让我们来看看vsprintf(buf, fmt, arg)是什么函数。 
    
   

int vsprintf(char *buf, const char *fmt, va_list args) 

   { 

    char* p; 

    char tmp[256]; 

    va_list p_next_arg = args; 

   

    for (p=buf;*fmt;fmt++) { 

    if (*fmt != '%') { 

    *p++ = *fmt; 

    continue; 

    } 

   

    fmt++; 

   

    switch (*fmt) { 

    case 'x': 

    itoa(tmp, *((int*)p_next_arg)); 

    strcpy(p, tmp); 

    p_next_arg += 4; 

    p += strlen(tmp); 

    break; 

    case 's': 

    break; 

    default: 

    break; 

    } 

    } 

   

    return (p - buf); 

   } 

      
    我们还是先不看看它的具体内容。

    想想printf要左什么吧

    它接受一个格式化的命令,并把指定的匹配的参数格式化输出。

   
    ok,看看i = vsprintf(buf, fmt, arg);

     vsprintf返回的是一个长度,我想你已经猜到了:是的,返回的是要打印出来的字符串的长度

    其实看看printf中后面的一句:write(buf, i);你也该猜出来了。

    write,顾名思义:写操作,把buf中的i个元素的值写到终端。

   
    所以说:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

    我代码中的vsprintf只实现了对16进制的格式化。

   
    你只要明白vsprintf的功能是什么,就会很容易弄懂上面的代码。

   
    下面的write(buf, i);的实现就有点复杂了

   
    如果你是os,一个用户程序需要你打印一些数据。很显然:打印的最底层操作肯定和硬件有关。

    所以你就必须得对程序的权限进行一些限制:

   
    让我们假设个情景:

    一个应用程序对你说:os先生,我需要把存在buf中的i个数据打印出来,可以帮我么?

    os说:好的,咱俩谁跟谁,没问题啦!把buf给我吧。

   
    然后,os就把buf拿过来。交给自己的小弟(和硬件操作的函数)来完成。

    只好通知这个应用程序:兄弟,你的事我办的妥妥当当!(os果然大大的狡猾 ^_^)

    这样 应用程序就不会取得一些超级权限,防止它做一些违法的事。(安全啊安全)

   
    让我们追踪下write吧:

   
    write:

     mov eax, _NR_write

     mov ebx, [esp + 4]

     mov ecx, [esp + 8]

     int INT_VECTOR_SYS_CALL

   
    位置:d:~/kernel/syscall.asm

   
    这里是给几个寄存器传递了几个参数,然后一个int结束

   
    想想我们汇编里面学的,比如返回到dos状态:

    我们这样用的

   
    mov ax,4c00h

    int 21h

   
    为什么用后面的int 21h呢?

    这是为了告诉编译器:号外,号外,我要按照给你的方式(传递的各个寄存器的值)变形了。

    编译器一查表:哦,你是要变成这个样子啊。no problem!

   
    其实这么说并不严紧,如果你看了一些关于保护模式编程的书,你就会知道,这样的int表示要调用中断门了。通过中断门,来实现特定的系统服务。

   
    我们可以找到INT_VECTOR_SYS_CALL的实现:

    init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);

   
    位置:d:~/kernel/protect.c

   
    如果你不懂,没关系,你只需要知道一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。(从上面的参数列表中也该能够猜出大概)

   
    好了,再来看看sys_call的实现:

    sys_call:

     call save

   
     push dword [p_proc_ready]

   
     sti

   
     push ecx

     push ebx

     call [sys_call_table + eax * 4]

     add esp, 4 * 3

   
     mov [esi + EAXREG - P_STACKBASE], eax

   
     cli

   
     ret

   
   
    位置:~/kernel/kernel.asm

   
    一个call save,是为了保存中断前进程的状态。

    靠!

    太复杂了,如果详细的讲,设计到的东西实在太多了。

    我只在乎我所在乎的东西。sys_call实现很麻烦,我们不妨不分析funny os这个操作系统了

    先假设这个sys_call就一单纯的小女孩。她只有实现一个功能:显示格式化了的字符串。

   
    这样,如果只是理解printf的实现的话,我们完全可以这样写sys_call:

    sys_call:

    
     ;ecx中是要打印出的元素个数

     ;ebx中的是要打印的buf字符数组中的第一个元素

     ;这个函数的功能就是不断的打印出字符,直到遇到:'\0'

     ;[gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串

     xor si,si

     mov ah,0Fh

     mov al,[ebx+si]

     cmp al,'\0'

     je .end

     mov [gs:edi],ax

     inc si

    loop:

     sys_call

   
    .end:

     ret

    
   
    ok!就这么简单!

    恭喜你,重要弄明白了printf的最最底层的实现!

   
   
    如果你有机会看linux的源代码的话,你会发现,其实它的实现也是这种思路。

    freedos的实现也是这样

    比如在linux里,printf是这样表示的:

   
    static int printf(const char *fmt, ...)

    {

     va_list args;

     int i;

   
     va_start(args, fmt);

     write(1,printbuf,i=vsprintf(printbuf, fmt, args));

     va_end(args);

     return i;

    }

   
     va_start

     va_end 这两个函数在我的blog里有解释,这里就不多说了

   
    它里面的vsprintf和我们的vsprintf是一样的功能。

    不过它的write和我们的不同,它还有个参数:1

    这里我可以告诉你:1表示的是tty所对应的一个文件句柄。

    在linux里,所有设备都是被当作文件来看待的。你只需要知道这个1就是表示往当前显示器里写入数据

   
    在freedos里面,printf是这样的:

   
     int VA_CDECL printf(const char *fmt, ...)

    {

     va_list arg;

     va_start(arg, fmt);

     charp = 0;

     do_printf(fmt, arg);

     return 0;

    }

   
    看起来似乎是do_printf实现了格式化和输出。

    我们来看看do_printf的实现:

    STATIC void do_printf(CONST BYTE * fmt, va_list arg)

    {

     int base;

     BYTE s[11], FAR * p;

     int size;

     unsigned char flags;

   
     for (;*fmt != '\0'; fmt++)

     {

     if (*fmt != '%')

     {

     handle_char(*fmt);

     continue;

     }

   
     fmt++;

     flags = RIGHT;

   
     if (*fmt == '-')

     {

     flags = LEFT;

     fmt++;

     }

   
     if (*fmt == '0')

     {

     flags |= ZEROSFILL;

     fmt++;

     }

   
     size = 0;

     while (1)

     {

     unsigned c = (unsigned char)(*fmt - '0');

     if (c > 9)

     break;

     fmt++;

     size = size * 10 + c;

     }

   
     if (*fmt == 'l')

     {

     flags |= LONGARG;

     fmt++;

     }

   
     switch (*fmt)

     {

     case '\0':

     va_end(arg);

     return;

   
     case 'c':

     handle_char(va_arg(arg, int));

     continue;

   
     case 'p':

     {

     UWORD w0 = va_arg(arg, unsigned);

     char *tmp = charp;

     sprintf(s, "%04x:%04x", va_arg(arg, unsigned), w0);

     p = s;

     charp = tmp;

     break;

     }

   
     case 's':

     p = va_arg(arg, char *);

     break;

   
     case 'F':

     fmt++;

     /* we assume %Fs here */

     case 'S':

     p = va_arg(arg, char FAR *);

     break;

   
     case 'i':

     case 'd':

     base = -10;

     goto lprt;

   
     case 'o':

     base = 8;

     goto lprt;

   
     case 'u':

     base = 10;

     goto lprt;

   
     case 'X':

     case 'x':

     base = 16;

   
     lprt:

     {

     long currentArg;

     if (flags & LONGARG)

     currentArg = va_arg(arg, long);

     else

     {

     currentArg = va_arg(arg, int);

     if (base >= 0)

     currentArg = (long)(unsigned)currentArg;

     }

     ltob(currentArg, s, base);

     p = s;

     }

     break;

   
     default:

     handle_char('?');

   
     handle_char(*fmt);

     continue;

   
     }

     {

     size_t i = 0;

     while(p[i]) i++;

     size -= i;

     }

   
     if (flags & RIGHT)

     {

     int ch = ' ';

     if (flags & ZEROSFILL) ch = '0';

     for (; size > 0; size--)

     handle_char(ch);

     }

     for (; *p != '\0'; p++)

     handle_char(*p);

   
     for (; size > 0; size--)

     handle_char(' ');

     }

     va_end(arg);

    }

   
   
    这个就是比较完整的格式化函数

    里面多次调用一个函数:handle_char

    来看看它的定义:

    STATIC VOID handle_char(COUNT c)

    {

     if (charp == 0)

     put_console(c);

     else

     *charp++ = c;

    }

   
    里面又调用了put_console

    显然,从函数名就可以看出来:它是用来显示的

    void put_console(int c)

    {

     if (buff_offset >= MAX_BUFSIZE)

     {

     buff_offset = 0;

     printf("Printf buffer overflow!\n");

     }

     if (c == '\n')

     {

     buff[buff_offset] = 0;

     buff_offset = 0;

    #ifdef __TURBOC__

     _ES = FP_SEG(buff);

     _DX = FP_OFF(buff);

     _AX = 0x13;

     __int__(0xe6);

    #elif defined(I86)

     asm

     {

     push ds;

     pop es;

     mov dx, offset buff;

     mov ax, 0x13;

     int 0xe6;

     }

    #endif

     }

     else

     {

     buff[buff_offset] = c;

     buff_offset++;

     }

    }

   
   
    注意:这里用递规调用了printf,不过这次没有格式化,所以不会出现死循环。

   
    好了,现在你该更清楚的知道:printf的实现了

   
    现在再说另一个问题:

    无论如何printf()函数都不能确定参数...究竟在什么地方结束,也就是说,它不知

    道参数的个数。它只会根据format中的打印格式的数目依次打印堆栈中参数format后面地址

    的内容。

   
    这样就存在一个可能的缓冲区溢出问题。。。

[转]printf 函数实现的深入剖析的更多相关文章

  1. C 中 关于printf 函数中度剖析

    题外话  这篇博文主要围绕printf函数分析的,主要讲解printf 使用C的可变参数机制, printf是否可重入(是否线程安全), printf函数的源码实现. 正文 1.C中可变参数机制 我们 ...

  2. 可变参数列表与printf()函数的实现

    问题 当我们刚开始学习C语言的时候,就接触到printf()函数,可是当时"道行"不深或许不够细心留意,又或者我们理所当然地认为库函数规定这样就是这样,没有发现这个函数与普通的函数 ...

  3. printf函数

    printf函数的格式及含义 d                    以十进制带符号的形式输出整数(对正数不输出符号) o                    以八进制无符号的形式输出整数(不输出 ...

  4. Linux Linux下特殊的printf函数和fputs函数

    Linux下,printf函数必须以'\n'结尾才会立刻输出到屏幕,如果没有'\n'直到输出缓冲区满了以后才会打印到屏幕上(敲击换行也算),如果需要不换行的输出,一般可以使用write函数代替.'\n ...

  5. 关于printf函数的所思所想

    缘起大一下学期,C语言程序设计徐小青老师的随口一提,经娄嘉鹏老师提醒,我觉得应该自己整理清楚这一问题.涉及网上资料将会标明出处. 关于printf函数的所思所想 * printf的定义 printf( ...

  6. C语言printf()函数:格式化输出函数

    C语言printf()函数:格式化输出函数 头文件:#include <stdio.h> printf()函数是最常用的格式化输出函数,其原型为:     int printf( char ...

  7. 关于printf函数输出先后顺序的讲解!!

    对于printf函数printf("%d%d\n",a,b);函数的实际输出顺序是这样的先计算出b,然后在计算a,接着输出a,最后在输出b:例子如下:#include<ios ...

  8. printf()函数

    printf()函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息. printf()函数的调用格式为: printf("<格式化字符串>", <参 ...

  9. printf函数重定向

    printf函数底层会调用fputc函数 /*重定向c库函数printf到USART1*/ int fputc(int ch, FILE *f) { /*发送一个字节数据USART1 */ USART ...

随机推荐

  1. 2014.12.01 B/S之windows8.1下安装IIS

    1.打开 控制面板——程序——程序和功能——启用或关闭windows功能 2.找到Internet信息服务         3.等待安装完毕即可 4.控制面板——系统和安全——管理工具——Intern ...

  2. iOS8的新特性

    iOS8的几个重要变化: 家庭分享.用户可以创建家庭分享,除创建者之外最多可以加入6个家庭成员.通过该功能,用户可以和家人分享位置.照片.日历.应用程序.音乐和视频等. 键盘.苹果在iOS8之后开放了 ...

  3. Qt 圆角矩形+鼠标左键拖动窗口

    #ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> namespace Ui { class MainWind ...

  4. 使用chrome调试xpath

    使用chrome调试xpath 相信玩过爬虫的都知道一些库,如lxml(python),可以使用xpath方便地对HTML进行提取,但当真正用的时候,问题就来了,想找到一个元素往往要调试好几遍,而且得 ...

  5. C#版-百度网盘API的实现(一)

    在这篇文章中,楼主将会给大家介绍一下,通过C# winform程序在后台模拟用户登陆百度网盘的基本思路 首先了解下模拟登陆的流程,如下: 一,访问http://www.baidu.com网站,获取BA ...

  6. Telnet RFC

    http://tools.ietf.org/html/rfc857 http://www.faqs.org/rfcs/rfc854.html 不错: http://blog.csdn.net/chao ...

  7. 製程能力介紹(SPC introduction) ─ Ck之製程能力解釋

    Ck之製程能力解釋 a=M-X: 代表規格中心(也就是製程之期望中心)與實際製造出來之群體中心的距離. b=T/2: 代表規格的一半. 所以,當Ck=a/b=M-X/(T/2)以文字來說明就是:實際作 ...

  8. VIM+qmake编译示例程序HelloQt出错问题的解决(文件名一定要使用.cpp,否则就会默认使用gcc编译,当然通不过)

    之前看到很多初学Qt的Linux友们在使用qmake编译第一个HelloQt或者HelloWorld程序时报错,并且始终找不到原因. 前几天我也遇到了同样的问题,我用的是<精通Qt4编程> ...

  9. iOS6和iOS7代码的适配(3)——坐标适配

    由于iOS7里面status bar和视图是重叠在一起了,所以应用的y坐标就没法和以前一致了,需要重新计算设定.基本上,你的应用用Xcode5运行一下就能看见这个问题,这里写了一个最简单的例子,一个V ...

  10. 10.30 NFLS-NOIP模拟赛 解题报告

    总结:今天去了NOIP模拟赛,其实是几道USACO的经典的题目,第一题和最后一题都有思路,第二题是我一开始写了个spfa,写了一半中途发现应该是矩阵乘法,然后没做完,然后就没有然后了!第二题的暴力都没 ...