strtok 的一个可能被忽略的细节

最近帮他们查了一下线上出现了一个神奇的 bug,先加载A库再加载 B 库 和 先加载 B 库再加载 A 库,会有不一致的行为,看起来不一致的行为是环境变量引起的,仔细排查了发现是strtok引起的,运行时会偷偷抹掉原本的字符串,稍微科普一下。

一、问题代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char data[] = "path1:path2:path3:path4";

void dump(char *buffer, int len) {
for (int i = 0; i < len; i++) {
if (buffer[i] == 0)
printf("0");
else
printf("%c", buffer[i]);
}
printf("n");
}

int main() {
char *current = data;
char *remain;
dump(data, sizeof(data));
do {
current = strtok_r(current, ":", &remain);
printf("path = %sn", current);
current = remain;
} while (remain != 0);
dump(data, sizeof(data));
}

这段代码本意是把 data 按照冒号拆开,分别访问每一项,打印出来,看起来是没什么问题的,但事实并非如此,输出的结果是

1
2
3
4
5
6
path1:path2:path3:path40
path = path1
path = path2
path = path3
path = path4
path10path20path30path40

发现 data 是被修改过的,其中冒号的位置都被替换为 0 了,这个是strtok_r 偷偷干的一件事。所以,将来有人再把data 当做string处理时,就只能读到path1 了。

二、另一个测试样例

1
2
3
4
5
6
int main() {
dump(data, sizeof(data));
strtok(data, ":");
printf("path = %sn", data);
dump(data, sizeof(data));
}
1
2
3
path1:path2:path3:path40
path = path1
path10path2:path3:path40

同样,使用 strtok 也会这样,会把冒号覆盖为0 。

三、一些细节

这时候问题就解决了,库 A 中使用这种方式访问环境变量 PATH,暗中已经将 PATH 改掉了,导致库 B 中访问的 PATH 其实是残缺的环境变量。

Q:为什么环境变量可以被直接写值?
A:在C里,getenv 的声明是 char * getenv(const char *name) ,没有说是const,所以编译是可以通过的;其次,有个 linux 的常识,环境变量默认是存在栈上的,在程序刚运行时的栈上,读写肯定是允许的。

综上,使用strtok 时一定要保证字符串是临时使用的串,使用完就丢弃掉,因为里面内容不可靠了。