ARTICLE DETAIL

资讯详情

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

用 clangd 和编译数据库为 STM32H7 遗留固件搭建代码分析工作台

用 clangd 和编译数据库为 STM32H7 遗留固件搭建代码分析工作台 1. 接手一份没人讲得清的固件我做了个工具1.1 这个项目到底在解决什么问题接手一份没人讲得清的固件这件事本身就够让人头疼了。代码能编译、能烧录、板子能跑但你要问这个函数谁调的这个全局变量在哪改的这个中断为什么这么配没人答得上来。前任工程师离职了文档只有半页 Word注释停留在三年前剩下的全靠猜。我这次接手的是一块基于 STM32H7 的板子固件规模不算大大概六万多行 C 代码但模块之间耦合得厉害光看代码根本理不清调用关系。我做的这个工具核心目标就一个把一份没人讲得清的固件变成一份自己能讲得清的代码地图。它不是一个全新的 IDE也不是什么黑科技而是把 VS Code、clangd、编译数据库这几样东西串起来再加上我自己写的一层脚本让代码跳转、符号查找、调用链分析、外设寄存器定位这些事变得顺手。说白了就是给一份陌生固件做一次体检 建档让后来的人不用再从零开始猜。这个工具适合谁适合所有需要接手遗留嵌入式项目的人适合想用现代编辑器写 STM32 但又不想被 Keil、IAR 绑死的人也适合那些想搞清楚 clangd 到底怎么配、为什么跳转跳不准的人。哪怕你用的是别的 MCU只要能把编译命令导出来这套思路都能复用。1.2 为什么不用现成的 IDE 方案很多人第一反应是直接用 Keil 或者 IAR 不就行了能跳转、能调试、能看寄存器。问题是Keil 的代码跳转在大型工程里经常卡顿索引慢跨文件查找体验差而且它那套工程文件是私有的你想做点自动化分析很难。IAR 好一些但同样封闭。更关键的是这些 IDE 的跳转只解决定义在哪不解决谁调用了它这个变量在哪些中断里被改过这个寄存器映射对应哪个外设。VS Code 加 clangd 这套组合优势在于开放。clangd 基于 LLVM 的编译前端它理解的是真正的 C 语言语义不是文本匹配。只要给它一份准确的 compile_commands.json它就能给出准确的跳转、补全、诊断。而且 VS Code 的插件生态可以让我把调用链分析寄存器映射查询固件版本对比这些自定义功能挂上去。换句话说我不是在用一个编辑器我是在搭一个可扩展的固件分析工作台。当然这套方案也有代价。clangd 对编译数据库的准确性要求很高交叉编译工具链的路径、宏定义、头文件搜索路径一个都不能错。STM32H7 的工程往往还涉及汇编启动文件、链接脚本、HAL 库的条件编译这些都会影响 clangd 的索引结果。我踩过的坑后面会细说但结论是前期配置麻烦一点后期收益巨大。2. 核心思路拆解让 clangd 真正读懂 STM32H7 固件2.1 编译数据库是整个方案的地基clangd 不像 Keil 那样自己维护工程模型它依赖一个叫compile_commands.json的文件。这个文件里记录了每个源文件是怎么被编译的用的什么编译器、什么宏、什么头文件路径、什么标准。clangd 拿到这些信息后才能用和真实编译一致的条件去解析代码。如果这个文件不准clangd 就会看错代码跳转自然也不准。对于 STM32H7 工程生成这个文件有几种常见做法。如果工程用的是 CMake那最简单在配置时加-DCMAKE_EXPORT_COMPILE_COMMANDSON就行。但很多遗留固件用的是 Makefile 或者 Keil 工程这时候就需要用bear或者compiledb这类工具去拦截编译命令。我这次接手的工程用的是 Makefile所以我用bear -- make跑了一遍完整编译生成了初始的compile_commands.json。但这里有个关键问题交叉编译工具链的路径。STM32H7 通常用arm-none-eabi-gcc如果你的系统里装了多个版本或者路径里有空格clangd 可能会解析失败。我的做法是在生成数据库后写了一个小脚本把里面所有编译器路径统一替换成绝对路径并且去掉-mcpu、-mfpu这些 clangd 不认识的 GCC 专有参数。这一步不做clangd 会报一堆unknown argument警告索引质量大打折扣。2.2 为什么选择 clangd 而不是 Microsoft IntelliSenseVS Code 自带的 C/C 插件用的是 Microsoft IntelliSense它也能跳转、也能补全。但在嵌入式场景下IntelliSense 有几个硬伤。第一它对交叉编译工具链的支持不如 clangd 原生。第二IntelliSense 的配置是写在c_cpp_properties.json里的宏和路径要手动维护工程一大就容易和真实编译脱节。第三clangd 的诊断更接近编译器真实行为比如它会对未使用的变量、隐式类型转换给出更准确的提示。我实测下来同一个 STM32H7 工程IntelliSense 在跳转到 HAL 库内部时经常跳到错误的宏分支而 clangd 因为拿到了真实的-D宏定义跳转准确率高很多。另外clangd 支持后台索引第一次打开工程会慢一点但索引完成后全局符号查找、跨文件引用查找的速度非常快。对于六万行级别的固件这个体验差距很明显。还有一个细节clangd 支持--background-index和--clang-tidy。前者让它在后台建立索引后者可以在你写代码时给出静态检查建议。对于接手陌生固件的人来说clang-tidy 能帮你发现一些潜在的 bug比如未初始化的变量、可疑的指针运算。这些在遗留代码里很常见。2.3 工具的整体架构我的工具不是一个单体程序而是一组脚本加配置的集合。整体分三层底层编译数据库生成与清洗。包括bear拦截、路径替换、参数过滤、宏定义补全。中层clangd 配置与 VS Code 工作区设置。包括.clangd配置文件、settings.json、插件推荐列表。上层自定义分析脚本。包括调用链提取、寄存器映射查询、固件版本对比、符号统计。这三层里底层最脏最累但最重要。中层决定日常使用体验。上层是我自己加的私货用来解决那些 clangd 本身不提供的分析需求。比如我想知道某个全局变量在哪些函数里被写过clangd 只能告诉我引用位置但我写了个脚本结合clangd --query和正则把写操作单独筛出来。这个在排查变量被谁改了的问题时特别有用。3. 实操过程从零搭建固件分析工作台3.1 环境准备与工具链确认先确认你的交叉编译工具链能正常工作。在终端里跑arm-none-eabi-gcc --version如果提示找不到命令先把它加到 PATH 里。我习惯把工具链放在/opt/gcc-arm-none-eabi下然后在~/.bashrc里加一行export PATH/opt/gcc-arm-none-eabi/bin:$PATH接着确认工程能编译通过。这一步不能跳过因为bear需要真实执行编译命令才能拦截。如果工程本身编译不过生成的数据库也是残缺的。我这次接手的固件有一个模块依赖一个已经删除的头文件导致编译失败。我先把这个依赖临时补上确保全量编译通过再生成数据库。然后安装必要工具sudo apt install bear clangdclangd的版本建议用 15 以上对 C 语言的支持更完善。VS Code 里安装 clangd 插件注意要把 Microsoft C/C 插件的 IntelliSense 关掉否则两个插件会打架。具体做法是在settings.json里加C_Cpp.intelliSenseEngine: disabled3.2 生成并清洗 compile_commands.json在工程根目录执行bear -- make -j8如果 Makefile 里有clean目标先make clean再跑上面这条。跑完后当前目录会出现compile_commands.json。先别急着用打开看看内容。你会看到每个条目里有directory、command、file三个字段。重点检查command里的编译器路径和参数。我写了一个 Python 脚本做清洗核心逻辑是import json import re with open(compile_commands.json) as f: data json.load(f) for entry in data: cmd entry[command] # 替换编译器为绝对路径 cmd cmd.replace(arm-none-eabi-gcc, /opt/gcc-arm-none-eabi/bin/arm-none-eabi-gcc) # 移除 clangd 不认识的参数 for flag in [-mcpu, -mfpu, -mfloat-abi, -mthumb]: cmd re.sub(r\s* re.escape(flag) r\S*, , cmd) entry[command] cmd with open(compile_commands.json, w) as f: json.dump(data, f, indent2)这里解释一下为什么要移除-mcpu这些参数。clangd 用的是 clang 前端它不认识 GCC 的-mcpucortex-m7这种写法会报错并可能导致索引失败。移除这些参数不影响代码语义分析因为 clangd 主要关心宏定义和头文件路径不关心具体指令集。但-D宏定义和-I头文件路径绝对不能动那是 clangd 理解代码的关键。清洗完后在工程根目录建一个.clangd文件内容如下CompileFlags: CompilationDatabase: . Add: - -stdgnu11 - -DUSE_HAL_DRIVER - -DSTM32H743xx Remove: - -W*Add里补上工程实际使用的宏Remove里去掉所有警告相关参数避免 clangd 刷屏。STM32H743xx这个宏要根据你的具体型号改STM32H7 系列不同型号的寄存器定义不一样宏错了会导致外设寄存器跳转错误。3.3 VS Code 工作区配置在工程根目录建.vscode/settings.json{ clangd.arguments: [ --background-index, --clang-tidy, --compile-commands-dir${workspaceFolder}, --query-driver/opt/gcc-arm-none-eabi/bin/arm-none-eabi-gcc ], clangd.path: /usr/bin/clangd, C_Cpp.intelliSenseEngine: disabled, files.associations: { *.h: c } }--query-driver这个参数很关键。它告诉 clangd 去查询交叉编译器的系统头文件路径。STM32H7 工程会用到stdint.h、stddef.h这些编译器自带头文件如果不加这个参数clangd 找不到它们会报一堆file not found。加上之后clangd 会自动把 GCC 的 include 路径纳入索引。--background-index让 clangd 在后台建索引第一次打开工程会看到右下角有进度条六万行代码大概需要几分钟。索引完成后CtrlClick跳转、F12查看定义、ShiftF12查看引用都会非常快。3.4 自定义分析脚本调用链与寄存器映射clangd 本身不提供调用链视图但它的--query模式可以输出符号信息。我写了一个脚本结合clangd --query和grep提取某个函数的调用者和被调用者。核心命令是clangd --queryusr --checkMyFunction main.c这个命令会输出MyFunction的 USR统一符号解析然后我拿这个 USR 去索引文件里查引用。索引文件在.cache/clangd/index/下是二进制格式但 clangd 提供了clangd-indexer工具可以转成文本。不过更简单的做法是用 VS Code 的ShiftF12手动看或者用cscope配合。我实际用的是cscope加ctags作为补充。虽然 clangd 的语义分析更准但 cscope 在谁调用了这个函数这种查询上更快而且支持命令行。我的做法是日常跳转用 clangd批量调用链分析用 cscope。两者互补不冲突。寄存器映射查询是另一个痛点。STM32H7 的寄存器定义在 HAL 库里但 HAL 库的宏层层嵌套有时候你想知道GPIOA-ODR对应的物理地址得翻好几层。我写了一个脚本解析stm32h743xx.h里的结构体定义生成一张外设-寄存器-地址的映射表。这个表在排查硬件问题时特别有用比如你怀疑某个 GPIO 配置错了直接查表就知道该往哪个地址写。4. 常见问题与排查技巧实录4.1 clangd 跳转不准的几种典型情况跳转不准是这套方案最常见的问题原因通常有三类。第一类是编译数据库不完整某些源文件没被bear拦截到。这种情况表现为打开某个.c文件clangd 提示未找到编译命令。解决办法是检查compile_commands.json里有没有这个文件的条目如果没有说明 Makefile 里这个文件是通过特殊规则编译的需要手动补一条。第二类是宏定义冲突。STM32H7 的 HAL 库大量使用条件编译比如#ifdef STM32H743xx。如果你的.clangd里定义的宏和实际编译不一致clangd 会走进错误的分支跳转到错误的定义。排查方法是在 VS Code 里打开一个 HAL 源文件看 clangd 状态栏显示的编译命令和compile_commands.json里的对比。第三类是头文件搜索路径缺失。交叉编译工具链的系统头文件、CMSIS 头文件、HAL 库头文件任何一个路径缺失都会导致跳转失败。--query-driver能解决系统头文件问题但 CMSIS 和 HAL 的路径需要你在.clangd的Add里手动补-I参数。我的经验是把compile_commands.json里所有-I路径提取出来去重后统一加到.clangd里这样最保险。4.2 索引速度慢与内存占用高六万行代码的工程clangd 后台索引大概吃 1.5GB 内存索引时间几分钟。如果你的机器内存小于 8GB可能会卡。优化方法有几个一是关闭--clang-tidy它会在索引时做额外检查很吃资源二是用--background-index-prioritylow降低索引线程优先级三是把不参与编译的目录比如Docs、Tools加到.clangd的Exclude里。还有一个坑如果你在 VS Code 里同时打开了多个工程clangd 会为每个工程建索引内存直接翻倍。我的做法是一次只开一个工程的工作区需要对比时用git worktree或者直接开两个 VS Code 窗口但只让一个启用 clangd。4.3 固件版本对比的实用技巧接手遗留固件时经常需要对比两个版本的差异。我用git diff加clangd的符号信息做了一个简易对比工具。思路是把两个版本的compile_commands.json都生成好然后用脚本提取每个版本的所有函数名和全局变量名做集合差集。这样能快速看出新版本多了哪些函数哪些函数被删了。更进一步如果两个版本都能编译可以用arm-none-eabi-objdump反汇编对比关键函数的汇编差异。这个在排查为什么新版本跑飞了时特别有用。我遇到过一个问题新版本某个中断处理函数多了一个局部变量导致栈溢出。用 objdump 对比栈帧大小一眼就看出来了。4.4 常见问题速查表问题现象可能原因排查方法解决办法跳转到错误宏分支宏定义与真实编译不一致对比.clangd和compile_commands.json的-D统一宏定义提示找不到头文件搜索路径缺失看 clangd 日志的 include 路径补-I或加--query-driver索引卡住不动内存不足或文件太多看系统内存占用关 clang-tidy排除无关目录某些文件无编译命令Makefile 特殊规则检查compile_commands.json手动补条目补全不出现IntelliSense 冲突看插件是否同时启用禁用 C/C 插件 IntelliSense寄存器跳转错误型号宏不对检查STM32H7xx宏改成实际型号4.5 几个我踩过的坑第一个坑bear和make -j一起用时偶尔会漏掉一些编译命令。原因是并行编译时bear的拦截可能丢事件。解决办法是用bear -- make不加-j虽然慢一点但数据库完整。或者用compiledb工具它对并行编译的支持更好。第二个坑.clangd文件里的Remove用了通配符-W*结果把-Wl,链接参数也删了。虽然 clangd 不关心链接但有些宏定义是写在链接参数里的删了会导致宏缺失。后来我改成精确删除-Wall、-Wextra这些具体参数。第三个坑VS Code 的 clangd 插件和远程 SSH 一起用时clangd 默认在本地跑索引的是远程文件速度极慢。正确做法是在远程机器上装 clangd然后 VS Code 通过 SSH 连接后插件会自动用远程的 clangd。这个在settings.json里不用特别配但你要确保远程机器的clangd在 PATH 里。第四个坑STM32H7 的PA0_C、PA1_C这类引脚在 HAL 库里的定义和普通PA0不一样clangd 有时候会跳转到错误的宏。原因是这些引脚涉及模拟开关配置HAL 库用了不同的宏分支。解决办法是在.clangd里补上-DUSE_HAL_DRIVER和对应的系列宏确保走进正确的分支。5. 工具之外的思考怎么让没人讲得清变成讲得清5.1 代码地图只是第一步工具能帮你跳转、能帮你查引用但讲得清最终还是要靠人。我在搭完这套环境后做了一件事用 clangd 的符号列表加 cscope 的调用关系画了一张模块依赖图。不是那种自动生成的、密密麻麻的图而是手工整理的、只保留核心模块的图。这张图后来成了团队新人的入门材料。具体做法是先用脚本提取所有跨模块的函数调用然后按模块聚合得到模块 A 调用了模块 B 的哪些函数。再结合固件的实际功能把模块分成驱动层中间层应用层。这张图贴在 Wiki 上比任何文档都直观。5.2 给后来者的建议如果你也接手了一份没人讲得清的固件我的建议是先别急着改代码先花两天把环境搭起来。这两天看似没产出但后面能省你两周的猜谜时间。环境搭好后第一件事是跑通全量编译第二件事是生成编译数据库第三件事是确认 clangd 跳转准确。这三步做完你才算真正看见了这份固件。另外别迷信工具。clangd 再准也替代不了你对业务逻辑的理解。工具帮你找到这个函数在哪但这个函数为什么这么写还得靠读代码、问人、做实验。我这次接手固件光靠工具是不够的还找了前任的 Git 提交记录、翻了三年前的邮件、甚至反汇编了旧版本的固件来对比行为差异。工具是杠杆但支点还是你自己的判断。5.3 后续可以扩展的方向这套工具目前只解决了看代码的问题还没解决看运行时的问题。下一步我打算把pyocd或者openocd集成进来在 VS Code 里做实时变量监控和断点调试。这样从静态分析到动态调试就打通了。另外clangd 的--clang-tidy可以配置自定义检查规则我打算针对 STM32H7 的常见坑比如中断里调用阻塞函数、DMA 缓冲区未对齐写几条规则让工具主动提醒。还有一个想法把固件版本对比和 CI 结合起来。每次提交代码自动生成新的compile_commands.json自动跑符号差异分析如果发现删除了某个被广泛引用的函数就报警。这个在团队协作里能避免很多改一处崩一片的问题。最后分享一个小技巧如果你用的是 VS Code 加 SSH 远程开发记得在远程机器的~/.clangd里也放一份配置或者在工程根目录的.clangd里写全路径。因为 clangd 在远程跑的时候工作目录可能和你想象的不一样相对路径容易出错。用绝对路径虽然丑但稳。
返回列表