前言
之前在 macOS 下从零配置 Neovim 里,我把 Neovim 的基础外观、目录树和终端插件配好了;后来整理 Neovim 快捷键速查 时,留了个坑说 LSP 和 DAP 的配置还没写。
正好最近在写 MIT 6.1810 的 xv6 实验,每天都要在内核源码里跳来跳去,还要连上 QEMU 单步跟代码。这篇就把我本地 ~/.config/nvim 里的 clangd 语言服务和 nvim-dap 调试配置补上。
重要
LSP 与 DAP 的职责划分
- 代码跳转与补全(LSP):走的是
Neovim → clangd,靠项目根目录的.clangd或compile_commands.json看懂代码结构。- 打断点单步调试(DAP):走的是
Neovim → nvim-dap → riscv64-elf-gdb → QEMU,靠编译出来的 ELF 符号表和远程端口连上虚拟机。这两条路是完全独立的:
gd能跳转到函数,不代表 GDB 已经加载了符号;反过来 GDB 能连上 QEMU,也不代表clangd认识交叉编译的头文件。
配置 clangd 语言服务
1. 启用内置 LSP 客户端
我在 lua/plugins/lsp.lua 里直接用了 Neovim 0.11+ 原生自带的 vim.lsp.config 和 vim.lsp.enable 接口(参考 Neovim LSP 文档),不用再装 nvim-lspconfig 也能直接配:
vim.lsp.config("clangd", {
cmd = {
"/usr/bin/clangd",
"--background-index",
"--clang-tidy",
"--completion-style=detailed",
},
root_markers = {
"compile_commands.json",
".clangd",
".git",
},
})
vim.lsp.enable("clangd")这里的 /usr/bin/clangd 是 Mac 自带 Command Line Tools 里的路径;如果自己用 Homebrew 装了 LLVM,改成对应的路径就行。
2. 配置 RISC-V 交叉编译参数
因为我的电脑是 Apple Silicon 的 Mac,如果什么都不配,clangd 默认会拿 macOS 的系统头文件去检查 xv6 代码,满屏都会是架构不匹配的报错。
最简单的办法是在 xv6 仓库根目录下建一个 .clangd 文件,加上交叉编译的目标架构(参考 clangd 配置说明):
CompileFlags:
Add:
- --target=riscv64-unknown-elf提示
提示 上面这个简短的
.clangd只是让它认识 RISC-V 架构,还没把Makefile里的-ffreestanding、-I.等参数全写进去。如果遇到稍微大一点的 C/C++ 项目,更推荐用bear -- make生成一份完整的compile_commands.json放在项目根目录下(详见 clangd 编译数据库说明)。
3. 绑定 LSP 缓冲区快捷键
我把 gd、gr、K 这些快捷键放在了 LspAttach 自动命令里,只有当前缓冲区真的连上了语言服务器才会生效,不会去干扰右侧目录树或者左侧终端窗口:
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local opts = { buffer = args.buf }
vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
vim.keymap.set("n", "gr", vim.lsp.buf.references, opts)
vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, opts)
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, opts)
end,
})如果打开 .c 文件发现按 gd 没反应,可以先敲这几条命令看看是不是 clangd 没连上:
:checkhealth vim.lsp
:verbose nmap gd
:lua vim.print(vim.lsp.get_clients({bufnr = 0}))另外我没有专门装自动弹窗的补全插件,写代码时想看补全提示,在插入模式下按原生的 <C-x><C-o> 就能调出 LSP 的补全列表。
配置 nvim-dap 与 GDB 适配器
1. 注册 GDB DAP 适配器
新版 GDB(14.1 以后)自己就内置了 --interpreter=dap 模式(见 GDB DAP 文档),不需要再去装第三方转接工具。
在 lua/plugins/dap.lua 里,我把之前用 Homebrew 装的 riscv64-elf-gdb 注册成了名为 gdb 的适配器:
local dap = require("dap")
dap.adapters.gdb = {
type = "executable",
command = "riscv64-elf-gdb",
args = {
"-q",
"-nx",
"--interpreter=dap",
},
}注意里面的 -nx 参数很关键:它会让 GDB 忽略项目目录下的 .gdbinit 文件,避免 .gdbinit 里自带的自动连接命令和 nvim-dap 自己的连接流程撞车。
2. 添加项目调试连接配置
整理这篇笔记翻看自己的 dap.lua 时,我发现当时只写了上面的 dap.adapters.gdb,漏写了告诉它“具体怎么连 xv6”的 dap.configurations.c。如果少了这一段,按 <Space>dc 开始调试的时候就会没有配置可选。
在 dap.lua 的适配器配置下面补上这段即可:
dap.configurations.c = {
{
name = "xv6: attach QEMU",
type = "gdb",
request = "attach",
program = "${workspaceFolder}/kernel/kernel",
target = "127.0.0.1:25501",
},
}这里有两个地方要注意:
program指向的是kernel/kernel(内核二进制)。target里的端口25501是我这台电脑上算出来的值。xv6 的Makefile会根据系统用户的 UID 算出一个端口号,配之前先在 xv6 目录里敲一下make print-gdbport看看你自己的端口是多少:
make print-gdbport启动调试与常用快捷键
因为上面配的是 attach(附加到已经跑起来的 QEMU),按 <Space>dc 并不会自动帮你跑 make。平时调试分两步走:
-
先在终端(或者按
<Space>tt打开 Neovim 左侧终端)里启动等待调试的 QEMU:make CPUS=1 qemu-gdb -
切回代码窗口,在想停的地方按
<F9>打个断点,然后按<Space>dc选择xv6: attach QEMU。
连上之后,底部的 nvim-dap-ui 面板会自动弹出来显示变量、寄存器和调用栈,代码行尾也会直接显示变量当前的值:
| 常用操作 | 快捷键 |
|---|---|
| 打断点 / 取消断点 | <F9> |
| 开始调试 / 继续运行 | <Space>dc |
| 单步跳过(不进函数) | <F10> |
| 单步进入函数 | <F11> |
| 跳出当前函数 | <F12> |
| 弹窗查看变量或表达式的值 | <Space>de |
| 打开 / 关闭 GDB 命令行控制台(REPL) | <Space>dr |
| 结束调试并关掉底部面板 | <Space>dq |
用户态程序的符号加载
用上面那套配置直接给 kernel/ 底下的内核文件打断点非常丝滑,但如果你打开 user/find.c 按 <F9> 打断点,会发现断点变成 pending,GDB 还会提示找不到 find.c。
这是因为配置里的 program 只加载了内核 kernel/kernel 的符号表,还没有加载 user/_find 的符号。遇到这个问题不用去重装插件,按 <Space>dr 打开调试控制台,执行 add-symbol-file user/_find 把用户程序的符号追加进去即可,具体排查过程见 xv6 调试排错:解决 GDB 找不到用户态源码与断点挂起。