41

Vim 最强调试插件:Vimspector

 3 years ago
source link: http://www.kongjunblog.xyz/2021/04/vim-vimspector.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Vim 最强调试插件:Vimspector 跳至主要内容

Vim 最强调试插件:Vimspector

Vimspector是一个基于DAP(debug adapter protocol)的Vim多语言调试插件,理论上能够支持所有支持语言(只要有对应的 DAP)。这个插件仍在实验阶段,可能会有各种bug,但是对C/C++、Python 等流行的语言已经进行了充分的测试。

这篇文章以调试 C/C++ 程序为例,介绍 vimspector 的配置与使用。

  • 带 Python3.6+ 支持的 Vim 8.2 或更高版本
  • 带 Python3.6+ 支持的 Neovim-0.4.3 或更高版本(最好是 Nightly 版本)

由于 vimspector 的作者主要在 GNU/Linux 上使用 Vim 开发,因此 Vimspector 作者 puremourning 明确表示在 vimspector 的充分测试并稳定后才会提供对 Neovim 的完整支持(见 issue coc.nvim: Debug Adapter Protocol Support #322),因此目前对于 Neovim 和 Windows 的支持都处于实验阶段。我个人建议在 Vim 中使用本插件。

vimspector

使用vim-plug安装:

xxxxxxxxxx
Plug 'puremourning/vimspector'

使用 dein.vim 安装:

xxxxxxxxxx
call dein#add('puremourning/vimspector')

调试适配器

最新版的 vimspector 可以在 Vim 中通过命令:VimspectorInstall安装调试适配器,按Tab键可以补全。

也可以使用安装脚本安装,进入vimspector的安装目录,执行:

xxxxxxxxxx
./install_gadget.py <language-name>

install_gadget.py会自动下载<language-name>所需的调试适配器并进行相应配置,--help可以查看vimspector所支持的全部语言。

以在Linux环境上打开C/C++支持为例:

xxxxxxxxxx
./install_gadget.py --enable-c

vimspector会自动下载微软开发的调试适配器cpptools-linux.vsixyour-vimspector-path/gadgets/linux/download/vscode-cpptools/0.27.0/中。如果是在mac上,linux会被改成mac

如果下载速度过慢,可以自己下载好放置在上面提到的目录中,然后再执行以上命令。

Vimspector 使用 json 作为配置文件的格式,每个配置都是一个 json 对象。

Vimpector 有两类配置:

  • 调试适配器的配置

    • 如何启动或连接到调试适配器
    • 如何 attach 到某进程
    • 如何设置远程调试
  • 调试会话的配置

    • 使用哪个调试适配器
    • launch 或 attach 到进程
    • 是否预先设置断点,在何处设置断点

这两类配置可以对应多个配置文件,vimspector 会将多个配置文件中的信息合并成一个配置。

调试适配器配置

调试适配器的这个配置在打开 vimspector 对某语言的支持时就已经自动设置好了,存放在 your-path-to-vimspector/gadgets/linux/.gadgets.json 中。

比如在我的设备上,.gadgets.json 内容如下:

xxxxxxxxxx
{
  "adapters": {
    "vscode-cpptools": {
      "attach": {
        "pidProperty": "processId",
        "pidSelect": "ask"
      },
      "command": [
        "${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7"
      ],
      "name": "cppdbg"
    }
  }
}

其中变量${gadgetDir}代表着存放 .gadgets.json 的目录。除此之外,vimspector 还定义了其他预定义变量,并提供了自定义和用户输入变量内容的功能,以便我们编写比较通用的配置文件。

调试适配器的配置还可以存在于其他配置文件中,vimspector 读取一系列配置文件,生成adapters对象。

调试适配器的配置可以存在于以下文件中:

  1. our-path-to-vimspector/gadgets/<os>/.gadgets.json:这个文件由 install_gadget.py 自动生成,用户不应该修改它。
  2. your-path-to-vimspector/gadgets/<os>/.gadgets.d/*.json :这些文件是用户自定义的。
  3. 在 Vim 工作目录向父目录递归搜索到的第一个 .gadgets.json。
  4. .vimspector.json 中定义的adapters

编号代表配置文件的优先级,编号越大优先级越高,高优先级的配置文件将覆盖低优先级的配置文件中的的adapters

在我的机器上没有 your-path-to-vimspector/gadgets/<os>/.gadgets.d目录,可能是需要自己创建。

不进行远程调试的情况下不太需要修改默认的调试适配器配置。我一般没有进行远程调试的需求,没有实际使用过 vimspector 的远程调试功能(虽然这个功能是 vimspector 重点支持的),因此不介绍调试适配器的配置。

调试会话配置

项目的调试会话的文件位于以下两个位置:

  1. <your-path-to-vimspector>/configurations/<os>/<filetype>/*.json
  2. 项目根目录中的 .vimspector.json

每当打开一个新的调试会话时,vimspector 都会在当前目录向父目录递归搜索,如果查找到了 .vimspector.json,则使用其中的配置,并将其所在的目录设定为项目根目录;如果未查找到,则使用 vimspector 安装目录中的配置文件,将打开的文件的目录设置为项目根目录。

修改了.vimspector.json 后不需要重启 Vim 就可以使用最新配置。

vimspector.json 中只能包含一个对象,其中包含以下子对象:

  • adapters:调试适配器配置,如果不是进行远程调试,一般不需要设置
  • configurations:调试程序时的配置

configurations主要包含以下以下字段:

  • adapter:使用的调试配置器名称,该名称必须出现在adapters块或其他调试适配器配置中。
  • variables:用户定义的变量
  • configuration:配置名,如configuration1
  • remote-request,remote-cmdLine:远程调试使用

其中adapterconfiguration是必须的。

configuration需要包含的字段和使用的 DAP 有关,我使用vscode-cpptoolsconfiguration必须包含以下字段:

  • request:调试的类型,lauchattach
  • typecppdgb(GDB/LLDB)或cppvsdbg(Visutal Studio Windows debugger)

除了以上的选项,还可以设置程序路径、参数、环境变量、调试器路径等,更详细的信息可以查看 vscode-cpptools 文档launch-json-reference

上面的选项构成了 vimspector 配置文件的主体框架,完整的选项参考vimspector.schema.json

Vimspector 提供了比较灵活的变量定义功能,可以方便的自定义配置。

预定义变量

predefined variables

自定义变量

自定义变量要在起作用的配置中定义,并置于variables块中,如下面的GDBServerVersionSomeOtherVariable

xxxxxxxxxx
{
  "configurations": {
    "some-configuration": {
      "variables": {
        "GDBServerVersion": {
          "shell": [ "/path/to/my/scripts/get-gdbserver-version" ],
          "env": {
            "SOME_ENV_VAR": "Value used when running above command"
          }
        },
        "SomeOtherVariable": "some value"
      }
    }
  }
}

可以通过 shell 设置变量的值,代码块中的GDBServerVersion的值就是 shell 脚本/path/to/my/scripts/get-gdbserver-version的输出。

经我实验,自定义变量似乎不能够依赖自定义变量,也不可以在shell中使用预定义变量,可能之后的版本会实现这些功能。

也可以从用于输入中获取变量的值。只要 vimspector 发现了未定义的变量,就会在运行时提示用户输入变量值。Vimspector 支持 splat 运算符(不清楚中文叫什么),语法为*${Variable},可以将一个变量拓展开。

以上两个特性结合在一起可以实现很灵活的配置,最典型的运例子是程序参数的传递。Vimspector 调试的程序的参数以数组的形式传递,在配置文件中将args设置为一个在运行时用户输入的变量,就可以模拟命令行的效果。

xxxxxxxxxx
  "args": [ "*${CommandLineArgs}" ]

在运行时 vimspector 会要求用户输入值,如果用户输入123,args就会被拓展成["1", "2", "3"]

可以为变量提供默认值,${variableName:default value}。在${}中引用变量时,}要通过\转义,即将}写为\\}

xxxxxxxxxx
  {
    "configuration": {
      "program": "${script:${file\\}}"
    }
  }

program默认设置为变量file的值。

vimspector 介绍的的用户输入都字符串,有时会出现类型和用户期望的不同的情况。比如,用户为布尔类型的变量StopOnEntry输入true,vimspector 接收到字符串"true",并将它赋给变量,这样出现了类型不一致的情况。

xxxxxxxxxx
  {
    "configuration": {
      "stopAtEntry": "${StopOnEntry}"
    }
  }

在字段后添加#json可以将接收到的字符串转换成 json 里的类型。如果变量以#json结尾,需要在字段尾部添加#s以告知 Vimspector 这个变量以#json结尾,而不是进行类型转换。

xxxxxxxxxx
 {
    "configuration": {
      "stopAtEntry#json": "${StopOnEntry}"
    }
  }

这样,用户输入true,vimspector 接收到字符串"true",然后再将它解析为布尔类型的true

多配置共存

可以在 .vimspector.vim 中写多个配置,在启动 vimspector 时再选择使用的配置。这样的话,可以将所有自己需要的配置写入到一个文件,在创建项目时复制到项目中。

和配置选择有关的字段有两个:

autoselect: 布尔类型。在只有一个配置可用时,是否自动选择它。

default: 布尔类型。当启动时用户没有选择配置时,使用本配置。

可以在配置中提前打好断点,比如在程序入口点暂停(通常是main()),在抛出异常时暂停,在某函数暂停等等。

stopAtEntry: 布尔类型。是否在程序入口点暂停。

cpp_throw: 在抛出异常时暂停

cpp_catch: 在捕获异常时暂停

xxxxxxxxxx
{
    "example":{
        "stopAtEntry": true,
        "MIMode": "gdb",            // 使用 GDB
        "breakpointers": {
        "exception": {
             "cpp_throw": "Y",
             "cpp_catch": "N"
         }
        }
    }
}

Vimspector 暂时不支持在配置中在函数、代码行上打断点,但是可以通过底层的调试适配器,直接在调试器中执行命令,绕开这个限制。

Vimspector 的配置其实很简单,但是纸上谈兵有些难以理解。这里将给出几个可以实际使用的配置,如调试 Vim、调试 qemu 模拟器中的 OS 内核。

调试 Vim

vimspector 文档中给出的调试vim的配置:

xxxxxxxxxx
{
  "configurations": {
    "Vim - run a test": {                           // 配置名
      "adapter": "vscode-cpptools",                 // 使用的调试适配器
      "configuration": {                            // 具体的配置
        "type":    "cppdbg",                        // 调试器类型:cppdbg(GDB/LLDB) 或 cppvsdbg(VISUAL STUDIO)
        "request": "launch",                        // 调试类型:launch(启动程序) 或 attach(连接进程)
        "program": "${workspaceRoot}/src/vim",      // 带有调试信息的可执行文件目录
        "args": [                                   // 程序的参数,一个 json 数组
          "-f",
          "-u", "unix.vim",
          "-U", "NONE",
          "--noplugin",
          "--not-a-term",
          "-S", "runtest.vim",
          "${Test}.vim"                             // 未定义的变量,用户输入
        ],
        "cwd": "${workspaceRoot}/src/testdir",      // 当前工作目录
        "environment": [                            // 环境变量
          { "name": "VIMRUNTIME", "value": "${workspaceRoot}/runtime" }
        ],
        "externalConsole": true,                    // 是否使用外部终端
        "stopAtEntry": true,                        // 是否在程序入口点暂停
        "MIMode": "lldb",                           // 使用 LLDB 作为调试器
        "logging": {                                // 调试适配器的输出
          "engineLogging": false                    // 是否打印调试适配器产生的信息,默认不打印
        }
      }
    }
  }
}

调试 qemu-riscv64 中的 OS 内核

用以下命令启动 qemu 模拟器,让它监听localhost:1234等待 GDB 连接:

xxxxxxxxxx
 qemu-system-riscv64 \
       -machine virt \
       -s -S         \
       -nographic    \
       -bios default \
       -device loader,file=kernel.img,addr=0x80200000

因此,我们要配置 vimspector 让 RISCV 架构的 GDB 连接到localhost:1234

xxxxxxxxxx
{
    "configurations": {
        "qemu-riscv64-oslab": {
            "adapter": "vscode-cpptools",
            "variables": {
                "kernelBin": "kernel.bin,                                // 带有调试信息的内核可执行文件
                "riscvGDB": "/usr/local/bin/riscv64-unknown-elf-gdb"     // GDB 路径
            },
            "configuration": {
                "type":    "cppdbg",
                "request": "launch",
                "program": "${kernelBin}",
                "cwd": "${workspaceRoot}",
                "environment": [],
                "externalConsole": true,
                "stopAtEntry": true,
                "MIMode": "gdb",                                         // 使用 GDB
                "miDebuggerPath": "${riscvGDB}",                         // GDB 路径为 ${riscvGDB}
                "setupCommands": [                                       // 设置 GDB 初始化命令,相当于 gdbinit
                    {
                        "description": "Enable pretty-printing for gdb", // 描述,不会被 GDB 使用
                        "text": "set architecture riscv",                // 命令
                        "ignoreFailures": false                          // 是否忽略错误
                    },
                    {
                        "description": "Connect gdbserver within qemu",
                        "text": "target remote localhost:1234",
                        "ignoreFailures": false
                    }
                ]
            }
        }
    }
}

上面的配置依赖 vscode-cpptools,不支持其他的调试适配器。

我自己的配置

我将我可能使用的配置写入到一个文件,这样不需要重复编写,在启动 vimspector 选择即可。

xxxxxxxxxx
{
    "configurations": {
                                                                   // 上面介绍过了,不再赘述
        "qemu-riscv64-oslab": {
           // ...
        },
        "launch-current-file": {
            "adapter": "vscode-cpptools",
            "configuration": {
                "default": true,
                "type":    "cppdbg",
                "request": "launch",
                "program": "${fileDirname}/${fileBasenameNoExtension}",
                "args": ["*${ProgramArgs}"],                      // 用户输入
                "cwd": "${workspaceRoot}",
                "environment": [],
                "externalConsole": true,
                "stopAtEntry": true,
                "MIMode": "gdb",
                "breakpointers": {
                    "exception": {
                        "cpp_throw": "Y",                          // 抛出异常时暂停
                        "cpp_catch": "N"                           // 捕获时不暂停
                    }
                }
            }
        },
        "launch-current-project": {
            "adapter": "vscode-cpptools",
            "configuration": {
                "variables": {
                    "ProgramName": {
                        "shell": ["basename ", "${workspaceRoot}"] // 无法正确执行,需要用户输入
                    },
                    "ProgramPath": "${workspaceRoot}/_builds/${ProgramName}"
                },
                "type":    "cppdbg",
                "request": "launch",
                "program": "${workspaceRoot}/_builds/${ProgramName}",
                "args": ["*${ProgramArgs}"],
                "cwd": "${workspaceRoot}",
                "environment": [],
                "externalConsole": true,
                "stopAtEntry": true,
                "MIMode": "gdb",
                "breakpointers": {
                    "exception": {
                        "cpp_throw": "Y",
                        "cpp_catch": "N"
                    }
                }
            }
        },
        "attach-current-file": {
            "adapter": "vscode-cpptools",
            "configuration": {
                "type": "cppdbg",
                "request": "attach",
                "program": "${fileDirname}/${fileBasenameNoExtension}",
                "MIMode": "gdb",
                "breakpointers": {
                    "exception": {
                        "cpp_throw": "Y",
                        "cpp_catch": "N"
                    }
                }
            }
        },
        "attach-current-project": {
            "adapter": "vscode-cpptools",
            "configuration": {
                "variables": {
                    "ProgramName": {
                        "shell": ["basename", "${workspaceRoot}"]
                    },
                    "ProgramPath": "${workspaceRoot}/_builds/${ProgramName}"
                },
                "type": "cppdbg",
                "request": "attach",
                "program": "${ProgramPath}",
                "MIMode": "gdb",
                "breakpointers": {
                    "exception": {
                        "cpp_throw": "Y",
                        "cpp_catch": "N"
                    }
                }
            }
        }
    }
}

vimspector-screenshot

Vimspector 预设了vscode mode 和 human mode 两套键盘映射(快捷键)。

开启vscode mode:

xxxxxxxxxx
let g:vimspector_enable_mappings = 'VISUAL_STUDIO'

1954702-20200505175802855-63918029.png

开启 human mode:

xxxxxxxxxx
let g:vimspector_enable_mappings = 'HUMAN'

1954702-20200505175855111-809170906.png

这两个套快捷键都要用到 F11 和 F12,这两个按键往往会和终端快捷键冲突,如 F11 是最大化终端,F12 是弹出 guake 之类的下拉框终端,建议终端用户重新定义快捷键。

GDB 前端推荐(题外话)

我从韦易笑大佬(他写了不少有深度的文章和插件,对我产生了很大影响,推荐关注)的知乎回答如何在vim中可视化的调试c++程序?中了解到了 vimspector。当时对 json 等还很不了解,在配置 vimspector 时遇到很大困难,在网上没有查找到比较详细的中文文章,因此写了这篇博客,希望能够帮助到有需要的朋友。这篇文章发布半年多以来,vimspector 处于活跃的开发中,原文的许多内容已经过时,为了不误导读者,更新了文章。

最近几天,参考韦易笑的文章终端调试哪家强,尝试了几种 C/C++ 调试方案,得到以下结论:

裸 GDB ==> cgdb ==> Vimspector/VSCode ==> gdbgui

其中 Vimspector 和 VSCode 均使用 vscode-cpptools,个人认为在能力上没有太大区别。gdbgui是一个基于浏览器的 GDB 前端,能力应该是上述几种方案中最强的。gdbgui 有以下几个突出特性:

  • 不需要配置 gdbgui 不需要配置,只需要像直接使用 gdb 一样输入命令即可,如gdbgui -g 'gdb program -x gdbinit'
  • 兼顾 GUI 和命令行 在 gdbgui 中可以直接在 gdb 命令行中输入命令,并且 GUI 会响应 gdb 命令。比如在命令行中打了断点,GUI 会立刻显示出来。
  • 图形化显示数据结构 GDB 可以图形化显示链表和树

Vimspector 仍在实验阶段,部分重要特性还没有稳定,如果需要更加强大的调试功能,可以考虑 gdbgui。

vim

此博客中的热门博文

使用 Vim 搭建 C/C++ 开发环境

刚接触 Vim 的同学往往因为无法搭建开发环境而“从入门到放弃”,本文旨在帮助这些同学搭建开发环境,聚焦于最核心的开发需求,忽略换配色调字体之类的细枝末节。如果需要开箱即用的 vim 配置(发行版),可以使用 Spacevim 。 本文使用 neovim-nightly,但也适用于 Vim 8.2+,不需要读者有任何 VimL 基础,以 C/C++ 为例,但应该适用于任何语言。   插件管理 在 Vim 中,插件只是一些脚本,存放在特定的目录中,运行时将它们所在的目录加入到 runtimepath 中。Vim 8 内置了插件管理功能,但不支持高级的插件管理功能。Vimmers 实现了多个插件管理器,可以自动下载、更新、安装插件,还可以延迟加载、按需加载,提高启动速度。 上古时期流行手动或使用 Vundle 管理插件,以上两种方式已经落伍了,这里介绍目前比较流行的三个插件管理器: vim-plug :简单易用高效,是目前最流行的插件管理器 dein.vim :功能强大,但使用复杂 vim-pathogen :另一款流行的插件管理器,没有用过不做评价 以上三款插件管理器风格各不相同,都有大量用户,功能相当完善,根据自己的喜好选取即可。推荐新手选择 vim-plug,对启动时间特别敏感的同学可以考虑 dein.vim,我的配置在安装 70 余个插件的情况下,启动仅需 60 余秒。 使用 vim-plug 安装插件只需要在 .vimrc 中写入以下代码: call plug#begin('~/.vim/plugged') " 括号里面是插件目录                                 " 只能在 plug#begin() 和 plug#end() 之间写安装插件的命令 Plug 'junegunn/vim-easy-align'   " 用户名/插件名,默认从 github 下载安装 Plug 'https://github.com/junegunn/vim-github-dashboard.git' " 从特定 URL 下载安装 call plug#end() 使用 dein.vim 安装插件: ​ x set runtimepath+=

Ibex 架构介绍

  Ibex 是什么? Ibex was initially developed as part of the PULP platform under the name "Zero-riscy" , and has been contributed to lowRISC who maintains it and develops it further. It is under active development. Ibex 是一个产品级的 32 位开源 RISC-V 处理器,使用 SystemVerilog 编写,麻雀虽小(11000 行左右),五章俱全。支持 RV32I、RV32C、RV32M、RV32B 等拓展,支持了 M-Mode 和 U-Mode,完整实现了 RISC-V 指令集规定的控制状态寄存器、中断异常、调试支持等,适用于嵌入式系统。 总体架构如下: 流水线 Ibex 默认使用两级流水线,但也支持三级流水线(实验性特性)。两级流水分别为: 取值(IF):通过预取缓冲区(prefetch buffer)从内存中取值,可以一个周期取一条指令,只要指令侧内存支持。 译码/执行(ID/EX):译码并立即执行,所有的操作,包括寄存器读写、内存访问都在该阶段进行。 Ibex 支持多周期指令,每条指令都至少需要两个周期才能通过流水线,周期数更大的指令将导致流水线停顿多个周期。指令类型及其停顿周期如下: Instruction Type Stall Cycles Description Integer Computational 0 Integer Computational Instructions are defined in the RISCV-V RV32I Base Integer Instruction Set. CSR Access 0 CSR Access Instruction are defined in ‘Zicsr’ of the RISC-V specification. Load/Store 1 - N Both loads and stores stall for at least one cycle to await a response. For loads this response is t

UNIX 进程关系

进程的起源 BSD 终端登录 网络登录 进程与进程组 会话与控制终端 作业控制 孤儿进程和孤儿进程组 总结 UNIX是分时系统,同时运行着多个进程,进程之间相互联系,形成了进程组、会话等进程关系,这些进程关系会影响某些函数/系统调用和信号的行为。 进程的起源 所有的进程都有一共同的起源,加电开机启动操作系统并登录(获取 login shell )就是用户进程的起始 1 。这里介绍传统的UNIX登录机制。 UNIX登录的过程一般分为两种: 终端登录( terminal login ) 网络登录( network login ) 终端登录就是在本地计算机中启动操作系统并取得 login shell,比如没有安装桌面环境(KDE、GNOME等)的 GNU/Linux 系统,启动后就是命令行,要求输入用户名和密码,验证通过后就会取得 login shell。 网络登录就是通过网络登录远程计算机取得 login shell,比如腾讯云、阿里云的 Linux 服务器,登录后就是命令行黑框框,就好像是在本地登录的一样。 BSD 终端登录 启动 init 进程,init 进程可能会读取终端相关的配置文件,如 /etc/ttys 等。 为每个终端 fork 出一个子进程,并使用 exec() 函数执行 getty() 例程。 getty() 例程打开终端设备(标准输出、标准输入、标准错误)、读取用户名、初始化环境变量,并使用 exec() 系列函数执行login 例程。 login 例程接收密码,如果验证成功就修改工作目录到家目录、修改终端所有权、设置用户信息(UID、GID等)、初始化环境变量(SHELL、PATH等)后执行 login shell。 如果验证失败,终止进程,init 进程再次执行步骤2。 通过以上步骤,用户就取得了 login shell,并且 login shell 是 init 进程的子进程(exec 系列函数只执行程序不改变进程ID)。login shell 也会读取它的配置文件来初始化自身。 BSD 的终端登录方式在 UNIX 世界有巨大的影响,被许多 UNIX 实现采用,比如 Linux 早期版本(Linux 0.12)就是用类似 BSD 的登录方式。现在终端登录复杂了很多,许多系统装有 X window,登录后是图形界面而不是字符界面。G

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK