printf 函数实现的深入剖析

研究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后面地址 
    的内容。 
    
    这样就存在一个可能的缓冲区溢出问题。。。

猜你喜欢

转载自blog.csdn.net/dongshibo12/article/details/77480839