VSCode 调试 RISC-V 程序

前提

本文主要涉及 VSCode 的相关配置,编译及调试工具需要提前安装好。

  • 已经安装好riscv-toolchain,包括riscv64-unknown-elf-gccriscv64-unknown-elf-gdb
  • 已经安装好qemu,包括riscv32-softmmu,riscv32-linux-user,riscv64-softmmu,riscv64-linux-user
  • 已经安装好g++,gdb

调试流程简介

对于我这样的新手,要调试一个项目源码最怕的就是开始,也就是怎么能把项目跑起来。

我们以一个简单的test项目,看看在 VSCode 里怎么跑起来。

拿到源码后,将其以文件夹形式,加入到 VSCode 中,文件 - 打开文件夹 - 选择 test 项目文件夹。项目就会在 VSCode 中打开,但是此时我们还无法编译运行,我们需要在 VSCode 上
构建出一个 C 语言的编译与调试环境。

首先得安装一个插件C/C++,打开插件中心Ctrl+Shit+X,搜索,安装。

然后输入F5,会弹出对话框,选择C++(GDB),继续选择g++。VSCode 会自动创建.vscode文件夹,已经两个文件launch.jsontasks.json

launch.json用来配置调试环境,tasks.json主要用来配置编译环境,当然也可以配置其他任务。task.json里配置的每个任务其实就相当于多开一个控制台。

配置tasks.json

因为我们先要编译源码,生成.out或者.exe文件,才能调试,所以先进行编译任务配置。

自动生成的文件是个配置模板,我们可以根据自己的实际情况进行配置,也有一部分可以保持默认。

// tasks.json
{
    // https://code.visualstudio.com/docs/editor/tasks
    "version": "2.0.0",
    "tasks": [
        {
             // 任务的名字,注意是大小写区分的
             //会在launch中调用这个名字
            "label": "C/C++: g++ build active file", 
             // 任务执行的是shell
            "type": "shell", 
             // 命令是g++
            "command": "g++", 
             //g++ 后面带的参数
            "args": [
                "'-Wall'",
                "-g",           // 生成调试信息,否则无法进入断点
                "'-std=c++17'",     //使用c++17标准编译
                "'${file}'",        //当前文件名
                "-o",               //对象名,不进行编译优化
                "'${fileBasenameNoExtension}.exe'",  //当前文件名(去掉扩展名)
            ],
        }
    ]
}

如果项目是通过 Makefile 编译的,那就更加简单,只需要配置一个任务即可。

{
  "version": "2.0.0",
  "tasks": [
    {
       //任务的名字方便执行
      "label": "Make Project",
      "type": "shell",
      "command": "make",
      "args":[
          //8线程编译
          "-j8",
      ],
    },
  ]
}

运行该任务时就会执行make命令进行编译。

配置launch.json

// launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            //调试任务的名字
            "name": "g++ - Build and debug active file", 
            //在launch之前运行的任务名,这个名字一定要跟tasks.json中的任务名字大小写一致
            "preLaunchTask": "C/C++: g++ build active file",  
            "type": "cppdbg",
            "request": "launch",
            //需要运行的是当前打开文件的目录中,
            //名字和当前文件相同,但扩展名为exe的程序
            "program": "${fileDirname}/${fileBasenameNoExtension}.exe", 
            "args": [],
            // 选为true则会在打开控制台后停滞,暂时不执行程序
            "stopAtEntry": false,
            // 当前工作路径:当前文件所在的工作空间
            "cwd": "${workspaceFolder}",
            "environment": [],
            // 是否使用外部控制台
            "externalConsole": false,  
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ]
        }]
}

运行

经过以上配置后,我们打开main.cpp文件,在cout处打一个断点,按F5,即可编译,运行,调试。一定要打开main.cpp文件,不能随便打开文件就开始哦。因为我们在配置时使用了一些预定义,比如${file}表示当前文件,所以只有打开需要调试的文件才能开始。

程序将会在cout语句停下来。

我们可以注意一下界面下方的控制台,可以更直观了解launch.jasontasks.jason

右边的框,就是我们在tasks.jason中配置的任务,左边的框就是我们在tasks.jasoncommand以及args的内容,他就是帮我们提前写好编译的选项。然后在 shell 中运行。

编译调试 RISC-V 程序

了解以上这些,就可以按需配置所需的环境了。我们还是从tasks.jason开始。因为开发用的电脑是x86的,所以先要编译出riscv的程序,再用模拟器模拟出rsicv的环境,然后在模拟的环境中运行程序,最后才能开始调试。

假设已经安装好开头所提到的工具。首先配置tasks.jason

{
    "version": "2.0.0",
    "tasks": [
        {
            // 编译当前代码
            "type": "shell",
            "label": "C/C++(RISCV): Build active file",
            // 编译器的位置
            "command": "/opt/riscv/bin/riscv64-unknown-elf-g++",
            "args": [
                "-Wall", // 开启所有警告
                "-g", // 生成调试信息s
                "${file}",
                "-o",
                "${workspaceFolder}/debug/${fileBasenameNoExtension}" // 我选择将可执行文件放在debug目录下
            ],
            // 当前工作路径:执行当前命令时所在的路径
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ]
        },
        {
            // 启动qemu供调试器连接
            "type": "shell",
            "label": "Run Qemu Server(RISCV)",
            "dependsOn": "C/C++(RISCV): Build active file",
            "command": "qemu-system-riscv64",
            "args": [
                "-g",
                "65500", // gdb端口,自己定义
                "${workspaceFolder}/debug/${fileBasenameNoExtension}"
            ],
        },
        {
            // 有时候qemu有可能没法退出,故编写一个任务用于强行结束qemu进程
            "type": "shell",
            "label": "Kill Qemu Server(RISCV)",
            "command": "ps -C qemu-riscv64 --no-headers | cut -d \\  -f 1 | xargs kill -9",
        }
    ]
}

tasks.jason是可以配置多个任务的,第一个任务用来编译成riscv架构下的程序,第二个任务用来启动 qemu,让程序在 qemu 上运行起来。

第一个任务中,command就是配置编译器riscv64-unkonown-elf-gcc的属性,第二个任务中,command是配置 qemu 模拟器qemu-system-riscv32的属性。第三个任务中,用来配置结束 qemu 模拟器的命令。

接下来配置launch.jason

{
    "version": "0.2.0",
    "configurations": [
        { 
            "name": "C/C++(RISCV) - Debug Active File",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/debug/${fileBasenameNoExtension}",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            // RISC-V工具链中的gdb
            "miDebuggerPath": "/opt/riscv/bin/riscv64-unknown-elf-gdb", 
            // 这里需要与task.json中定义的端口一致
            "miDebuggerServerAddress": "localhost:65500" 
        }
    ]
}

我们在配置x86下的调试环境时,launch.jason中有个"preLaunchTask": "C/C++: g++ build active file",属性,这个属性的目的是在启动调试之前,先执行任务名字为"C/C++: g++ build active file"任务,也是就编译的任务。

因为启动 qemu 会导致阻塞,所以这里没有加preLaunchTask,在启动调试之前,先把 qemu 运行起来。输入Ctrl+Shift+P,打开 VSCode 命令行。输入Run Task

点击第一个,选择任务,我们可以看到出现的三个任务就是我们在tasks.jason中配置的三个任务。选择第一个 Build,编译出程序,再重复操作,选择第三个执行 QEMU 任务。

预定义变量

官网