最近帮他们查了一下线上出现了一个神奇的 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 时一定要保证字符串是临时使用的串,使用完就丢弃掉,因为里面内容不可靠了。