C/C++中printf函数的底层实现
wyp20130701_是一个蒟蒻
·
·
科技·工程
研究 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` 的实现了。