ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ROS段错误调试:exit code -11与GDB core dump实战

ROS段错误调试:exit code -11与GDB core dump实战 ROS 程序跑着跑着突然崩了终端上刷出一行process has died [pid 20083, exit code -11, cmd /home/...]这种场面我见过太多次。exit code -11 是 Linux 系统里非常典型的一个信号——SIGSEGV也就是大家常说的段错误。这个信号的意思是程序试图访问一块它没有权限访问、或者根本就不存在、或者已经被释放掉的内存地址。对于刚上手 ROS 的朋友来说看到这行日志往往一脸茫然节点明明编译通过了话题也能正常收发为什么一运行到某个环节就猝死更让人抓狂的是它不像语法错误那样会给你报具体的行号而是直接给你一个进程 PID 和一个退出码剩下的什么都没有日志里可能只有一句干巴巴的[ERROR] ... process has died。这篇文章就是来帮你把这块硬骨头啃下来的。我会从段错误的本质讲起带你搞清楚 exit code -11 到底意味着什么然后手把手教你用 GDB 配合 core dump 把崩溃现场冻结下来逐个回溯调用栈定位到具体的代码行。不管你是刚装完 ROS 在跑例程的初学者还是已经在做机械臂、导航小车这类项目的开发者这套调试方法都能直接复用。文章涉及的知识点包括 Linux 信号机制、core dump 生成策略、GDB 常用调试命令、ROS 消息回调中的野指针排查、多线程场景下的竞态问题定位以及如何在没有图形界面的环境里高效分析。整个过程我会尽量说人话把每一步为什么这么做讲清楚让你不仅这次能修好下次遇到同类问题也能自己上手。ES: 段错误调试这件事核心不在于记住一堆命令而在于理解程序在崩溃那一刻机器里到底发生了什么。1. 先搞懂 exit code -11 到底是什么1.1 从一次真实的崩溃日志说起我先把典型的崩溃日志还原一下你对照看看是不是很眼熟。一个用 C 写的 ROS 节点可能是在做点云处理也可能是在解析自定义消息运行大概几十秒到几分钟不等终端突然输出类似下面的内容[ INFO] [1700000000.123456789]: Node started, waiting for messages... [ INFO] [1700000005.234567890]: Received message, processing... process has died [pid 20083, exit code -11, cmd /home/user/catkin_ws/devel/lib/my_pkg/my_node __name:my_node __log:/home/user/.ros/log/.../my_node-1.log]. log file: /home/user/.ros/log/.../my_node-1.log注意这里的关键信息只有三个pid 20083、exit code -11、以及节点的可执行文件路径。日志文件里通常也不会告诉你更多最多在崩溃前有几行 INFO 或者 WARN。这就是段错误最恶心的地方——它属于运行时错误编译期完全查不出来必须靠运行时分析工具才能定位。exit code -11 的绝对值 11 对应的是 Linux 信号编号 11也就是 SIGSEGV。信号是 Linux 里进程间通信的一种机制也是内核通知进程你出事了的方式。当进程收到 SIGSEGV 且没有注册处理函数时默认动作就是终止进程并且把退出码记录成-信号编号。所以 exit code -11 等价于进程被 11 号信号杀掉了。类似的还有 exit code -6SIGABRT通常是断言失败或异常未捕获、exit code -9SIGKILL通常是内存爆了被 OOM Killer 干掉。1.2 SIGSEGV 背后的几种典型成因段错误听起来玄乎实际上成因就那么几类我按出现频率从高到低给你排一下。第一类是空指针解引用。这是新手最容易踩的坑。比如你订阅了一个话题回调函数里拿到消息指针但某次消息字段是空的你没做判空就直接访问程序当场就崩。在 ROS 里还有一种隐蔽的空指针ros::NodeHandle没有正确初始化或者某个boost::shared_ptr已经析构了但你还在用。第二类是数组或容器越界访问。C 的std::vector用operator[]访问时不会做边界检查你如果下标算错了读到的是内存里的随机数据写进去更是直接破坏别人的内存。这类问题往往在循环里出现而且不一定每次都崩可能改个数据就崩了非常难查。第三类是使用已释放的内存use-after-free。指针指向的对象已经被delete或者随作用域结束析构了但指针本身没有置空后续代码又通过它访问内存。这在多线程环境里尤其常见一个线程在读另一个线程已经把它释放了。第四类是栈溢出。通常是递归调用太深或者函数里定义了超大局部数组。ROS 节点里如果做递归的坐标变换、树结构遍历不小心写成无限递归也会触发段错误。第五类是多线程数据竞争导致的内存破坏。两个线程同时读写同一个容器或者一个线程在遍历时另一个线程在增删元素迭代器失效后继续使用同样会崩。这一类是最难复现的因为它的触发依赖具体的线程调度时序。理解这些成因很重要因为后面用 GDB 分析时你看到调用栈的第一反应应该是这行代码属于哪一类问题而不是盲目地一行行看。1.3 为什么光看日志定位不了问题很多人的第一反应是去翻~/.ros/log/下面的日志文件希望能看到更多线索。但现实是段错误发生时进程直接被内核杀掉程序自己根本没机会把上下文写进日志。日志里最多记录到崩溃前最后一次正常的操作比如收到了第 N 帧点云但这一帧数据到底哪里有问题日志不会告诉你。还有个常见的误区是加ROS_INFO打印来定位。这个方法在小程序里能用但在复杂节点里效率极低因为你不知道崩在哪只能一段段加打印编译运行一轮又一轮。更麻烦的是加了打印之后崩溃位置可能还会变这就是典型的内存越界特征把你彻底带偏。真正高效的做法是让程序在崩溃时留下遗言——把崩溃瞬间的内存状态、调用栈、寄存器信息全部保存下来然后慢慢分析。这就是 core dump 配合 GDB 的价值所在。2. 让崩溃现场留下来core dump 生成配置2.1 core dump 是什么为什么默认关着core dump 是 Linux 在进程崩溃时生成的一个内存镜像文件它记录了这个进程崩溃那一刻几乎全部的运行时信息各个线程的调用栈、寄存器数值、堆和栈的内容、加载的动态库列表等等。有了它你就能像看监控回放一样把崩溃现场一帧帧倒回去看。但你会发现在大多数 Linux 发行版上程序崩了之后根本找不到 core 文件。原因是系统默认把 core dump 的大小限制设成了 0相当于告诉内核别生成了。这么做主要是为了节省磁盘空间——一个稍微大点的 ROS 节点core 文件可能几百 MB 甚至上 G如果每个崩溃都生成磁盘很快就满了。所以第一步就是把这个限制放开。用ulimit -c查看当前限制如果显示 0就说明没开。要生成 core 文件执行ulimit -c unlimited这条命令只对当前 shell 会话有效你换一个终端或者重启就失效了。如果想让它永久生效需要改配置文件。不过我不建议一上来就永久打开因为磁盘容易爆建议按需开启。2.2 让 core 文件生成到你能找到的地方放开 ulimit 之后还有个坑core 文件的默认命名和存放位置由/proc/sys/kernel/core_pattern决定。你先看一眼cat /proc/sys/kernel/core_pattern如果输出是core或者core.%p之类的那 core 文件会生成在进程的当前工作目录下。ROS 节点的当前工作目录不一定是你的 catkin 工作空间可能是~/.ros或者你启动 roslaunch 的那个目录找起来很费劲。我一般会把它改成带 PID 和程序名的格式放到一个固定目录方便查找sudo sysctl -w kernel.core_pattern/tmp/core-%e-%p-%t这里%e是可执行文件名%p是 PID%t是崩溃时间戳。改完之后崩溃产生的文件名类似core-my_node-20083-1700000000一眼就能对上日志里的 PID非常省事。注意如果 core_pattern 里第一个字符是|说明系统把 core 文件通过管道交给了别的程序处理比如某些系统自带的崩溃报告工具。这种情况下你直接找文件是找不到的需要先把这行改掉或者去那个处理程序配置的目录里找。2.3 编译时打开调试符号光有 core 文件还不够如果编译出来的可执行文件没有调试信息GDB 看到的调用栈全是地址和问号等于白忙活。ROS 的 catkin 默认用 Release 模式编译调试符号基本没有。正确的做法是切换成 Debug 模式重新编译。有两种方式一种是在catkin_make时指定catkin_make -DCMAKE_BUILD_TYPEDebug另一种是在CMakeLists.txt里加一行set(CMAKE_BUILD_TYPE Debug)两选一即可。Debug 模式会关掉优化-O0并加上-g这样 GDB 能精确对应到源码行。这里有个经验优化等级对调试影响非常大。开了-O2之后编译器会做指令重排、变量复用、内联展开导致 GDB 显示的栈帧和实际代码对不上甚至某些变量被优化掉了看不到值。所以我强烈建议调试阶段统一用-O0。如果你的工作空间里有很多包不想全部重新编译可以用catkin_make --pkg my_pkg只编你关心的那个包但前提是依赖关系没变。3. GDB 上手指南从命令到实战3.1 最快上手的一条命令拿到 core 文件之后进入你的可执行文件所在目录执行gdb /home/user/catkin_ws/devel/lib/my_pkg/my_node /tmp/core-my_node-20083-1700000000第一个参数是可执行文件必须是生成那个 core 文件的同一个版本编译参数、源码都要一致否则栈信息会错乱。第二个参数就是 core 文件。执行后 GDB 会加载符号然后停在一个类似这样的提示Program terminated with signal SIGSEGV, Segmentation fault. #0 0x00007f8a1c2b3d4e in MyNode::processCloud (this0x55d4a2f0, msg...) at /home/user/catkin_ws/src/my_pkg/src/my_node.cpp:145 145 float x msg-points[i].x;看到没这一行就是凶案现场。GDB 直接告诉你程序死在my_node.cpp的 145 行也就是访问msg-points[i].x的时候崩了。到了这一步问题范围已经缩小到了极其具体的一行代码。3.2 读懂调用栈完整还原崩溃路径但只看到崩在哪一行还不够你还需要知道程序是怎么走到这一行的。执行bt命令打印完整调用栈(gdb) bt #0 MyNode::processCloud (this..., msg...) at my_node.cpp:145 #1 0x00007f8a1c3f2a11 in boost::function1void, ...::operator() (...) from /opt/ros/noetic/lib/... #2 0x00007f8a1c5d1e22 in ros::SubscriptionCallbackHelperT...::call (...) from /opt/ros/noetic/lib/... #3 0x00007f8a1c6e2f33 in ros::SubscriptionQueue::call (...) from /opt/ros/noetic/lib/... #4 0x00007f8a1c7d4a44 in ros::CallbackQueue::callOneCB (...) from /opt/ros/noetic/lib/... #5 ...从下往上看你能看到完整的调用链ROS 的回调队列触发了订阅回调回调里调用了processCloud然后在第 145 行崩了。如果崩溃发生在多线程回调里栈里还能看到线程相关的帧。bt后面可以加数字看部分栈比如bt 5只看最内层 5 帧。如果栈非常深bt full会把每一帧的局部变量也打出来信息量很大但输出也长建议先bt看结构再针对关键帧用frame切换。3.3 切换栈帧查局部变量知道了崩在哪下一步就是看崩溃点周围的变量值。用frame N切到第 N 层栈帧然后info locals看局部变量print xxx看具体变量(gdb) frame 0 (gdb) info locals i 32768 msg (const sensor_msgs::PointCloud2ConstPtr ) 0x7ffd...: {px 0x0, py 0x0, ...} (gdb) print msg-width $1 100 (gdb) print msg-height $2 1如果看到i 32768而msg-width只有 100那问题就清楚了循环变量越界了跑到 32768 还在访问msg-points[i]当然崩。这里msg的指针显示px 0x0也可能说明整个数据块是空的你根本不该访问。除了局部变量print还能看指针指向的内容、结构体字段、数组元素。比如print msg-data.size()看数据长度print *(int*)ptr看指针指向的整数值。表达式支持 C 语法非常好用。3.4 常用命令速查与使用场景我把 GDB 里最常用的命令整理成一张表方便你对着用命令简写作用典型场景backtracebt打印调用栈第一时间看崩溃路径frame Nf N切换到第 N 层栈帧查看某层调用的上下文info locals显示当前帧局部变量看崩溃点的变量值print xp x打印变量/表达式看具体值、指针内容listl显示源码看崩溃行附近的代码info threads列出所有线程多线程崩溃必看thread N切换线程定位是哪个线程崩的info sharedlibrary列出加载的动态库排查库版本问题disassembledisas反汇编当前函数没有源码时分析quitq退出 GDB分析完退出这张表不用背用几次就记住了。真正需要理解的是每个命令背后的意图bt解决怎么走到这frame解决这层的上下文是什么info locals和print解决变量到底是多少info threads解决是哪个线程出的事。提示如果 GDB 里bt显示的都是??和地址八成是可执行文件版本和 core 文件不匹配或者没开调试符号。先确认这两点再往下查。4. 多线程与 ROS 回调场景的进阶排查4.1 定位是哪个线程崩的ROS 的节点通常有多个线程主线程跑ros::spin()还有消息回调线程、定时器线程等等。多线程场景下的段错误最关键的一步是先确认崩溃发生在哪个线程。用info threads可以看到所有线程的状态GDB 会在当前正在执行的那个线程前面标一个星号(gdb) info threads Id Target Id Frame 1 Thread 0x7f8a... (LWP 20083) MyNode::mainLoop at my_node.cpp:88 * 2 Thread 0x7f8a... (LWP 20091) MyNode::processCloud at my_node.cpp:145 3 Thread 0x7f8a... (LWP 20092) ros::TimerQueue::...星号在 2 号线程上说明崩在这个回调线程里。然后用thread 2切过去再用bt看它的调用栈。注意每个线程的栈是独立的你切到不同线程看到的就是那个线程的执行路径。这一步的意义在于很多段错误看起来是主逻辑崩了实际是多线程竞争导致某个容器被破坏了主逻辑只是碰巧踩到了这块坏内存。只有确认了崩溃线程才能顺着正确的方向排查。4.2 数据竞争与容器迭代器失效我在实际项目里遇到最多的一类 ROS 段错误就是订阅回调里的容器被并发修改。比如你有一个全局的std::vector用来缓存历史数据主线程在遍历它做计算回调线程同时在push_back新数据。当push_back触发扩容时整个 vector 会重新分配内存主线程手里的迭代器或引用瞬间失效继续访问就是访问已释放内存直接段错误。这类问题的特征是崩溃位置不固定有时在遍历处有时在访问元素处改个数据量或者加点延时可能就不崩了。看到这种飘忽不定的崩溃第一反应就应该是并发问题。排查方法有两种。一种是加锁在遍历和修改的地方用std::mutex保护起来这是根治手段。另一种是先用 GDB 确认看崩溃线程的栈里有没有容器操作。如果info locals里显示的迭代器地址指向一片看起来是垃圾数据的内存基本就能确定。在 ROS 里还有个专门的坑默认情况下同一个节点的多个订阅回调是在同一个线程里串行执行的单线程 spinner但如果你用了ros::AsyncSpinner或者多线程 spinner回调就可能并行执行。所以先确认你的节点用的是哪种 spinner这直接决定了会不会有并发问题。用ros::MultiThreadedSpinner的时候一定要假设所有回调都可能同时跑。4.3 boost::shared_ptr 与话题消息的生命周期ROS 的消息传递大量使用boost::shared_ptrROS 2 里是std::shared_ptr。这个设计本身是为了避免拷贝但如果你不理解它的生命周期规则也容易出问题。一个典型的错误是在回调里把消息指针存到一个全局变量或成员变量里想留着以后用但没考虑消息可能很大、或者这个指针被反复覆盖。更隐蔽的一种是你把消息指针传给了另一个异步任务比如一个后台线程但回调返回后消息的引用计数降到了 0消息被释放了后台线程还在用它直接段错误。排查这类问题的关键是看info locals和print出来的指针地址。如果指针看起来是合法的但它指向的内存内容是垃圾值或者 shared_ptr 的引用计数显示为 0那就说明生命周期管理出了问题。经验在 ROS 里只要消息指针被保存到了回调函数作用域之外就必须先boost::shared_ptr拷贝一份ptr msg;这样引用计数会加一保证在你用的时候消息不会被释放。4.4 用条件断点和 watchpoint 抓现行有些崩溃很难在 core 文件里看出来因为它是在程序运行过程中某块内存被逐步破坏最后才在某个不相干的地方崩掉。这种内存踩踏最头疼因为崩溃点和破坏点可能相隔十万八千里。针对这类问题GDB 的watchpoint监视点就派上用场了。它的原理是监视一块内存地址只要这块内存的值被改变GDB 就暂停程序让你看是谁改的。用法是先让程序跑到某个合适的位置然后(gdb) watch *(int*)0x55d4a2f0 (gdb) continue不过 watchpoint 需要程序正在运行也就是你得用 GDB 直接启动程序而不是分析 core 文件而且它会拖慢程序执行速度因为每次内存写都要检查。一般配合二分法用先大概定位是哪块内存被破坏再针对性地监视。条件断点也是类似思路比如你怀疑某个循环越界可以设break my_node.cpp:145 if i 100只在 i 越界时才停下。这在复现概率低的问题时特别有用。(gdb) break my_node.cpp:145 if i 100 (gdb) run5. 完整排查流程与实战案例拆解5.1 一套可以照着做的标准流程把前面讲的东西串起来一套完整的排查流程是这样的第一步确认问题现象。看到exit code -11或者Segmentation fault先记下 PID 和节点名去~/.ros/log/找对应日志确认崩溃前的最后操作。第二步准备调试环境。开ulimit -c unlimited设置core_pattern用 Debug 模式重新编译出问题的包。第三步复现崩溃。用平时启动节点的方式再跑一次尽量用相同的输入数据让崩溃稳定复现。如果崩溃是随机的就多跑几次或者用录制好的数据包rosbag回放来保证输入一致。第四步定位 core 文件。到core_pattern配置的目录找最新的 core 文件文件名里的 PID 应该和日志里的 PID 一致。第五步GDB 分析。加载可执行文件和 core先bt看调用栈再切帧看变量判断属于哪类问题空指针、越界、use-after-free、并发。第六步对照源码修复。找到问题代码改完重新编译跑原来的场景验证。这里要注意一定要用同样的触发条件验证否则可能只是问题被掩盖了。第七步加固代码。修复之后顺便检查同类代码有没有相同隐患加判空、加边界检查、加锁防止下次换个地方又崩。这套流程看起来步骤多但熟练之后整个过程可能十几分钟就能搞定。关键是每一步都要做扎实尤其是第三步的稳定复现它决定了你能不能拿到有分析价值的 core 文件。5.2 案例一机械臂控制节点解析关节角崩溃我拿一个真实场景来说。有个机械臂控制节点订阅了关节状态话题回调里解析消息然后更新内部模型。运行几分钟后崩溃exit code -11。拿到 core 文件后bt显示崩溃在回调函数的这一行double angle current_joints[name];info locals显示name是一个看起来正常的字符串但current_joints这个std::map的指针是合法的问题出在另一个线程正在修改这个 map。原来这个节点还开了个服务服务回调里会清空并重建current_joints而订阅回调还在读它。两个回调都在各自的线程里跑因为用了 MultiThreadedSpinnermap 被并发读写内部的红黑树结构被破坏读到一半就崩了。修复很简单给current_joints加一个std::mutex读写都先加锁。修复后连续跑了几个小时都不再崩。这个案例的教训是只要用了多线程 spinner所有共享容器都要当成并发访问来对待。GDB 帮我们确认了崩溃线程和崩溃点剩下的就是根据代码逻辑推断竞争关系。5.3 案例二SLAM 节点处理点云越界第二个案例来自一个建图节点。它订阅点云话题回调里把点云按行拆分成多个子任务处理。崩溃位置在访问points[i]的地方i的值是 2000 多但points的大小只有几百。print points.size()显示 480print i显示 2100。原因是在计算行数时用错了字段代码里用点云的width乘了一个系数算出了索引上限但实际数据是按row_step排列的width这个字段在特定设备驱动下可能不准确。修正逻辑后索引不再越界。这类问题靠日志根本看不出来因为崩溃前一切看起来正常。只有 GDB 里把i和容器大小一对比问题一目了然。所以 core 文件的价值就在于它能在崩溃瞬间把变量的真实值冻结下来。5.4 案例三定时器线程里的野指针第三个案例更隐蔽。一个节点用ros::Timer定时执行某个任务任务里访问一个指向配置对象的指针。程序稳定运行但每次修改配置参数通过动态参数服务器后不久就崩。bt显示崩溃在定时器回调里访问那个配置对象指针。info locals里指针地址看起来正常但print *config_ptr显示里面全是 0 或者垃圾值。进一步排查发现动态参数回调里会delete旧配置对象然后new一个新的但定时器线程可能还在用旧指针。这是典型的 use-after-free。修复方案是把裸指针换成智能指针或者用读写锁保护指针的替换和访问。我倾向于前者智能指针的引用计数能自动保证对象在使用期间不被释放。6. 常见问题与避坑清单6.1 分析 core 时最容易卡住的地方在实际调试里有几个问题反复出现我整理成一张速查表现象可能原因解决办法GDB 里bt全是??可执行文件无调试符号用 Debug 模式重新编译bt显示的源码行对不上可执行文件与 core 版本不一致用崩溃时的那个版本重新加载找不到 core 文件ulimit 为 0 或 core_pattern 有问题检查ulimit -c和 core_patterncore 文件只有几 KB核心转储被截断检查磁盘空间和 ulimit 设置崩溃点每次都不一样内存越界或并发问题用 watchpoint 和条件断点辅助GDB 加载库符号失败动态库路径不对设置solib-search-path这里重点说两个。一个是版本不一致GDB 显示的行号对不上往往是因为你重新编译了代码但用的是旧的 core 文件或者反过来的情况。分析 core 最重要的原则就是可执行文件、动态库、源码三者必须匹配。如果改了代码core 文件就作废了得重新复现。另一个是动态库符号加载失败。ROS 节点依赖一堆 .so如果 GDB 找不到它们栈里有关库的那几帧就显示不出来。可以在 GDB 里用set solib-search-path /opt/ros/noetic/lib:/home/user/catkin_ws/devel/lib指定搜索路径。6.2 几个我踩过的坑第一个坑别在优化模式下调试。我之前为了性能用-O2编译结果 GDB 里变量值全是乱的有的变量直接被优化没了查了半天以为是内存问题其实是优化导致的假象。后来老老实实切回-O0问题半小时就定位了。第二个坑core 文件生成目录可能是只读的。有次配了 core_pattern 指向一个目录但那个目录权限不对内核写不进去core 文件生成失败日志里只留一句被截断的提示。后来改成/tmp就好了。所以配完之后最好手动制造一个崩溃测试一下确认 core 文件真的能生成。第三个坑别小看 exit code 的其他值。虽然这次讲的是 -11但实际运维里还会遇到 -6断言失败和 -9被杀。有一次我以为是段错误查了半天发现系统内存不足触发了 OOM Killer进程被 -9 干掉了。遇到非 -11 的退出码先确认信号含义别想当然。第四个坑rosbag 回放是复现利器。很多崩溃依赖特定输入数据靠手动跑很难稳定复现。把崩溃前的数据录成 rosbag然后用rosbag play回放能大大提高复现概率。我有一次专门录了 10 分钟的点云数据回放到第 8 分钟必崩定位起来轻松很多。6.3 在没有图形界面的环境里怎么分析很多朋友的 ROS 跑在服务器或者虚拟机上没有图形界面担心 GDB 不好用。其实完全不用担心GDB 本来就是命令行工具而且分析 core 文件根本不需要图形界面。如果想看得更直观可以用 GDB 的 TUI 模式gdb -tui /path/to/binary /path/to/core这样窗口会分成上下两部分上面显示源码下面输入命令边看代码边调试。如果 TUI 在这环境里显示有问题也可以在 GDB 里直接敲layout src切换。对于远程服务器用 SSH 连上去跑 GDB 是完全可行的。如果 core 文件太大不方便传输可以在服务器上分析把关键输出复制下来。实在需要图形化也可以把 core 文件拉回本地用本地的 GDB 加载前提是本地有相同版本的可执行文件和库。补充一点如果崩溃节点跑在容器里core 文件默认会在容器内生成容器退出就没了。需要把 core_pattern 指向一个挂载到宿主机的目录或者用docker cp及时把文件捞出来。这一点在做容器化部署时特别容易忽略。6.4 从被动调试到主动预防调试多了你会发现最好的段错误处理其实是让它根本不发生。几个习惯能帮你少踩很多坑所有从外部来的数据消息字段、参数、配置都做合法性检查所有指针使用前都判空多线程共享的数据结构一律加锁或者换成线程安全的设计涉及容器索引的地方全部用at()或者先判断范围。还有个小技巧ROS 节点上线前可以跑一遍valgrind或者 AddressSanitizer。AddressSanitizer 编译时加-fsanitizeaddress就行能在运行时提前抓出越界、use-after-free 这类问题把崩溃消灭在测试阶段而不是等到线上才爆。我用 ASan 抓到过好几个平时根本复现不出来的内存问题虽然它会拖慢程序但测试阶段完全值得。最后再分享一个习惯每次修完一个段错误我都会在代码注释里记一句这里曾经因为 xxx 崩过改成现在这样。过几个月回头看就能提醒自己别再犯同样的错。内存问题这东西经验比什么都重要踩过的坑多了一眼就能看出哪行代码有嫌疑。
返回列表