新闻详情

新闻详情

首页 / 资讯中心 / 详情

stack smashing detected 定位指南:从栈保护机制到AddressSanitizer实操

发布时间:2026/10/2 13:15:36来源:尧图网络
stack smashing detected 定位指南:从栈保护机制到AddressSanitizer实操
一次线上程序崩溃控制台打出一行让人摸不着头脑的报错*** stack smashing detected ***: terminated。如果你不是第一次见到这行字多半会顺手把栈保护关掉眼不见心不烦。但问题是这个报错出现的真实意义是“你的程序已经踩到了缓冲区溢出的边缘”关掉保护只是把一颗定时炸弹的引信拆了爆炸本身还在。这篇文章不讲怎么关防护重点讲怎么从stack smashing detected反推回去定位到具体是哪个函数、哪一行代码把栈给写穿了。内容全部来自我在Linux环境下的实际排障经历配合命令和代码示例尽量让看到这篇文章的人能照着操作省去自己瞎折腾的时间。1. 从一次崩溃说起到底是谁在报错先还原一个典型场景。程序是用C/C写的编译的时候带了-fstack-protector或-fstack-protector-strong平时跑得好好的一到某个输入分支就崩溃终端只留下stack smashing detected和Aborted (core dumped)。程序不是段错误没有非法地址访问也没有空指针就是一个不带任何位置的abort。这种报错最让人抓狂的点是它不告诉你具体在哪一行。段错误还能用gdb直接看到崩溃点但栈保护检测到溢出后系统直接调用abort()而abort()的位置在glibc的__stack_chk_fail里。如果你直接在gdb里跑看到的堆栈是#0 __pthread_kill_implementation #1 raise #2 abort #3 __stack_chk_fail #4 真正出问题的函数只要编译时带了-g第4帧就是肇事函数如果你运气好甚至能直接看到是哪个局部变量数组被写穿了。但只定位到函数还不够一个函数里可能有十几处写数组、拷贝字符串的操作还得继续往下挖。先说结论stack smashing detected的定位难度分三层第一层gdb能看到肇事函数但函数内部写坏栈的地方不明确。第二层gdb连肇事函数都看不到因为有些溢出发生在内联函数、构造函数、析构函数里或者栈已经被破坏得连回溯都失效了。第三层程序在完全随机的时机崩报错位置每次都不一样说明栈溢出发生在很早之前只是到某个函数返回时才触发检测。后面所有的方法都是围绕这三层难度展开的。如果你运气好属于第一层十几分钟就能解决如果属于第三层那就得动用AddressSanitizer或者逐步排查了。2. 栈保护机制剖析为什么错误到函数返回才爆发要理解怎么定位先得知道stack smashing detected到底是怎么检测出来的。这不是内核的功劳也不是什么运行时魔法而是编译器在编译阶段插入的一段检查代码。2.1 栈帧布局与金丝雀值一个函数的栈帧stack frame在内存里大体长这样高地址 ---------------------- | 函数参数 | ---------------------- | 返回地址 | ---------------------- | 保存的帧指针 | ---------------------- | canary值 | -- 编译器插入 ---------------------- | 局部变量/数组 | ---------------------- 低地址栈是向下增长的局部变量在低地址返回地址在更高地址。如果你声明了一个char buf[64]然后往里拷贝了80字节多出来的16字节会往高地址方向覆盖先是覆盖canary值再往上就是保存的帧指针和返回地址。编译器在函数入口处会把一个随机值放到canary位置这个随机值来自线程局部存储TLS每次程序启动都不同。在函数返回之前编译器会插入一段检查代码把canary位置的值和TLS里存的值做比较如果不一致说明有人越过边界写坏了这片内存立即调用__stack_chk_fail也就是我们看到的stack smashing detected。这个机制叫Stack Canary也翻译成金丝雀值。名字来自煤矿里用金丝雀检测有毒气体的做法金丝雀先死矿工就知道有危险了。需要注意的是canary只保护了“从canary到返回地址”这一段区域。如果你的溢出发生在canary以下的局部数组之间比如一个数组写到了另一个数组没有越过canary栈保护是检测不到的。这也是很多人开了-fstack-protector仍然感觉“没用”的原因——它保护的是返回地址不是你的局部数据。2.2 检测时机决定了定位难度关键点来了canary检查不是在写入越界的那一刻触发的而是在函数即将返回的时候。这意味着什么假设函数A里有个局部数组越界写入把canary破坏了但A接下来还有几行代码要执行它不会立刻崩溃。等到A执行完准备返回时检查代码才发现canary不对此时程序才abort。而在这期间程序的状态已经是被破坏过的了。更麻烦的情况是你无法确定崩溃点就是越界点。比如A函数越界写了但A没有检查canary编过译条件不同或者A的canary值虽然错了但程序继续往下跑了好几个函数直到某个函数返回时才被发现。这个“延迟反馈”的特性是定位栈溢出的最大难点。另外还有个与优化相关的坑-O2及以上优化级别下编译器可能把小的结构体内联进寄存器不占用栈空间也可能把局部变量重排。有时候你从gdb里看到的变量地址和实际栈布局不一致。遇到这类情况建议先用-O0 -g重新编译一遍再做定位等找到根因再恢复优化级别验证。2.3 栈保护的三个级别你得知道GCC提供了三档栈保护级别它们直接影响了报错出现的频率和定位难度编译选项保护范围备注-fstack-protector仅保护含有char类型数组等易受攻击对象的函数老牌选项覆盖面窄很多函数不保护-fstack-protector-strong保护含有局部数组、结构体数组、取地址操作、内联函数等更多函数覆盖面较广性能和安全的平衡点-fstack-protector-all所有函数都插入canary检查最全面但性能开销最大一般只在调试期用发行版编译系统默认通常用-fstack-protector-strongUbuntu、Debian等。我建议遇到问题先不急着加-all而是想清楚你的程序到底是什么结构。如果你程序里有大量小函数用-all会让性能下降但换来的好处是崩溃点更精准。调试期临时开-all是完全可以接受的线上就别用了。3. 定位栈溢出的几种实用方法现在进入正题报错已经出现了怎么定位到具体代码行。我按从简单到复杂的顺序梳理了几种方法每种方法都附了对应场景你可以根据实际情况选。3.1 基础手段编译选项加足gdb回溯如果你还没重新编译过程序先停一下把编译命令里的优化级别降到-O0加上-g并确保启用了-fstack-protector-allgcc -g -O0 -fstack-protector-all -o app app.c然后让程序在gdb里崩溃直接看回溯gdb ./app (gdb) run (gdb) bt如果运气好能看到类似的输出#0 __pthread_kill_implementation (signo6, ...) #1 raise (sig6) #2 abort () #3 __stack_chk_fail () #4 process_input (data0x7fffffffd820, len128) at app.c:47 #5 main () at app.c:88第4帧的app.c:47告诉你process_input函数返回时canary检查失败。但这不代表第47行就是罪魁祸首只能说这个函数里有越界操作。接下来需要确认是哪个局部变量被写穿了。用watchpoint监测canary地址的变化。具体做法在process_input入口处打断点执行到那里。看一下TLS里保存的canary值或者直接看栈上canary的位置。更简单的方法是在__stack_chk_fail处打断点因为此时canary已经被破坏在检查代码里能拿到当前的canary值。但更实用的是提前看。GDB可以这样查看canary值。在函数入口处(gdb) break app.c:40 (gdb) run (gdb) info registers rsp (gdb) x/8gx $rsp栈顶往下数几个位置找一找和fs:0x28处值一致的8字节数据。这个操作有点繁琐如果你不熟悉栈布局看不太出来。更直接的办法是给canary所在地址设watchpoint(gdb) watch *(unsigned long long*)地址 (gdb) continue一旦有代码写入这个地址gdb就会停下来把你带到越界写入的那一行。这是最直接的定位方式不过前提是你得先找到canary的地址。如果你不想折腾地址计算还有个更简单的办法直接catch syscall abort或者在__stack_chk_fail上打断点然后用frame切到肇事函数用info locals查看所有局部变量的地址手动观察哪个数组的边界可疑。这种方式有点碰运气但配合watchpoint用效率很高。3.2 进阶利器AddressSanitizer 定位到行号gdb watchpoint适合函数逻辑不复杂的情况。如果函数特别长局部变量特别多或者栈溢出发生在很深的内联调用链里我强烈建议直接用AddressSanitizerASan。这是目前Linux下定位内存问题最好用的工具没有之一。重新编译加上ASangcc -g -fsanitizeaddress -fno-omit-frame-pointer -o app_asan app.c直接运行./app_asanASan会在越界写入的现场立即报告而不是等到函数返回。输出会明确告诉你这是stack-buffer-overflow在哪个源文件的哪一行写入地址是多少邻近的合法变量是什么。一个典型的ASan输出长这样ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff6a1a7f20 at pc ... WRITE of size 8 at 0x7fff6a1a7f20 thread T0 #0 copy_data const/plain at app.c:32 #1 process_input at app.c:45 #2 main at app.c:88 Address 0x7fff6a1a7f20 is located in stack of thread T0 at offset 64 in frame #0 process_input at app.c:40 This frame has 3 object(s): [32, 96) buf -- 这就是被写穿的数组注意看两处WRITE of size 8 at ...告诉你写操作发生在app.c:32This frame has 3 object(s)告诉你被破坏的对象是buf它占了64字节从32到96。一行一行报出来根本不需要太多猜测。ASan的定位方式是“在越界发生时立刻停下来”和canary的延迟反馈完全不同。这也是为什么我说它是排查栈溢出的第一利器。3.3 辅助手段core dump 与分析有些时候程序在客户环境或生产环境崩溃你手上只有一个core文件。别慌core文件里往往保留了足够的信息。先确认系统core dump是打开的ulimit -c unlimited崩溃后用gdb加载coregdb ./app core (gdb) bt如果崩溃点恰好是__stack_chk_fail回溯里能看到肇事函数。如果core文件被strip过没有符号信息那就比较难办只好重新编译一个带符号的版本复现。如果你用的是-rdynamic动态符号还能留一两个。core文件里最有价值的是内存内容。你可以直接在gdb里查看栈内存搜索可疑的字符串或数据。比如(gdb) x/200gx $rsp人工翻栈上数据找到被覆盖的canary。如果函数是memory()、strcpy()一类的库函数调用越界栈上会留下源地址、目标地址、长度的痕迹多半能推导出问题点。3.4 终极排除法二分法裁剪代码如果ASan因为某些原因用不了比如程序依赖特殊硬件、需要和现有库联动还有一个土办法二分法注释代码。把怀疑的函数体内容逐步注释掉或者把写入数组的操作临时去掉用#if 0包起来编译运行看报错是否消失。每次去掉一半的写操作很快就能缩小区间。这个方法虽然原始但在复杂的遗留代码里往往比静态分析更快。关键是要有一个快速复现的环境。如果输入数据来自文件那就写一个小工具生成固定输入让程序一启动就必现崩溃这样二分法的效率才高。4. 一个完整实例从报错到手到病除光说不练不行下面我用一个简单的例子走一遍完整流程。这段代码模拟了最常见的越界场景把外部输入拷进局部数组时长度没控制好。4.1 复现问题确认报错形式#include stdio.h #include string.h void process_input(const char *data, size_t len) { char buf[16]; char *dst buf; for (size_t i 0; i len; i) { dst[i] data[i]; } dst[len] \0; } int main() { char payload[] 0123456789abcdefghijklmnopqrstuvwxyz; process_input(payload, strlen(payload)); printf(done\n); return 0; }编译并运行gcc -g -O0 -fstack-protector-all -o demo demo.c ./demo输出*** stack smashing detected ***: terminated Aborted (core dumped)问题稳定复现。4.2 用gdb与watchpoint做初轮定位gdb ./demo (gdb) run程序在__stack_chk_fail处停住。bt看回溯(gdb) bt #0 __pthread_kill_implementation #1 raise #2 abort #3 __stack_chk_fail #4 process_input (data0x7fffffffdbc0, len35) at demo.c:14 #5 main () at demo.c:22肇事函数确定了。到process_input入口(gdb) break demo.c:6 (gdb) run查看栈帧信息。在x86_64下canary通常保存在fs:0x28。我们读一下(gdb) p/x $fs_base 0x28 (gdb) x/gx $fs_base 0x28会看到一个随机值比如0x8d42f6c32d4a1b00。再看栈上哪个位置有这个值。先看rsp(gdb) x/10gx $rsp找到与canary值一致的那个地址对它设watchpoint(gdb) watch *(unsigned long long*)0x7fffffffdb98继续运行gdb会在第一次写入该地址时停下显示是process_input里dst[i] data[i]这一行。到这里不依赖ASan也定位到了写越界的代码行。4.3 用ASan验证并精确定位到行直接用ASan编译一次gcc -g -fsanitizeaddress -fno-omit-frame-pointer -o demo_asan demo.c ./demo_asan输出直接给出ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc3d4a37f0 at pc ... WRITE of size 1 at 0x7ffc3d4a37f0 thread T0 #0 process_input demo.c:10 #1 main demo.c:22两秒钟定位到demo.c:10的dst[i] data[i]。这个效率比gdb手算canary地址快了一个量级。修复方法也简单限制拷贝长度确保不超过sizeof(buf) - 1size_t copy_len len sizeof(buf) ? len : sizeof(buf) - 1; memcpy(buf, data, copy_len); buf[copy_len] \0;重新编译再跑done正常输出。问题解决。5. 常见问题速查与独家避坑最后一部分我把实战中常见的坑和排查经验整理成一个速查表再讲几个别人很少在文档里写到的细节。5.1 常见问题速查表现象可能原因推荐排查方法每次崩溃位置一致固定输入触发固定越界gdb回溯到肇事函数检查函数内数组拷贝崩溃位置随机时有时无越界写入发生在多个路径或依赖输入内容ASan编译观察第一次WRITE发生在哪里小数据量不崩大数据量必崩越界长度与输入长度成正比检查strcpy、memcpy、sprintf是否使用无长度限制版本-O2下崩溃-O0下正常优化导致栈布局变化隐藏了问题用-O0 -fsanitizeaddress定位然后再回到-O2验证修复报错在destructor/析构函数类的成员变量边界写坏返回时检测检查所有对成员数组的写入重点看memcpy编译时没加-fstack-protector却有报错可能是glibc的fortify机制或外部库自带保护检查_FORTIFY_SOURCE宏以及链接库的编译选项报错出现在printf/scanf格式化字符串写入越界检查%s对应的缓冲区大小用%ns限制长度多线程下偶发崩溃某个线程栈溢出污染相邻线程栈调大线程栈大小ASan查看是哪个线程的WRITE越界速查表里的每一项我在实际项目里都遇到过不是凭空写的。尤其是“-O2下崩溃、-O0下正常”这条很多人以为是编译器bug其实是栈布局变了变量在优化后挨得更近了越界更容易踩到canary。5.2 这几个细节能少踩很多坑第一不要一上来就关栈保护。-fno-stack-protector能让报错消失但程序里的越界还在可能变成更隐蔽的段错误或数据错乱。我在实际项目里见过一个“灵异bug”程序线上跑三个月才崩一次查到最后就是一处无长度限制的sprintf把一个局部数组写穿了。关闭栈保护只是把检错机制关掉不是修bug。第二-fstack-protector-strong比-fstack-protector更适合排查问题。前者保护了更多函数报错频率更高定位范围更小。如果你用的是发行版默认的-fstack-protector-strong报错出现的函数基本就是真正的肇事函数。第三ASan定位完成后记得用原始编译选项再做一次验证。ASan会有一定的性能开销并且会改变内存布局有些问题在ASan下暴露得早但在原始条件下可能找到的不是最小复现路径。修复后两种编译方式都跑一遍确保不会再崩。第四__stack_chk_fail本身不是bug它是最后一道防线。看到它意味着栈上的金丝雀已经死了你的程序在内存安全上已经出了问题。花点时间找出真凶比在论坛上搜“怎么关闭stack smashing”有意义得多。第五如果你的项目是用CMake管理的建议在调试模式下默认打开ASan。就像这样# CMakeLists.txt 片段 if(CMAKE_BUILD_TYPE STREQUAL Debug) add_compile_options(-fsanitizeaddress -fno-omit-frame-pointer -g) add_link_options(-fsanitizeaddress) endif()这样每次跑Debug版都是在做内存检查很多问题在开发阶段就被拦下来了根本活不到上线。从个人经验说遇到stack smashing detected我现在的第一反应不是打开gdb而是直接加-fsanitizeaddress重新编译。ASan给出的行号信息基本可以帮你节省掉一半的排查时间。如果项目依赖复杂、ASan不好上再退回gdb watchpoint的路线。栈保护报错本身不可怕可怕的是在不知道根因的情况下绕开它。花半个小时把真正的越界修掉后面能省下好几个小时的线上排障时间。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

OpenShell:为Windows 11重塑高效经典开始菜单的完全指南 2026/10/2 14:02:54

OpenShell:为Windows 11重塑高效经典开始菜单的完全指南

1. 项目概览:为什么时至今日还需要第三方开始菜单1.1 一个老工具的新生命站在2025年往回看,OpenShell(原名Classic Shell)绝对称得上是Windows生态里的“活化石”级工具。最早它诞生的初衷很简单——很多从Windows 7升到Windows 8…

阅读更多 →
遥感语义分割毕设指南:UNet数据管线与训练避坑 2026/10/2 14:02:54

遥感语义分割毕设指南:UNet数据管线与训练避坑

简介:这份资源面向计算机相关专业的本科生与课程设计学习者,提供一套基于UNet网络的遥感图像语义分割完整项目,可用于毕业设计、期末大作业或课程设计场景,难度适中,适合具备一定Python与深度学习基础的同学上手。压缩…

阅读更多 →
devops-exercises 实战:用 Bash 与 diff 命令比较两个目录的差异 2026/10/2 14:02:48

devops-exercises 实战:用 Bash 与 diff 命令比较两个目录的差异

文档教程DevOps运维 【免费下载链接】devops-exercises Linux, Jenkins, AWS, SRE, Prometheus, Docker, Python, Ansible, Git, Kubernetes, Terraform, OpenStack, SQL, NoSQL, Azure, GCP, DNS, Elastic, Network, Virtualization. DevOps Interview Questions 项目地址&…

阅读更多 →
汽车美容店数据库设计:真实业务建模实战指南 2026/10/2 14:02:48

汽车美容店数据库设计:真实业务建模实战指南

简介:本资源是高校《数据库原理及应用》课程设计的完整实践成果,面向计算机、信息管理等专业本科生,聚焦中小型服务类企业管理系统的数据库建模与实现。内容涵盖需求分析、概念/逻辑/物理设计全过程,配套SQL Server环境下的可运行…

阅读更多 →
LSTM 做 SDN 流量预测与负载均衡:从原理到 Ryu 控制器实战 2026/10/2 14:02:42

LSTM 做 SDN 流量预测与负载均衡:从原理到 Ryu 控制器实战

简介:本资源面向计算机、人工智能、通信工程等专业的在校学生与教师,以及希望进阶学习SDN与深度学习的开发者,提供一套基于LSTM的SDN流量预测与负载均衡完整Python实现方案,可用于毕业设计、课程设计或项目立项演示。压缩包共10个…

阅读更多 →
基于LSTM的SDN流量预测与负载均衡实战 2026/10/2 14:02:42

基于LSTM的SDN流量预测与负载均衡实战

简介:本资源面向计算机、人工智能、通信工程等专业的在校学生与教师,以及希望进阶学习SDN与深度学习的开发者,提供一套基于LSTM的SDN流量预测与负载均衡完整实现方案,可用于毕业设计、课程设计或项目立项演示。压缩包共10个文件&a…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉