使用栈上字符串作为strtol的endptr参数触发栈溢出的原因及相关疑问
看起来你对strtol的endptr参数的用法有个关键误解,这才导致了栈溢出的问题。咱们一步步拆解清楚:
先搞懂strtol的endptr到底是干嘛的
你可能误以为strtol会把转换后剩下的字符串复制到endptr指向的容器里,但完全不是这样!strtol的endptr参数本质是用来记录位置的:它要求传入一个char**类型的指针,strtol会把原字符串中第一个无法被转换的字符的地址,直接存在*endptr里。也就是说,它不需要你预先分配内存存剩余字符串,只是让这个指针指向原字符串里的某个位置而已。
你第一个能跑的代码其实也绕了弯路——根本不需要malloc内存给ptr,直接用一个栈上的char*变量就行,比如:
#include <stdio.h> #include <stdlib.h> #include <string.h> int main() { const char str[] = "12aaa"; char *endptr; // 普通的char*指针变量 long val = strtol(str, &endptr, 10); printf("str: %s\n", str); printf("strtol结果: %ld\n", val); printf("剩余部分: %s\n", endptr); // 直接输出endptr指向的内容,就是"aaa" return EXIT_SUCCESS; }
这个代码和你第一个malloc版本的效果完全一样,但更简洁安全,也没有内存泄漏风险。
你的错误代码出问题的核心原因
咱们看你修改后的代码:
char container[] = "aaaaa"; // 栈上的char数组,长度5 printf("strtol: %ld\n", strtol(str, &container, 10));
这里有两个致命问题:
1. 类型完全不匹配
container是一个长度为5的char数组,&container的类型是char (*)[5]——也就是“指向长度为5的char数组的指针”,而strtol需要的参数类型是char**(指向char指针的指针)。
虽然编译器可能会给你一个警告(如果你没开警告的话一定要开!),但强行传参后,strtol会把&container当成char**来用:它会把原字符串中第一个无效字符的地址(比如str+2,也就是指向"12aaa"中第一个'a'的地址),写入到*endptr指向的位置——也就是container数组的起始地址。
2. 栈内存溢出
在64位系统中,一个指针的大小是8字节,而你的container数组只有5字节的空间。当strtol试图把8字节的指针值写入到只有5字节的栈数组里时,就会覆盖栈帧中数组后面的其他数据(比如main函数的返回地址、栈保护的cookie值)。这直接触发了GCC的栈溢出检测机制(*** stack smashing detected ***),程序被终止。
哪怕是32位系统(指针4字节),虽然不会立刻溢出,但你写入的是一个内存地址,而不是你以为的剩余字符串,container数组里会被填成乱码,后续访问也会出问题。
再解答你的几个小疑问
- 为什么用char而不是const char?:其实你不需要把
container改成非const——因为strtol根本不会修改原字符串,也不会修改你传入的container数组(它只是错误地往里面写了个指针值)。正确用法里的char* endptr是用来存地址的,和原字符串的const属性无关。 - 栈变量的生命周期:你说的没错,
container作为main函数里的栈变量,生命周期确实是到main执行完为止,但问题根本不是生命周期,而是你错误地用了数组的地址,导致类型不匹配和内存溢出。
最后给你几个建议
- 永远不要忽略编译器警告:比如类型不匹配的警告,这几乎都是bug的前兆。编译时加上
-Wall -Wextra,能帮你提前发现很多问题。 - 仔细啃man手册:strtol的man页里明确写了
endptr的作用:If endptr is not NULL, strtol() stores the address of the first invalid character in *endptr.——重点是“stores the address”,不是“copies the remaining string”。 - 区分数组和指针:C里的数组和指针是完全不同的东西,
char arr[]是栈上的连续内存,&arr是指向整个数组的指针;而char* ptr是一个存储地址的变量,&ptr才是strtol需要的char**类型。
内容来源于stack exchange

