在工作中,c/c++开发中,日志输出是必不可少的,而日志输出,基本都是使用格式化输出。正常的情况下,我们使用下面的语句来输出日志
printf("%s/%d: some thing error=[%s]", __FILE__, __LINE__, geterror());
但是,如果手抖,写错了呢
printf("%s/%d: some thing error=[%s]", __FILE__, __LINE__,error);
这就悲剧了,程序会core dump。如果是如下的用法呢?
printf("%s/%d", __FILE__, __LINE__, geterror());
printf("%s/%d: some thing error=[%d]", __FILE__, __LINE__);
printf("%s/%d: some thing error=[%d]", __FILE__, __LINE__, geterror());
事实证明,上述三个表达式都不会造成程序core dump,Why?,为了寻找原因,咱们还是得从C的可变参数函数的实现原理中找到病根。先来看看printf的源码
int printf(const char *fmt, ...)
{
char printf_buf[1024];
va_list args;
int printed;
va_start(args, fmt);
printed = vsprintf(printf_buf, fmt, args);
va_end(args);
puts(printf_buf);
return printed;
}
C为了实现可变参数函数,提供了va_list,va_start, va_arg,va_end等宏或函数来支持可变参数函数。这里不一一粘贴它们的定义,在msdn上都有。
va_list是指向可变参数列表数据结构,其定义如下:
#ifdef _M_ALPHA
typedef struct {
char *a0; /* pointer to first homed integer argument */
int offset; /* byte offset of next parameter */
} va_list;
#else
typedef char * va_list;
#endif
va_start是用于初始化va_list变量,使其指向可变参数栈的第一个参数。
va_arg用于从可变参数栈中根据参数类型获取参数的地址,同时计算偏移地址。
va_end用于将va_list变量作废。
这里的关键点就是va_arg函数,函数声明为
type va_arg( va_list arg_ptr, type );
type指定了要读取的参数类型,va_arg的实现是
arg_ptr += _SIZEOF(type);
return *( type *)(arg_ptr - _SIZEOF(type));
分析了printf实现原理,再来看看问题,printf其实就是内存操作,core dump的原因肯定就是段错误,段错误无非就是内存非法,在c/c++基本类型操作中,只有指针才会有内存非法的情况发生。
先来看案例1,
printf("%s/%d: some thing error=[%s]", __FILE__, __LINE__, error);
vprintf解析格式串的时候,发现需要从va_list找出第三个类型为char *的参数值
char *s = va_arg(args, char *);
如果error值为0,则s=0; 这样输出s就会段错误,这就是core dump的原因。
再来看案例2:
printf("%s/%d", __FILE__, __LINE__, geterror());
这个情况是参数值多一个,va_arg少取是没有问题的,所以,这里没有问题。
再来看案例3:
printf("%s/%d: some thing error=[%d]", __FILE__, __LINE__);
这个案例是参数值少了一个,溢出参数栈了,按理说,应该可能会段错误呀,但是我实验了很多次都没有,估计这里做了保护,或者地址还属于进程空间。
再来看案例4:
printf("%s/%d: some thing error=[%d]", __FILE__, __LINE__, geterror());
这个案例是把char *s的地址值赋给一个整形,输出的时候,不会有问题,只是将这个地址值打印一下而已。
这个情况很是应该引起开发人员的注意,这在大多数c++编译器下是不会检查输出格式和实际的参数类型的对应关系的,这将对程序带来不可预估的危险,特别是在测试没有走到的分支里面。