C/C++中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; 
} 

在形参列表里有这么一个玩意:...
这个是可变形参的一种写法。
当传递参数的个数不确定时,就可以用这种方式来表示。
很显然,我们需要一种方法,来让函数体可以知道具体调用时参数的个数。
先来看 printf 函数的内容:
这句:va_list arg = (va_list)((char*)(&fmt) + 4);
如果不懂,我再慢慢的解释:
C 语言中,参数压栈的方向是从右往左。也就是说,当调用 printf 函数的适合,先是最右边的参数入栈。

对于一个 `char *` 类型的变量,它入栈的是指针,而不是这个 `char *` 型变量。 换句话说:你 `sizeof(p)`($p$ 是一个指针,假设 `p=&i`,$i$ 为任何类型的变量都可以)得到的都是一个固定的值。(32 位计算机中都是得到的 4) 当然,我还要补充的一点是:栈是从高地址向低地址方向增长的! 现在我想你该明白了:为什么说 `(char*)(&fmt) + 4)` 表示的是 `...` 中的第一个参数的地址。 下面我们来看看下一句:`i = vsprintf(buf, fmt, arg);`。 让我们来看看 `vsprintf(buf, fmt, arg)` 是什么函数。 ```cpp 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` 要做什么吧? 它接受一个格式化的命令,并把指定的匹配的参数格式化输出。 看看 `i = vsprintf(buf, fmt, arg);`。 `vsprintf` 返回的是一个长度,我想你已经猜到了:是的,返回的是要打印出来的字符串的长度。 其实看看 `printf` 中后面的一句:`write(buf, i);`,你也该猜出来了。 `write(buf, i);`,把 $buf$ 中的 $i$ 个元素的值写到终端。 所以说:`vsprintf` 的作用就是格式化。它接受确定输出格式的格式字符串 $fmt$。用格式字符串对个数变化的参数进行格式化,产生格式化输出。 下面的 `write(buf, i);` 的实现就很复杂了。 让我们追踪下 `write` 吧: ``` write: mov eax, _NR_write mov ebx, [esp + 4] mov ecx, [esp + 8] int INT_VECTOR_SYS_CALL ``` 这里是给几个寄存器传递了几个参数,然后一个 `int` 结束。 想想我们汇编里面学的,比如返回到 `dos` 状态: 我们这样用的: ``` mov ax,4c00h int 21h ``` 为什么用后面的 `int 21h` 呢? 这是为了告诉编译器:号外,号外,我要按照给你的方式(传递的各个寄存器的值)变形了。 编译器一查表:哦,你是要变成这个样子啊。 其实这么说并不严紧,如果你看了一些关于保护模式编程的书,你就会知道,这样的 `int` 表示要调用中断门了。通过中断门,来实现特定的系统服务。 我们可以找到 `INT_VECTOR_SYS_CALL` 的实现: ``` init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER); ``` 如果你不懂,没关系,你只需要知道一个 `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 ``` 太复杂了,如果详细的讲,设计到的东西实在太多了。 我们不妨假设这个 `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 ``` 就这么~~简单~~! 恭喜你,重要弄明白了 `printf` 的最最底层的实现! 如果你有机会看 linux 的源代码的话,你会发现,其实它的实现也是这种思路。`freedos` 的实现也是这样。 比如在 linux 里,`printf` 是这样表示的: ```cpp 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; } ``` 限于篇幅,我只解释最底层的 `write`。 这里的 `write` 和 windows 的不同,它还有个参数:1。 1 表示的是 tty 所对应的一个文件句柄。 在 linux 里,所有设备都是被当作文件来看待的。你只需要知道这个 1 就是表示往当前显示器里写入数据。 在 `freedos` 里面,`printf` 是这样的: ```cpp 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` 的实现: ```cpp 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`。 来看看它的定义: ```cpp STATIC VOID handle_char(COUNT c) { if (charp == 0) put_console(c); else *charp++ = c; } ``` 里面又调用了 `put_console`。 显然,从函数名就可以看出来:它是用来显示的。 ```cpp 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` 的实现了。