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


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值