最近在论坛上看到很多人对下面这样的程序有疑问:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("%d");
return 0;
}
首先认为这样写有错误,其次认为这样打印出来的值是不确定的。
其实这是对printf函数执行机理的不理解,下面我就结合源代码来对printf执行的机理进行一点必要的解释。
首先我们看这样一个程序:
#include <stdio.h>
int main(int argc, char *argv[])
{
int n=5;
printf("%d",n);
return 0;
}
用microsoft的编译器,cl /FA 进行反汇编后,有如下输出:
_DATA SEGMENT
$SG797 DB '%d', 00H
_DATA ENDS
...
mov DWORD PTR _n$[ebp], 5 ;这里是将数字5赋给变量n所在的内存
mov eax, DWORD PTR _n$[ebp] ;将n移动到寄存器eax中
push eax ;将eax入栈,也就是将n入栈
push OFFSET FLAT:$SG797 ;将printf中的打印字符串入栈
call _printf ;调用_printf过程
add esp, 8 ;设置正确的栈顶位置
...
以上过程就是可变参数的函数printf调用过程。
接着我们看一下glibc中对printf函数的实现,我们用的是glibc的1.09.1的版本,以下是printf.c中的内容
int DEFUN(printf, (format), CONST char *format DOTS)
{
va_list arg;
int done;
va_start(arg, format);
done = vprintf(format, arg);
va_end(arg);
return done;
}
可以看到printf其实在内部调用的是vprintf,通过查看vprintf.c中的内容,我们可以看到vprintf其实是通过vfprintf实现的,它的函数原型是这样的:
int DEFUN(vfprintf, (s, format, args),
register FILE *s AND CONST char *format AND va_list args)
这个函数的整体执行结构是这样的:
register CONST char *f; //可以看到f是一个const char的指针
f = format;
while (*f != '/0')
{
...
if (*f != '%')
{
...
}
if (*f == '%')
{
fc = *f++;
...
switch (fc)
{
case 'd':
...
case 'c':
...
....
}
}
}
从上面的结构我们可以看出,函数首先读取字符串中的字符,然后一个个比较,如果是%,则马上用switch...case结构判断后续字符
在每一个case语句块里面,都有这样的语句:
nextarg(...);
outchar(...);
nextarg()是一个宏,有如下宏定义
#define castarg(var, argtype, casttype) /
var = (casttype) va_arg(args, argtype)
#define nextarg(var, type) castarg(var, type, type)
一出现va_arg,我们就很熟悉了,这个宏的作用就是读取可变参数,在这里的作用就是将args中的内容读入。也就是利用栈顶指针读取
栈中的内容。
outchar(...)也是一个宏,它的定义如下
#define outchar(x) /
do /
{ /
register CONST int outc = (x); /
if (putc(outc, s) == EOF) /
RETURN(-1); /
else /
++done; /
} while (0)
不难看出,是利用putc将内容输出到设备或文件中的。
我们再来看本文开头的那个程序,程序独到%d后,会去栈中取要显示的内容,在实际的实现中也就是将栈顶指针减8,但此时这个位置已经不是栈中的位置了,程序输出的值也就是这个位置的内容,所以开头那个程序无论控制字符是怎么样的,也就是不管换成%d还是%f,仅仅是输出的结果不同,实际的内容都是一样的。