C++代码调试技巧:面试官问“你的程序崩了怎么排查“,别只说printf
上篇聊了CMake把项目的编译构建搞明白了。今天说编译之后的事——调试。先讲个面试场景。面试官问你的机器人程序跑着跑着突然segfault了你怎么排查我当时脱口而出加printf看哪里挂了。面试官没说话就看着我。我接着补充也可以用日志……场面一度很尴尬。后来才知道面试官想听的不是这个。他想听的是系统性的排查思路——core dump分析、GDB定位崩溃点、ASAN检测内存问题、日志系统辅助定位。这几个工具的组合使用才是一个成熟的C开发者应该具备的调试能力。GDBC调试的瑞士军刀GDB是Linux下最强大的调试器。虽然用起来不如IDE的调试器直观但在服务器环境和机器人本体上你通常只能通过命令行调试。基本操作大家都知道break设断点run运行next单步continue继续print打印变量。这些是入门级的。真正有用的是几个进阶用法。第一个是core dump分析。程序崩溃后系统会生成一个core文件记录了崩溃时的内存状态。用GDB加载它gdb ./my_robot_node core进去之后输入btbacktraceGDB会告诉你崩溃时的函数调用栈。比如#0 0x00005555 in LidarProcessor::processScan (this0x0, scan...) #1 0x00005678 in PerceptionModule::update (this0x55555...) #2 0x00005890 in main ()一眼就能看到LidarProcessor的this指针是0x0空指针解引用。问题定位到了。要注意生成core dump需要系统配置。默认情况下Ubuntu的core dump大小限制是0。需要执行ulimit -c unlimited或者配置systemd的coredump handler。面试的时候如果能提到这个细节加分。第二个是条件断点。在循环里某个特定条件才触发的问题不可能手动一步步走到那个条件。用条件断点break sensor.cpp:42 if distance 0.01只有当distance小于0.01时才会停下来。这在调试数值计算问题时特别有用。第三个是watchpoint。你想观察某个变量什么时候被修改了watch robot_state每次robot_state被写入程序都会暂停。找那种不知道谁改了全局变量的bug时watchpoint是神器。ASAN内存问题的照妖镜GDB擅长定位崩溃现场但有些内存问题不会立刻崩溃——它们像定时炸弹不知道什么时候炸。比如堆缓冲区溢出写数据时越界了一个字节use-after-free对象被delete了还在用内存泄漏new了没delete栈溢出递归太深或者局部变量太大这些问题GDB不一定能抓到。但AddressSanitizerASAN可以。ASAN是编译器内置的内存检测工具。用法非常简单编译时加两个flagset(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -fsanitizeaddress -g) set(CMAKE_EXE_LINKER_FLAGS ${CMAKE_EXE_LINKER_FLAGS} -fsanitizeaddress)然后正常运行程序。一旦发生内存问题ASAN会立刻终止程序并输出详细的错误报告精确到文件、行号告诉你那块内存是什么时候分配、什么时候释放的。ASAN的性能开销大概是2倍左右。所以一般只在Debug模式下开Release发布时关掉。在机器人项目里我建议在CI里加一个ASAN的构建配置每次提交自动跑一遍测试。除了AddressSanitizer还有ThreadSanitizer检测数据竞争、MemorySanitizer检测未初始化内存、LeakSanitizer检测内存泄漏。用法都一样换个flag就行。面试的时候如果能提一嘴这些工具的区别和适用场景说明你确实有实际调试经验。日志系统生产环境的调试手段ASAN和GDB都适合开发阶段。但程序部署到机器人上跑的时候你不可能挂着GDB。这时候日志就是最重要的调试手段。很多新手用std::cout打日志。这在本地demo里够用但在真实项目里问题很多没有级别控制、没有文件输出、多线程下输出会交错。工业级的做法是用日志库。C常用的有spdlog和glog。spdlog速度快、API友好是目前最流行的选择#include spdlog/spdlog.h int main() { spdlog::info(Robot node started); spdlog::error(Sensor processing failed: {}, e.what()); }日志的级别很重要。DEBUG级别的日志在发布时关掉INFO记录关键流程WARN记录异常但可恢复的情况ERROR记录严重错误。通过配置文件或者运行时参数调整日志级别不需要重新编译。在ROS2里每个节点自带日志系统rclcpp::get_logger()。用法类似RCLCPP_INFO(this-get_logger(), Lidar scan received, %zu points, scan.points.size()); RCLCPP_ERROR(this-get_logger(), Failed to connect to lidar);日志输出会自动加上节点名、时间戳和日志级别。在多节点的机器人系统里这个前缀信息对于区分日志来源非常关键。还有一个实用技巧在关键路径上加带上下文的日志。比如处理传感器数据之前记录数据的大小和时间戳处理之后记录结果的摘要。出了问题翻日志就能定位到是哪一步出的问题。几个实战排查思路面试里还有一个经常被追问的细节内存泄漏怎么排查除了ASANValgrind是经典工具。valgrind --leak-checkfull ./your_program能告诉你每一处未释放的内存分配点和调用栈。缺点是运行速度会慢几十倍不适合实时性要求高的程序。实际项目中我一般先用ASAN快速筛查确认有泄漏后再用Valgrind定位具体位置。还有一个实用技巧GDB的watch命令可以监控变量变化。比如你怀疑某个指针被意外修改设置watch ptr当ptr的值发生变化时程序会自动暂停然后你用bt查看调用栈就能看到是谁修改了这个指针。这在排查多线程环境下的野指针问题时特别有用。面试的时候面试官问程序崩了怎么排查他期望的是一个完整的思路而不是零散的工具名。第一步看core dump。用GDB加载bt看调用栈info locals看局部变量定位崩溃点。第二步如果崩溃点和内存有关segfault、double free开ASAN重跑让ASAN告诉你具体的内存错误类型。第三步如果问题不是崩溃而是逻辑错误输出不对、行为异常加日志缩小范围。在关键函数的入口和出口打日志二分法缩小问题区间。第四步如果是多线程问题偶发崩溃、数据不一致用ThreadSanitizer检测数据竞争或者用GDB的thread apply all bt看所有线程的调用栈。第五步如果问题只在特定环境出现对比两个环境的差异——库版本、编译选项、硬件配置。这个排查流程有条理地讲出来比单纯说我用GDB调强太多了。给正在准备面试的你调试能力这个东西很难通过看书学会。你得真的遇到bug真的去调才能积累经验。建议你在做项目的时候故意给自己制造一些bug——写个越界访问、写个空指针、写个多线程数据竞争——然后用上面这些工具去定位。经历过几次之后面试的时候讲起来就有底气了。下篇聊C性能优化实战——profile驱动优化的完整流程。代码能跑了还得跑得快。怎么找到性能瓶颈、怎么优化这是面试中区分初级和高级开发者的话题。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。「机器人软件开发面试·从入门到精通」连载系列上一篇第63篇 CMake编译系统详解——从入门到能写复杂项目 下一篇预告第65篇 C性能优化实战——profile驱动优化的完整流程有任何问题欢迎评论区留言我会尽量回复。