平时大家说“debug”总觉得是个高大上的词好像跟“写代码”是两件事。实际上我写了这么多年代码最大的感受是debug 不是写代码的附属品它本身就是核心工作的一部分。这个“debug学习记录1”系列就是想把我这些年跟 bug 交手、被 bug 教育的过程沉淀下来。这篇文章不会只讲某个 IDE 的断点按钮在哪个菜单里更多是想说清楚为什么调试会卡住、怎么快速缩小范围、以及那些你翻文档翻不到的实际经验。适合刚接触调试的初学者也适合被某个诡异问题折磨半天的老手你可以跳着看挑自己需要的部分。很多人的调试姿势是出问题了先打个断点跑一下看看没看出来再换个地方打一个。这不是调试这是碰运气。真正有效的调试核心只有一句话——缩小怀疑范围。1.1 调试的核心不是“看代码”而是“排除变量”我第一次带新人的时候经常遇到这样的场景接口报了个 500新人先从头到尾读一遍业务代码然后一脸茫然地问我“逻辑看起来没问题啊”。问题恰恰出在这里——你盯着代码看是在验证“我认为它没错”而不是在验证“它哪里错了”。正确的做法是先把问题切成几段。比如一个支付回调接口返回失败先别急着怀疑签名算法。你可以问自己请求到底进来没有参数解析对不对业务校验卡在哪一步落库有没有成功每次只验证一段就像排查家里水管漏水一样先确认是不是进水总阀的问题再一层层往下找。这种“分段排除”的思路我几乎用在了所有疑难 bug 上。真要做起来不一定非得用断点。最简单的办法就是临时加日志把每一段入口和出口的数据打出来。哪一段的输入正常而输出不正常问题就在哪一段。把范围从“整个接口”缩小到“某几行代码”接下来要做的事情就变得非常明确了。1.2 复现问题是你的第一优先级调试里最可怕的事情从来不是“错误信息看不懂”而是“这个问题我复现不出来”。一个只在上线后出现的 bug就像薛定谔的猫你盯着它的时候它不出现你转身它就来一下。遇到这种情况第一件事不是去猜原因而是想尽办法把现场留下来。我自己的习惯是整理一份“问题清单”把触发条件一条一条列出来——什么操作系统、什么浏览器、什么账号、什么操作顺序、输入了什么特殊字符。别小看这些信息我印象最深的一个崩溃 bug排查了两天最后发现是某个用户名带了全角空格偏偏在特定数据库排序规则下才会触发。如果没有记录“这个用户比较特殊”这个细节可能又要多花两天。复现的另一个技巧是做最小化裁剪。系统跑完整流程才崩那就把流程一步步停掉某个模块移除后问题消失说明嫌疑就在这个模块附近。好比医生诊断不是一上来就开全身检查而是根据症状先做针对性化验。2. 常见调试工具的正确打开方式工具不在多在于用对。我常用的调试手段其实是三样IDE 调试器、日志输出、远程调试。每一样都有它最合适的场景选错了就会觉得“这个工具怎么这么难用”。2.1 IDE 调试器断点只是门槛关键在于怎么用先说最常见的 IDE 调试器Java 系的 IDEA、前端系的 VS Code、嵌入式系的 Keil/CCS本质上思路一样的。很多人只会“双击打一个红色断点然后按继续”这样只能解决逻辑流程问题遇到稍微隐蔽一点的场景就抓瞎。我建议至少把三件事用熟条件断点不是每次进到这个位置都停而是在满足某个条件时才停。比如循环里想只在第 100 次时停下来看数据就不用一遍遍按继续。IDEA 里在断点上右键输入i 100Keil 里也支持类似的表达式。这个功能一旦用熟调试效率不是高一星半点。调用栈Call Stack很多人停下之后只看变量窗口我恰恰相反最先看的是调用栈。它能直接告诉你“谁把我调用成这个样子的”。大部分逻辑问题看调用栈比看变量快得多。表达式求值Evaluate Expression运行到断点时临时算一个表达式比如某个方法在这个数据下会返回什么。这比在代码里写一堆临时变量再重新部署要方便太多。另外步进操作那几个按钮的区别也值得重新记一遍。Step Over 是按一下执行完当前这一行如果这行是方法调用它会直接跳到下一行Step Into 则会进到方法内部。不少初学者在方法调用那行一直按 Step Into结果一头扎进 JDK 源码里出不来这是很经典的局面。2.2 日志与串口嵌入式调试里没有 printf 解决不了的如果说 IDE 调试器是“手术刀”日志就是“生命体征监测仪”。尤其是在嵌入式开发里很多时候仿真器都不好使一根串口线反而最可靠。先把日志分级这件事想明白Info 记录正常流程Debug 记录关键变量Error 记录异常路径。大多数人栽跟头是因为日志全用 Debug 级别打线上环境一开日志就刷屏关了又什么都看不到。实际上在生产环境保留 Error 和 WARN在测试环境打开 Debug才是正常用法。串口调试在单片机项目里几乎是必修课。标准的做法是重定向printf让格式化输出到串口。以 STM32 为例在 Keil 里勾选 MicroLIB 之后重写fputc把字符丢到 UART 的发送寄存器里串口助手里就能看到打印内容。RT-Thread、FreeRTOS 这类系统一般还自带日志组件按模块过滤起来更顺手。这里必须提醒一句串口打印太频繁会反噬性能。我曾经在一个高频中断里放了打印语句结果中断周期被拉长系统直接超时复位。日志是给“人”看的不是给“机器”看的高频路径上要么不放要么用标志位先缓存、再在低优先级任务里统一输出。2.3 远程调试本地打断点远端跑代码远程调试的适用场景很明确程序跑在服务器或者开发板上本地想用 IDE 图形界面来跟踪。Java 后端最常见的做法是 JDWP 协议嵌入式的做法是 gdbserver 配合 GDB思路其实一样。以 Java 远程调试为例启动服务时加上这样的参数java -agentlib:jdwptransportdt_socket,servery,suspendn,address5005 -jar your-service.jar然后在 IDEA 里新建一个 Remote JVM Debug 配置Host 填服务器地址Port 填 5005启动调试就可以像本地一样打断点了。这里的suspendn很关键它表示服务启动时不要一直挂起等待调试器连接否则没人连它就一直不启动。远程调试确实方便但我个人有一条原则只在测试环境或内网环境使用生产环境最好不要开这个口子。原因不复杂调试端口等于给服务开了一个后门配合调试协议是可以远程执行一些操作的安全性完全取决于网络环境。3. 一次完整的 Java 接口调试实录很多热词里都提到“java 想要调试某个接口 如何debug”说明这是后端开发最高频的需求。我拿一个典型的业务接口来说说从接到问题到定位原因完整走一遍。3.1 先理清调用链再打断点假设现在有一个下单接口前端反馈“偶尔会重复下单”。接到这个反馈不要急着在 Controller 第一行打断点。先做三件事确认前端什么时候发的请求是不是做了多次提交没有做按钮 loading确认是否存在重试机制比如网关超时自动重试确认订单服务是否做了幂等校验。这三件事表面上看起来像“业务分析”实际上就是在缩小调试范围。把顺序理清楚之后我通常会在三个位置临时加日志接口入口记录请求参数、核心校验处记录校验结果、落库前后记录订单号和数据。这样跑一轮流程日志一对比基本就能看出是“同一笔请求被发送了两次”还是“两笔不同的请求进来了”。如果确认是需要用断点细看的逻辑再打开 IDEA 调试模式在 Controller 入口打断点看请求体里的参数是不是跟日志一致。这一看其实就是在验证“前面几层有没有问题”如果入口参数已经异常就不用再往后查了。3.2 断点命中了下一步看什么断点命中只是开始接下来最容易犯的错误是“不知道往哪看”。我的建议顺序是先看调用栈确认这个接口是从哪条链路进来的。除了 HTTP 入口是不是还有 MQ 消费者在调同一个方法你是不是停错了入口。再看当前栈帧的局部变量重点看参数对象里的字段是否存在明显异常比如空指针、默认值、过期时间。最后用 Step Into 进入关键方法一行一行确认逻辑分支是否真正走入了预期路径。这里还有个非常实用的技巧如果问题只在特定条件下出现直接在断点上写条件。比如只关心用户 ID 为 10086 的请求断点条件可以写userId 10086。这样就不用几十次手动跳过无关请求。条件断点看似只是 IDE 的某个选项实际用得好不好直接决定你是在“做调试”还是“在熬时间”。3.3 远程调试 Java 接口的配置细节前面提到的 JDWP 参数实际配置时还有一个容易踩的坑防火墙。服务器 5005 端口开没开、本机到服务器网络通不通很多人一调不通就怀疑参数写错了。正确的排查顺序是先telnet 服务器IP 5005看端口通不通再回头看 IDE 配置。端口不通的话JDWP 配得再对也白搭。还有一点经验如果服务是通过 Docker 跑的映射端口时要同时映射调试端口。我遇到过一次服务容器只映射了业务端口调试端口没映射出来本地怎么都连不上折腾半天才发现是端口映射的问题。suspend 参数的取舍也可以根据场景调整。suspendy表示 JVM 启动后会停下来等调试器 attach适合需要从启动第一行代码就要跟踪的场景比如排查启动流程问题suspendn则适合服务先启动、随时再 attach 的场景。大多数线上排查用 n 就够不需要让服务卡住等待。4. 嵌入式调试另一个维度的“debug”嵌入式调试跟纯软件调试区别非常大。最大的区别在于代码是跑在真实硬件上的旁边还有一个“独立于 CPU 内核而存在”的看门狗还有时钟、低功耗模式、硬件复位等一大堆变量。热词里有人问“单片机如何 debug 导致单片机重启”这几乎是每个嵌入式工程师都会撞上的问题。4.1 单片机一调试就重启先查这三处第一个嫌疑是看门狗。很多人用 STM32 开了独立看门狗IWDG或窗口看门狗WWDG调试时程序停在断点处CPU 不喂狗了但看门狗外设还在跑。只要超出喂狗时间窗看门狗就会把芯片复位。从现象看就是“一进调试就重启”非常迷惑。这类问题有两种解决办法一是条件编译里加一个调试开关调试版本直接关闭看门狗初始化二是调试时把喂狗代码里的断点打在一个不影响时序的位置保证暂停时间不至于太长。我更推荐前者因为调试时关掉保护机制本来就是很常见的做法只要记得发布前打开就行。第二个嫌疑是低功耗模式。如果代码里进入了睡眠或停止模式调试器再想通过 SWD/JTAG 接口连接就非常困难因为内核时钟已经停了。看起来也是“连不上”或者“一连接就复位”。处理办法是先把低功耗模式相关的代码屏蔽掉或者等待芯片被外部事件唤醒后再连接。第三个嫌疑是硬件复位和调试器复位信号冲突。某些板上复位电路容值偏大调试器拉低复位脚的时间不够就会导致调试器反复初始化失败。可以尝试在调试器设置里把复位延时调大或者改成不执行硬件复位直接连接。下面用表格总结一下排查顺序现象优先怀疑快速验证一进调试就重启看门狗注释掉 IWDG/WWDG 初始化后再跑调试器连接不上低功耗模式屏蔽睡眠代码段连接失败或反复初始化复位电路/线缆降低调试速率或手工复位目标板在线调试正常但下载后跑飞启动文件/时钟配置检查 SystemInit 与默认时钟树4.2 Keil MDK 调试配置细节以 STM32 Keil 为例工程配置里值得注意的选项其实不少。很多人 Debug 页面选好了 ST-Link 就直接点仿真结果要么识别不到芯片要么下载后不自动运行。建议按这个顺序过一遍Options for Target - Debug右上角选择调试器ST-Link Debugger 或 J-Link然后点 Settings。Settings 里确认 Debug 连接方式选的是 SWD 还是 JTAG这个必须跟板子上的调试口一致再看 Port 速率如果线比较长或者有干扰建议把速率降下来比如从 4MHz 降到 1MHz。还有一个经常被忽略的Flash Download 里的 Programing Algorithm。如果芯片型号匹配但对算法选择不对下载时也会报错。另外我习惯勾选 “Reset and Run”这样下载完程序就能直接跑起来省得每次手动按复位。4.3 用 VS Code 调试 Keil 工程现在的趋势越来越多人用 VS Code 写代码但工程还是 Keil 的。两者怎么结合起来调试简单粗暴的做法是用 Keil 带的是 ARM 编译器编译然后让 VS Code 里的 Cortex-Debug 插件连接同一个调试器。这里面最核心的是一个launch.json配置。大致思路是这样{ type: cortex-debug, request: launch, servertype: stlink, device: STM32F407VG, executable: ${workspaceFolder}/build/your_project.elf, svdFile: ${workspaceFolder}/STM32F407.svd, interface: swd, runToEntryPoint: main }executable指向的是 Keil 编译生成的 .axf 或 .elf 文件servertype对应实际用的调试器device必须改成你的芯片型号。配置好后VS Code 里按 F5 就能开始调试断点、变量、外设寄存器窗口都能用。用 VS Code 调试的最大好处不是断点功能更强而是能把代码浏览、搜索、版本管理都放在一个界面里。不过我也要说句实话Keil 内置调试器在硬件外设查看方面更直接比如看 GPIO 状态、看外设寄存器的变化VS Code 要配合 SVD 文件才能体验接近。所以我的选择是日常写代码用 VS Code需要深入看外设寄存器时切回 Keil两者互补。4.4 平台级调试验证调试串口与整机调试模式嵌入式不止单片机还有 Linux 类平台这类平台调试入口基本靠“调试串口”。拿瑞芯微这类 SoC 平台来说要修改 debug 串口通常要动三处U-Boot 环境变量里的 console 设置、内核 cmdline 里的console参数、以及设备树里对应 UART 节点的引脚复用。任何一处不匹配都会出现“内核启动日志打到一半换了个串口”的情况。另外以前做方案调试时经常需要进入系统底层的“可调试状态”。比如调试音频通路时不仅要接耳机看声音还要打开底层日志开关把音频 DSP 和 Codec 的寄存器状态打印出来。系统默认状态可能会裁剪日志、限制调试接口所以做这类调试的第一步往往是确认系统处于可调试模式把权限限制和日志级别都放开然后再去复现问题和打日志。千万别一上来就改音频参数那样改错了都不知道是参数问题还是链路问题。5. 启动类故障与诡异报错的排查实录调试中有一类问题特别让人头大就是工具或运行时环境在“启动阶段”就报错。热词里的几个报错IDEA 的fatal error in native method、CCS 的IcePick_C_0初始化失败、以及vd is starting, please check vendor daemons status我都实际遇到过这里逐个拆一遍。5.1 IDEA 报“Fatal error in native method”怎么查这个报错字面意思是“在 native 方法里发生了致命错误”。很多人一看这个词就懵native 方法是什么Java 里用 JNI 调用的非 Java 代码比如某些加密库、数据库驱动、热部署 Agent都可能是触发点。遇到这个报错我的排查路径是三步走。第一步去应用工作目录找hs_err_pid*.log这是 JVM 崩溃时的现场日志里面记了崩溃线程、寄存器、栈帧还有触发崩溃的 native 库名称这是最重要的线索。第二步检查最近是不是改了什么 Agent 或者 JVM 参数比如新增了-javaagent配置、升级了某个 SDK 版本优先回滚验证。第三步排除是不是硬件或驱动层面的兼容性问题比如某些 Windows 环境下杀毒软件注入导致 native 崩溃这种情况虽然少见但确实存在。这个报错不像普通业务异常那样有清晰的堆栈要有点耐心关键就是先看hs_err日志不要在 IDE 里反复重启碰运气。5.2 CCS 卡在“Initializing: IcePick_C_0”怎么办CCS 是 TI 的嵌入式 IDE报错消息里面的IcePick_C_0指的是 ARM 调试访问组件DAP中的其中一个调试接口翻译成人话就是调试器正在尝试通过 ARM 内核的调试口连接目标芯片但卡住了。比较常见的原因有这么几种目标板没有稳定供电、调试线接触不良、芯片处于复位状态、或者是另一个调试器/调试会话已经占用了同一个调试口。还有人在调试低功耗代码时遇到这个芯片睡了调试口当然连不上。排查动作按顺序来先断电重新上电恢复芯片到一个确定能连上的状态再检查调试器与板子的连接线尤其是地线地线不可靠时最容易出现这种“初始化到一半就挂”的现象如果还不行在 CCS 的调试配置里把 JTAG/SWD 速率调低比如从 5MHz 降到 500kHz能提高连接稳定性。最后确认一下电脑上是不是同时开了多个调试窗口连同一个板子这种情况经常会互相把对方顶掉。5.3 FlexLM 启动报“vd is starting, please check vendor daemons status”这个报错多出现在商业软件授权服务启动时属于许可证服务FlexLM的经典问题。它的含义是许可证主服务已经启动了但它需要的一个 vendor daemon厂商守护进程没有正常起来服务之间握手失败。排查方法并不复杂就是要多看日志。按照正常流程先确认环境变量LM_LICENSE_FILE或服务配置指向的 license 文件路径是否正确再检查 license 文件里VENDOR行指定的守护进程名字和可执行文件是否存在最后看日志输出确认 vendor daemon 有没有因为端口被占用、缺少系统库、权限不够等原因退出。有些时候问题出在启动顺序上。FlexLM 的 license server 和 vendor daemon 是两套进程配置自动启动时如果脚本只起了前者也会出现这个提示。我在实际项目里遇到最多的情况反而是防火墙拦截了 TCP/IP 端口导致本机里两个进程都互相访问不到。先查日志再查端口最后查权限基本能覆盖大多数这类问题。6. 调试习惯比调试技巧更值钱文章写到最后我不想讲什么大道理就说说踩过无数坑之后沉淀下来的几个习惯。这些习惯很难被写进官方文档但每一个都是靠加班和熬夜换来的。第一个习惯是先保护现场再动手。我在调试任何问题之前先花两分钟把当前现象、日志、操作步骤截个图或者存成文件。别嫌这两分钟浪费很多时候你改了几行代码之后发现“咦现在报错跟刚才不一样了”回头一看现场没了那才是真的崩溃。第二个习惯是“一次只改一个变量”。新手最喜欢的操作是同时怀疑两个三个原因然后一次性改掉多处代码跑完发现还是不工作但完全不知道是哪个改动没生效。正确做法是每改一处就验证一次哪怕这处改动你觉得“肯定不是它的原因”。这个习惯看起来笨实际上在漫长调试中最省时间。第三个习惯是善用“二分查找”。如果一段逻辑很长不要在中间打断点仿佛能一眼看到问题。而是把范围分成两半先看前半段的输出对不对对就继续看后半段不对就往前半段收敛。循环几次之后问题区间会指数级缩小。别小看这个思路它背后的逻辑跟算法里的二分查找一模一样只是很多人不会把它用在调试上。最后再分享一个非常实用的小技巧写代码的时候顺手留好“上下文”。我在项目的核心入口和每一层调用的出口都会输出关键参数的摘要日志。这样线上出了问题不用远程打断点光看日志基本就能推断出问题出在哪一层。事后加日志的调试永远比事先留日志的调试慢很多。调试这件事说到底拼的不是天赋而是你有没有一套稳定的方法论。这个系列后面还会继续更新遇到各种有意思的“debug 实录”我都会补进来敬请期待。