最近帮他们查了一下线上出现了一个神奇的 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 = %s\n", current); current = remain; } while (remain != 0); dump(data, sizeof(data)); } |
这段代码本意是把 data 按照冒号拆开,分别访问每一项,打印出来,看起来是没什么问题的,但事实并非如此,输出的结果是
1 2 3 4 5 6 |
path1:path2:path3:path4\0 path = path1 path = path2 path = path3 path = path4 path1\0path2\0path3\0path4\0 |
发现 data 是被修改过的,其中冒号的位置都被替换为 \0 了,这个是 strtok_r 偷偷干的一件事。所以,将来有人再把 data 当做string处理时,就只能读到 path1 了。
二、另一个测试样例
1 2 3 4 5 6 7 8 9 10 |
int main() { dump(data, sizeof(data)); strtok(data, ":"); printf("path = %s\n", data); dump(data, sizeof(data)); } path1:path2:path3:path4\0 path = path1 path1\0path2:path3:path4\0 |
同样,使用 strtok 也会这样,会把冒号覆盖为 \0 。
三、一些细节
这时候问题就解决了,库 A 中使用这种方式访问环境变量 PATH,暗中已经将 PATH 改掉了,导致库 B 中访问的 PATH 其实是残缺的环境变量。
Q:为什么环境变量可以被直接写值?
A:在C里, getenv 的声明是 char * getenv(const char *name) ,没有说是const,所以编译是可以通过的;其次,有个 linux 的常识,环境变量默认是存在栈上的,在程序刚运行时的栈上,读写肯定是允许的。
综上,使用 strtok 时一定要保证字符串是临时使用的串,使用完就丢弃掉,因为里面内容不可靠了。