printf 函数实现的深入剖析[转载]

来源:互联网 发布:金隅悦城丽悦园网络 编辑:程序博客网 时间:2024/05/19 16:37

研究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为任何类型的变量都可以)得到的都是一个固定的值。(32位计算机中都是得到的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后面地址的内容。这样就存在一个可能的缓冲区溢出问题。。。


原创粉丝点击