跳板宏:一个安全使用 scanf 函数的技巧

dyx
2016-09-18 +0800

前言

scanfsscanf 堪称 C 的杀手级标准库函数,因为使用起来实在是太方便了,但是它们的安全隐患也是众所周知:

char buf[1024];
scanf("%s", buf);

这段代码里 scanf 没有办法对输入的字符串做长度限制,如果读入的字符串过长,会导致危险的缓冲区溢出。不想放弃 scanf 系列函数带来的便利,又要消除这种安全隐患,我们需要在格式描述串里加入对长度的限制:

char buf[1024];
scanf("%1023s", buf);

这虽然保证了安全却大大增加了维护的难度,因为出现了 magic number,当缓冲区大小改变时我们必须保证对应的格式描述串也得到修改,稍不注意便会滋生 bug。

失败的尝试

在通常的C代码中,缓冲区大小通常是使用宏来定义的:

#define MAXL 1023
char buf[MAXL + 1];	

所以我们自然想到在格式描述串中直接使用这个宏定义,可惜这个宏定义是一个整数,没有办法方便地融合进格式描述串,所以我们尝试利用一个转换宏将整数转换为字符串:

#define S(x) "%" #x "s"

#x 的作用是将宏参数 x 变为字符串,例如 S(123) 会被预处理器替换为 "%" "123" "s",根据 C99 的字面字符串连接语法,这等价于 "%123s"。这样我们似乎可以如下使用 scanf 系列函数。

#define S(x) "%" #x "s"
#define MAXL 1023

char buf[MAXL + 1];	
scanf(S(MAXL), buf);

但这是行不通的,因为 C 语言规定 # 后的宏参数不会继续展开,也就是说上面代码的最后一行等价于: scanf("%MAXLs", buf), 这是错误的。

灵机一动:再来一个宏

要解决这个问题,我们引入一个跳板宏:

#define _S(x) "%" #x "s"
#define S(x) _S(x)

S(MAXL) 会被替换为 _S(MAXL),由于这里 MAXL 不在 # 后,所以会被进一步展开为 _S(1023),最后被转换为 "%" "1023" "s"。这便达到了我们的预期效果,也就是我们可以这样安全地使用 scanf 系列函数:

#define _S(x) "%" #x "s"
#define S(x) _S(x)

#define MAXL 1023

char buf[MAXL + 1];	
scanf(S(MAXL), buf);