I am LAZY bones? AN ancient AND boring SITE

2010年 01月 04日 的归档

用“函数属性”来避免C中格式化字符串时可能存在的错误

为了说明这个问题,先来看下这个简单的C程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdarg.h>
 
void writeLogInfo(const char *sFormat, ...){
	char sOutBuffer[4196];
	va_list lvalist;
 
	va_start(lvalist, sFormat);
	vsnprintf(sOutBuffer, sizeof(sOutBuffer)-2, sFormat, lvalist);
	va_end(lvalist);
 
	printf("log: %s\n", sOutBuffer);
}
 
int main(){
    writeLogInfo("int=%s", 123);
    return 0;
}

这程序用gcc编译,即使是用 -Wall 打开所有的警告,也是不会有任何报错的。
但是执行结果是什么呢?由于 writeLogInfo 的是一个参数里指定的是 %s ,而第二个参数确是整型数字 123。所以程序义无反顾地出现了“段错误”而崩溃掉。这种问题在项目代码超过万行以后,要debug起来,也是会浪费很多时间的。
有的人会发现,如果把main里的writeLogInfo直接换成printf,那么在编译的时候,gcc会报一个警告:“警告:格式‘%s’需要类型‘char *’,但实参 2 的类型为‘int’”(Gcc4.x默认就会报,Gcc3.x要加 -Wall 选项才报),如果我们自己的定义的writeLogInfo函数也能有这个警告,那么这种bug将在编译的时候就可以完美解决了。
那么具体怎么实现呢?先来看下面这段代码,功能是和上面的完全一样的,连错误都一样,呵呵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdarg.h>
 
#ifndef __GNUC__
#  define  __attribute__(x)  /*NOTHING*/
#endif
 
void writeLogInfo(const char *sFormat, ...){
	char sOutBuffer[4196];
	va_list lvalist;
 
	va_start(lvalist, sFormat);
	vsnprintf(sOutBuffer, sizeof(sOutBuffer)-2, sFormat, lvalist);
	va_end(lvalist);
 
	printf("log: %s\n", sOutBuffer);
}
void writeLogInfo(const char *sFormat, ...) __attribute__((format(printf,1,2)));
 
int main(){
    writeLogInfo("int=%s", 123);
    return 0;
}

当你尝试用gcc编译这个文件的时候,你就可以看到警告了,哈哈。
可以看到,这里的关键就是“__attribute__((format(printf,1,2)))” ,这句话的作用就是告诉编译器,前面这个函数呢,参数类型是类似printf的,格式化字符串在参数的第1个位置,扩展参数从第2个位置开始,然后编译器就明白了~
然后,上面的4~6行呢,是为了兼容非Gcc的编译器而加的,这样其他的编译器就会直接无视整个 __attribute__ 了,这样至少不会报错。
其实,这个检查格式化字串的功能(format),只是“函数属性”的一个而已,另外还有许多有用又有意思的属性,比如函数的别名啊(alias),是否已经过时啊(deprecated),等等~要了解这些用法的话,建议去看看官方文档