上周帮同事查一个嵌入式设备偶发重启的问题,日志里只有一行:Segmentation fault (core dumped)。用 GDB 加载 core 文件后,发现崩溃点在函数返回后的几条指令上——函数明明执行完了,CPU 却还在往一个非法地址写数据。
问题出在“栈上借来的变量”
翻代码时看到这么一段:
char* get_temp_str() {
char buf[64];
snprintf(buf, sizeof(buf), "temp_%d", rand() % 1000);
return buf; // ⚠️ 这里返回了栈上数组的地址
}调用方是这样用的:
void process() {
const char* s = get_temp_str();
printf("Got: %s\n", s); // 有时正常,有时乱码或崩溃
}buf 是局部数组,生命周期只到 get_temp_str 函数结束。函数一返回,这块栈内存就“还给系统”了——不是清零,也不是锁住,而是随时可能被下一次函数调用覆盖。这时候再拿 s 去读,就像拆掉脚手架后还站在上面拍照:看着稳,其实底下早空了。
为什么不是每次必崩?
因为栈空间复用有随机性。如果 printf 调用时没压太多新栈帧,buf 原来的那片内存还没被改写,就读到了旧值;一旦中间插了个 fopen 或递归调用,栈被快速重用,s 指向的位置就变成别的变量、甚至返回地址,轻则打印乱码,重则跳转到非法指令地址触发硬件异常。
怎么快速定位这类问题?
在 GCC 编译时加上 -fsanitize=address -fno-omit-frame-pointer,跑一遍就能捕获:
==12345==ERROR: AddressSanitizer: stack-use-after-return on address 0x7fff1234abcd
#0 0x4012ab in process /src/main.c:12
#1 0x4013cd in main /src/main.c:20Clang 同样支持。没有 sanitizer?那就用 GDB 在函数返回前设断点,记下 &buf 的地址,返回后再 x/s 查看该地址内容是否已变——变就是铁证。
安全的改法很简单
要么让调用方负责内存(传入缓冲区):
void get_temp_str(char* out, size_t len) {
snprintf(out, len, "temp_%d", rand() % 1000);
}要么用静态存储(注意多线程不安全):
const char* get_temp_str() {
static char buf[64];
snprintf(buf, sizeof(buf), "temp_%d", rand() % 1000);
return buf;
}或者干脆用堆分配(记得 free):
char* get_temp_str() {
char* buf = malloc(64);
if (buf) snprintf(buf, 64, "temp_%d", rand() % 1000);
return buf;
}别图省事写 return buf,栈上变量不是快递柜,不能“寄存”给外人取。