UCAS OSLab Guide: Setting up the Development Environment
/操作系统研讨课可以说是国科大计算机系最硬核的几门课之一了,选修这门课的同学需要从0开始构建自己的操作系统内核,无论是从设计还是调试角度,难度都是相当大。当然,从硬核的课程中,自然也能学到非常多硬核的知识,修完这门课,个人的编程能力、系统能力都会有相当大的提升。
我是在2022年秋季选修的这门课,当时老师给的开发环境是RISC-V工具链,调试环境为课题组魔改的qemu+gdb. 使用gdb命令行来调试内核虽然可行,但是仍然显得比较繁琐,而且可视化程度比较低。唯一可用的可视化工具tui
,在教学团队提供的gdb中也被禁用了,所以不得不想一些其他的办法来降低调试难度,从而提高开发效率。
我自己是非常熟悉vscode这个编辑器的,所以当时在写代码之前,我就把开发环境搭建好了,之后写代码、调试,效率都非常高,正所谓“磨刀不误砍柴工”。因为大三上课业任务非常繁重,我只把我的这些办法传授给了身边的几个同学,效果还是非常明显的,他们的开发效率也得到了大幅度的提升。在保研结束后,时间相对来说比较充裕,我跟这门课的主讲老师蒋老师交流了这个想法,蒋老师表示非常赞成,并且在课堂上会把我的方法分享给学弟学妹,所以也就有了这篇文章。
1. 准备
这门课的教学团队会给每一个人一个Ubuntu虚拟机,这个虚拟机是没有图形界面的,但是对外暴露了ssh端口,同学们应该都是通过ssh远程连接到虚拟机,然后进行开发。我想几乎所有同学都会选择vscode作为编辑器,所以我在这里推荐几款插件,来提高同学们的开发效率:
C/C++
微软官方的C/C++插件,提供调试、代码补全、错误提示等功能。
个人感觉微软官方的这个插件,除了调试之外的功能,都不算太好用。比如错误提示经常会抽风,写着写着分号忘记了,有时候这个插件也不会提示,最后编译的时候报错才开始找自己是哪里哪里写错了,非常费劲。代码检查能力也不太行,很多写的不好的地方也不会及时给出警告。
Clangd
这个插件可以提供代码补全、代码检查、错误提示等功能,性能和使用体验远远超过微软官方插件。首先这个插件的响应速度非常快,其次对代码有着更严格的检查,能让你写出更规范的代码。
上面这两个插件已经完全够用,我再根据我自己的使用习惯推荐几款插件:
Tabout
这个插件功能非常简单,只要使用tab键就能跳出右括号、右引号等,就不需要去按方向键一点一点的移动了。对我个人来说是非常方便。
MetaJump
这个插件可以在几次按键内让你的光标跳转到编辑器当前页面的任何位置,类似于vim。我个人不熟悉vim,此处就不在各位同学面前班门弄斧了。
如果同学们想要使用clangd这个插件,那么需要首先在虚拟机中安装clangd:
1 | sudo apt-get install clangd |
在安装clangd和clangd插件之后,vscode会提示clangd插件的代码提示功能与C/C++插件的代码提示功能冲突,这时候可以选择关闭C/C++插件的代码提示功能,之后clangd就能够正常工作了。
2. 配置
通常来说,用vscode打开项目所在的文件夹,会看到非常多红色和黄色的波浪线。这是因为提供代码检查功能的插件无法识别变量名、类型名等词法符号。下面的内容将要介绍如何配置vscode,让插件正确解析在头文件中声明的变量、类型或者函数等,这样会方便我们跳转到词法符号对应的定义或者声明处,从而更快地上手项目。
正确解析这些符号依赖的是编译数据库compile_commands.json
。如果项目是用CMake构建的,那么只需要在使用CMake构建项目时,添加选项-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
即可。但是我们的OSLab并不是使用CMake构建的,而是用的Makefile,那么该如何导出compile_commands.json呢?其实也比较简单,这会用到一个小工具,叫bear
。
首先需要安装bear
:
1 | sudo apt-get install bear |
在使用bear
之前,首先使用make clean
清空此前已经编译好的文件,此后执行命令
1 | bear make all |
执行完成之后,就可以在当前目录中看到compile_commands.json
这个文件了,该文件的内容大概如下:
compile_commands.json
1 | [ |
实质上就是描述了该文件是如何编译的,所以可以称它为“编译数据库”。
在得到该文件之后,Clangd会检测到该文件,并询问是否设置相关选项,这边可以设置,之后就不要再更改compile_commands.json
文件的位置了。我个人通常倾向于将该文件放到.vscode
文件夹里。
如果你使用的是Clangd插件,那么配置编译数据库非常简单,只需要设置Clangd插件的选项。在已经安装的插件列表里面找到Clangd插件之后,右击右边的小齿轮,转到Extension Settings
,找到Arguments
选项,添加一项--compile-commands-dir=${workspaceFolder}/.vscode
即可。
关于Clangd的Arguments,大家可以根据官方文档或者网上的其他博客来设置。我建议同学们再添加一个选项--header-insertion=never
,如果不添加这个选项,那么Clangd就会为你提示一些C标准库中的函数或者变量,但是注意我们的内核项目是无法使用C标准库的,因此这样会对开发造成一些障碍。
Clangd目前似乎还不支持Headerfile Completion,有兴趣的同学可以到Clangd的issue下面寻找相关答案,因为我个人用Clangd的时间还不是太长,有些东西还不太熟悉。目前可能的解决方案是这个。如果有同学弄明白了Clangd的这些机制,欢迎来找我交流,我会及时更新我的博客。
在正确配置Clangd之后,你会在.vscode
文件夹下面发现.clangd
文件夹,这里面存放的都是索引文件。
如果你不使用Clangd插件,其实也是可以的(我去年就是这么干的),这样配置C/C++插件起来会稍微麻烦一些。首先在开发目录下创建.vscode
文件夹,之后创建c_cpp_properties.json
文件,该文件内容如下:
c_cpp_properties.json
1 | { |
这样同样能够实现正确的代码检查和跳转,前提是compile_commands.json
文件的位置在.vscode
目录下。
虽然我上面提到的这几个json配置文件看起来都非常复杂,但其实还是有迹可循的,从每一个选项的名字,你就能大概猜出来这个选项是什么意思,如果你要修改改选项的话,可以参考微软的官方文档来进行修改。
比如上面c_cpp_properties.json
文件中的"compile_commands"
选项,很容易猜出这个选项是用来指定编译数据库的所在位置的。${workspaceFolder}
表示当前vscode打开的文件夹的路径。
3. 调试
单纯的使用gdb命令行调试效率相对来说比较低,特别是调试文件数量巨大的项目时。我们可以使用C/C++插件来实现调试可视化,从而大幅度提高开发效率。
3.1 配置launch.json
launch.json
是项目运行的配置文件,使用vscode运行或者调试项目,就必须配置好该文件。
首先在.vscode
文件夹里创建空白的launch.json
文件。打开该文件,会看到编辑器右下方出现了Add Configuration
字样的蓝色按钮。单击后,选择"C/C++: (gdb) Launch",vscode就会在launch.json
填充默认的配置,内容如下:
launch.json
1 | { |
关于配置的各个字段,在vscode官方文档中都有详细的介绍,这里我只介绍比较重要的几个字段:
program
:你要调试的可执行文件。通常情况下,我们都需要调试内核,所以这个地方填写编译出来的内核可执行文件的路径即可,比如我的路径为"${workspaceFolder}/build/main"
stopAtEntry
:如果设置为true,那么程序会在main
函数执行前暂停cwd
:current working directory,当前工作目录。这个就相当于你在bash中启动程序,当前bash所在的目录。如果cwd
这个选项出了问题,那么可能程序在运行时执行的某些代码就会报错,比如在当前目录打开文件等操作。environment
:传给可执行程序的环境变量args
:传给可执行程序的命令行参数
我们这次的开发环境相对来说比较特殊,用到了qemu提供的调试功能,所以需要加一些远程调试的选项。下面是我个人的launch.json
的部分内容:
launch.json
1 | { |
其中miDebuggerServerAddress
就是qemu暴露的端口地址,教学团队提供的是127.0.0.1:1234
;miDebuggerPath
就是gdb所在的路径。
关于postRemoteConnectCommands
和preLaunchTask
这两个字段,之后再进行解释。
在配置好launch.json
之后,单击vscode的调试按钮,或者按F5(在未关闭功能键的笔记本电脑上,需要按Fn+F5)直接开启调试了,在调试的时候,可以在行号左边单击设置断点,程序在运行时就可以停在断点处,此时就可以查看程序的运行状态,如局部变量、全局变量、寄存器状态等。
3.2 配置tasks.json
在调试的时候,我们通常会有这样的一种需求:调试发现问题之后,修改源代码,然后再进行调试。在第二次调试之前必须要重新编译,否则调试功能无法正常使用。这个时候你就要打开终端,输入make all
,等待程序重新编译,再切换到vscode,开启调试,会显得非常繁琐。
南京大学的蒋炎岩老师在他的操作系统课上讲到过一句话,我印象非常深,大概意思就是任何繁琐且机械的操作,我们都可以交给程序来做。这里同样也是如此,在调试之前总要重新进行编译,这个过程就可以交给程序来做,而preLaunchTask
这个字段就是用来解决这个问题的。
首先需要配置tasks.json
。在.vscode
文件夹中添加空白文件tasks.json
。我个人的tasks.json
的内容如下:
1 | { |
可以看到,tasks
这个数组中有两个对象(此处涉及到json格式以及javascript的一些内容,有不懂或者感兴趣的地方可以自行查询相关资料),分别代表了两个任务,label
字段为任务的名称。
每一个任务都相当于在命令行执行了一条命令:
command
就是使用的可执行文件,比如gcc
,bash
args
就是传递给这些可执行命令的参数cwd
代表该命令执行时所在的目录。
在配置好tasks.json
之后,如何在调试之前运行任务呢?只需要将launch.json
中preLaunchTask
字段设置为对应的任务的名字即可。
3.3 调试过程
在配置好上面提到的配置文件之后,我们就可以开心的开始调试了,调试流程也变得非常简单。
在vscode的内置终端中输入make debug
(或者其他命令,视情况而定):
可以看到程序陷入了等待状态,此时我们单击调试按钮或者按F5,之后会自动跳转到DEBUG CONSOLE窗口,这时回到TERMINAL,可以看到如下情形:
在按任意键之后,可以看到执行任务的终端被关闭,回到了刚才输入make debug
的窗口,此时原来被阻塞的程序开始运行:
输入loadboot后,内核开始运行,会在你打断点的地方停住:
这样我们就成功的实现调试可视化了,左边栏分别是局部变量、监视变量、函数调用栈以及断点,右边就是我们的源代码,下面是qemu进程,可以实时监视它的输出。不仅如此,单步执行、转到函数定义、终止调试都很方便。
3.4 使用gdb其他命令
配置好vscode之后,单步调试就不用我们一直在命令行里面输next了,非常的方便,但在有些情况下,还是会用到gdb的命令的,比如使用x
命令查看某块内存的值,或者是使用wa
命令监视变量的值,而这些命令vscode C/C++插件都没有可视化的支持。那么如何在vscode中使用这些命令呢?
在启动调试之后,转到DEBUG CONSOLE一栏:
在图中,vscode插件给出了相应的提示:
Execute debugger commands using "-exec
", for example "-exec info registers" will list registers in use (when GDB is the debugger)
也就是说,如果你需要用到上述gdb命令的话,只需要在对话框中输入相应的命令,并在命令之前添加-exec
前缀即可:
3.5 gdb脚本
通常同学们在调试内核的时候,会有这样一个需求:每次在调试的时候,都要输入一些命令,来设置gdb的一些选项,比如我每次都会输入下面的命令:
1 | set confirm off |
每次开启调试,都需要在命令行里面输入这些命令,而要想找到一个bug,需要调试非常多次,如果每次调试都需要我们手动去输入这些命令的话,会显得非常繁琐。
正如我前面提到的那样,任何繁琐且机械的操作,我们都可以交给程序来做。这里同样有办法来解决这个问题,就是之前我提到过的那个launch.json
里面的那个字段:postRemoteConnectCommands
。可以大致猜出来这个选项的意思是在连接成功后,执行一些命令,这样的话我们就可以利用这个机制,执行gdb命令。
首先创建一个gdb脚本文件.gdbinit
,在这里面输入你需要执行的命令,之后设置launch.json
中的对应字段:
launch.json
1 | { |
3.6 调试汇编
同学们在调试内核的时候经常会需要调试汇编,这样就来了问题了:之前的配置,都只是调试C源文件,汇编文件怎么去调试呢?或者我想调试C语言编译出来的汇编,又该怎么办呢?别着急,让我一一为同学们解答。
当你用gdb命令行调试汇编的时候,最通用的办法就是使用gdb自带的tui。在gdb命令行中输入layout asm
即可出现汇编语言的调试界面,很不幸的是,教学团队给的gdb中不含tui,那又该怎么办呢?
如果你坚持使用gdb命令行调试汇编,那么可以使用这样一条命令:x/10i $pc
,表示打印当前PC寄存器指向的指令,并且打印该指令后面的9条指令。这样的话,也是可以的。
但是如何用vscode来调试汇编呢?其实在编译的时候,汇编源文件的调试信息已经被编译到了kernel可执行文件中,所以当你在C源文件中step into到一个汇编函数时,会直接跳转到相应的汇编文件中,非常的方便。但是这个方式有一个致命的缺点:不能给在汇编文件上打断点。你用鼠标单击汇编文件的行号左侧,可以发现是打不上断点的。那还有其他的办法来给一条汇编指令打断点吗?
幸运的是,C/C++ 插件提供了这样的一个支持。转到侧边栏左侧CALL STACK一栏,找到当前所在的函数,右击:
单击Open Disassembly View,可以看到如下界面:
这样当前执行的汇编一目了然,也可以在某一条指令处设置断点,非常的方便。
3.7 调试用户态测试程序
在很多情况下,我们需要调试教学团队给出的测试文件,但是上面所有的配置都是关于调试内核的。这个问题解决起来用到这样两个命令:file
和add-symbol-file
。
当你想调试用户态测试文件的时候,使用这两个命令,并在参数中指定要调试的可执行文件的目录即可。这两个命令有一些微小的区别:
- file命令是切换调试的文件
- add-symbol-file 只是让gdb多解析了一些符号,并不会切换调试的文件
上面这两个命令我没有具体了解过,有错误的话欢迎同学们斧正。使用这两个命令之后,就可以正常调试测试程序了:
4. 后记
这篇文章并不是一个保姆级别的教程,只是提供了一个大致的思路,供同学们去思考和实践。对vscode不熟悉的同学,会在配置的过程中踩非常多的坑,遇到非常多的麻烦。对此我想说的是:
- 这是一个非常锻炼人的一个过程,也是一个非常好的一个提高自己动手能力与信息检索能力的机会
我不是保姆,我不会回答每一个同学的所有问题,我只会回答其中有价值的部分。实际上,你使用vscode遇到的问题,在你之前已经有很多人遇到了,他们也会写博客总结,你只需要通过搜索引擎来获取他们的经验,来解决自己的问题
- 在提问之前,首先要学会如何问问题,这里前人总结出了一篇文章提问的智慧,供同学们参考。有一个比较重要的点是:如果搜索引擎能够回答你的问题,最好不要去麻烦别人
欢迎大家的提问与交流,我的邮箱是anwentao1@gmail.com;我会根据同学们的问题不断更新这篇博客,同时欢迎同学分享你的经验,如果你的经验对同学们非常有价值,我会更新这篇博客,并在相应的地方标注你的名字和联系方式
最后,祝同学们学习愉快!^(* ̄(oo) ̄)^