手把手教你使用VSCode进行linux内核代码阅读和开发

现状

Linux内核由于其本身代码量庞大,其中又包含各种平台宏定义开关/配置,外加各种结构体指针的注册,这使得阅读内核代码变成一件令人头疼的事。针对这个问题目前常见的解决方案有以下几种:

  1. 使用简单的文本浏览工具 + grep进行代码搜索浏览,这种方法最简单,效率也最低。
  2. 使用source insight进行代码浏览,使用这种方法的人应该很多,但是在浏览内核代码的时候有个缺点就是内核下有多个平台的头文件、源码,如果不做排除的话在代码跳转的时候往往能找到很多个同名函数或变量的定义,还得一个一个去确认,非常麻烦。虽然可以手动排除目录但仍然比较麻烦,之前好像看到网上有大神弄了一个脚本还是插件啥的可以根据内核中编译出的.o自动排除未编译的文件,这种相对方便点,但是头文件可能还是得手动排除。另外最重要的是source insight是收费软件,后面不用我说,相信大家心里有数┐( ̄ヘ ̄)┌。
  3. 使用vim + ctags,这种方法就比较高大上了,比较适合大神使用,咱vim都没玩的很透的人一般用不了。
  4. 使用VSCode加上C++ Intellisense插件或者global插件(见文章末尾参考链接2、3、4),类似source insight,需要手动排除未编译文件减小索引范围,代码定位不准。

新的方法

本文要介绍的是使用VSCode + Remote SSH + clangd插件来阅读linux内核代码。其中VSCode作为最强大的代码开发工具之一,主要负责主体框架及界面展示。Remote SSH插件用于访问远程服务器,实现远程代码本地化访问,也具有与远程服务器shell交互的终端功能,可以替代常用的putty、xshell、securecrt之类的工具(虽然功能上略弱)。clangd插件用于代码语义分析、代码补全、跳转等。该方案克服了上面列举的几种方案的几乎各种缺点,能做到代码精准跳转、精准自动补全,其他默认一些如代码着色自动缩进之类的VSCode自带。另外最重要的是这些都是免费的!

除VSCode IDE以外,该方法的核心是clangd插件,clangd默认通过读取工程编译自动生成的compile_commands.json文件来索引其中包含的源文件和关联的头文件,因此能避免索引非编译的代码造成解析时语义混乱。下面展示一段compile_commands.json文件中的大致内容:

[
    {
        "arguments": [
            "cc",
            "-c",
            "-Wp,-MD,net/netfilter/.nf_conntrack_proto_udp.o.d",
            "-nostdinc",
            "-isystem",
            "/home/work/my-kernel/prebuilts/gcc-x64/arm-cortexa9-linux-gnueabihf-4.9.3/bin/../lib/gcc/arm-cortexa9-linux-gnueabihf/4.9.3/include",
            "-I./arch/arm/include",
            "-Iarch/arm/include/generated/uapi",
            "-Iarch/arm/include/generated",
            "-Iinclude",
            "-I./arch/arm/include/uapi",
            "-Iarch/arm/include/generated/uapi",
            "-I./include/uapi",
            "-Iinclude/generated/uapi",
            "-include",
            "./include/linux/kconfig.h",
            "-D__KERNEL__",
            "-mlittle-endian",
            "-Wall",
            "-Wundef",
            "-Wstrict-prototypes",
            "-Wno-trigraphs",
            "-fno-strict-aliasing",
            "-fno-common",
            "-Werror-implicit-function-declaration",
            "-Wno-format-security",
            "-std=gnu89",
            "-fno-PIE",
            "-fno-dwarf2-cfi-asm",
            "-fno-ipa-sra",
            "-mabi=aapcs-linux",
            "-mno-thumb-interwork",
            "-mfpu=vfp",
            "-funwind-tables",
            "-marm",
            "-D__LINUX_ARM_ARCH__=7",
            "-march=armv7-a",
            "-msoft-float",
            "-Uarm",
            "-fno-delete-null-pointer-checks",
            "-Wno-maybe-uninitialized",
            "-O2",
            "--param=allow-store-data-races=0",
            "-DCC_HAVE_ASM_GOTO",
            "-Wframe-larger-than=1024",
            "-fstack-protector",
            "-Wno-unused-but-set-variable",
            "-fomit-frame-pointer",
            "-fno-var-tracking-assignments",
            "-Wdeclaration-after-statement",
            "-Wno-pointer-sign",
            "-fno-strict-overflow",
            "-fno-merge-all-constants",
            "-fmerge-constants",
            "-fno-stack-check",
            "-fconserve-stack",
            "-Werror=implicit-int",
            "-Werror=strict-prototypes",
            "-Werror=date-time",
            "-DMODULE",
            "-DKBUILD_STR(s)=#s",
            "-DKBUILD_BASENAME=KBUILD_STR(nf_conntrack_proto_udp)",
            "-DKBUILD_MODNAME=KBUILD_STR(nf_conntrack)",
            "-o",
            "net/netfilter/nf_conntrack_proto_udp.o",
            "net/netfilter/nf_conntrack_proto_udp.c"
        ],
        "directory": "/home/work/my-kernel/linux",
        "file": "net/netfilter/nf_conntrack_proto_udp.c"
    },
    {
        "arguments": [
			......
		],
        "directory": "/home/work/my-kernel/linux",
        "file": "......"
	},
	......
]

该文件就是由每个源文件的编译参数、路径等信息组成的一个json文件,clangd通过这个文件可以准确定位源文件需要引用的头文件从而准确的找到各种宏定义、函数、变量声明的准确值了。

【文章福利】小编在群文件上传了一些个人觉得比较好得学习书籍、视频资料,有需要的可以进群【977878001】领取!!!额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)

手把手教你使用VSCode进行linux内核代码阅读和开发_第1张图片

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

那么问题来了,编译内核的时候也没有生成compile_commands.json这个文件呀。这个文件在编译cmake工程的时候可以自动生成,但是内核用的是make。这时候就需要另一个工具bear了,他就是专门用来生成这个的。ubuntu下直接sudo apt install bear就好了,如果不能用该命令安装的话也可以去github上搜索bear项目,找到源码自己下载编译安装。生成json文件的命令也很简单,只需要在你平时编译内核的命令前面加上bear就行了,比如:

bear make ARCH=arm CROSS_COMPILE=arm-linux-gnu- zImage

编译完成后在当前目录就会生成compile_commands.json这个文件。由于bear只是记录每个文件的编译命令,不会对编译速度有多少影响。

手把手教你使用VSCode进行linux内核代码阅读和开发_第2张图片

安装与配置

1.安装VSCode

去微软官网下载安装即可,官网嫌慢去国内软件下载站下载也行,注意不要下载到垃圾软件就行。

2.安装Remote - SSH插件

在VSCode Extension组件页搜索Remote - SSH插件安装,完成后建议重启一下VSCode(可能会自动提示需要重启)。

3.添加远程主机

由于我们一般嵌入式开发还是离不开windos,开发环境一般是windows主机用于办公+开发,然后要么虚拟机,要么单独的linux编译服务器用于编译linux系统,因此这一步就是用Remote SSH连接远程服务器,实现远程代码本地化访问。按F1键打开命令输入栏,输入Remote-SSH: Connect to Host…(不用全输,下面出现对应项点击就可以了)。在连接窗口输入要连接的远程主机(前面加上“用户名@”,如:[email protected]),也就是你编译linux内核的主机。连接上后可能会弹窗提示你是否记住该主机,点同意就行,然后输入登录用户的密码,登录成功后VSCode左下角状态栏会显示登录的主机状态,这个时候你就可以通过右下输出窗口旁边的TERMINAL标签页跟远程主机交互了(暂时还不用这个)。

手把手教你使用VSCode进行linux内核代码阅读和开发_第3张图片

4.安装clangd

在VSCode Extension组件页搜索clangd,在插件介绍界面点击安装即可。需要注意的是clangd有两个安装选项,一个是安装到本地,也就是windows系统上,也可以选择安装到远程服务器上,比如页面显示可以看到有个"Install in SSH: 192.168.50.170"安装选项,这两个都需要安装。linux远程服务器上的clangd默认是安装到~/.vscode-server/目录下。VSCode在安装linux版本的clangd时是在github上下载安装包然后通过ssh导入到服务器上,正常途径访问不了github的同学这一步可能会超时安装失败,可以通过其他途径到clangd的 github发布页 按平台下载安装包,安装包在linux系统上解压出来,然后手动拷贝到对应系统目录即可。如果没有系统权限,参考vscode默认方式安装到自己的home目录也可以(可能需要自己导出路径到环境变量)。需要注意的是如果VSCode之前安装过C++ Intellisense插件需要禁用或者卸载掉,因为会和clangd插件有冲突。

手把手教你使用VSCode进行linux内核代码阅读和开发_第4张图片

5.在远程服务器安装bear

前面已经说过了,很简单,sudo apt install bear即可(可以直接在第3步的terminal下操作)。

6.配置clangd

在已安装的Extension组件页选中clangd,点击图标旁边的齿轮打开设置页,User和Remote标签页中的Clangd Arguments都按照下面设置(点击Add Item,一个item输入下面的一行)

--compile-commands-dir=${workspaceFolder}
--background-index
--completion-style=detailed
--header-insertion=never
-log=info

设置完后关掉设置页面即可,vs会自动保存。

手把手教你使用VSCode进行linux内核代码阅读和开发_第5张图片

7.触发clangd工作

在上面都设置完后,我们回到第3步,由于Remote SSH已经连接到远程服务器,这个时候我们点击主菜单“File” -> "Open Folder"会自动浏览远程服务器上的路径(如果没有连接remote ssh就直接点击Open Folder的话只能浏览本地路径)。浏览到内核源码根目录,也就是compile_commands.json所在的目录打开,这个目录就作为我们的worksapce了。如果执行make的目录不是工程源码的根目录,而是在某个子目录下,就不能以这个目录作为VS的workspace了,不然无法浏览上层目录的代码,这个时候可以在工程源码根目录创建一个软链接指向子目录中的compile_commands.json,然后以源码根目录作为workspace。然后在左边文件列表里双击任何一个.c内核源文件(需要是参与编译的),这个时候你就可以看到最下面状态栏clangd开始执行indexing了,也就是解析workspace下compile_commands.json文件里描述的所有源文件,创建索引数据库(保存在workspace目录下的.cache/clangd目录下),待所有索引文件创建完成再回到代码窗口可以看到#include后面的头文件名下面都有了下划线,这个时候代码里的函数、变量、宏定义和头文件就可以通过Ctrl+鼠标左键点击来跳转查看定义了,指针悬停在这些地方也会显示出预览了。相比source insight创建index,感觉clangd要快不少,而且创建的index数据库也比较小。后续如果文件改动了,clangd在启动的时候会自动重新同步并生成新的index,但是如果新增了文件的话就需要重新执行bear make去生成新的compile_commands.json,这样新的文件才会被索引。

手把手教你使用VSCode进行linux内核代码阅读和开发_第6张图片

效果

结构体成员自动补全

手把手教你使用VSCode进行linux内核代码阅读和开发_第7张图片

指针悬停自动查看函数、变量原型,宏定义值等

手把手教你使用VSCode进行linux内核代码阅读和开发_第8张图片

精确查找变量、函数被引用的地方。这里查找的是vin_pipe_ops结构体的close函数指针被引用的地方,可以看到查找的结果是非常准确的,没有查找到其他驱动或内核代码的close指针。

手把手教你使用VSCode进行linux内核代码阅读和开发_第9张图片

遇到的问题

使用时遇到一个困扰了比较久的问题就是手上有一个平台的内核源码compile_commands.json生成没问题,但是clangd在生成索引的时候速度快的不正常,几千个文件的索引数据几秒钟就生成好了你敢信。生成完后代码跳转查看定义功能完全没有。到workspace下的.cache/clangd下面查看实际就只生成了几十个文件的索引,尝试删掉目录重新启动VSCode重新创建索引库,但效果依旧,不知道啥原因,在clangd官网Troubleshooting上也没找到类似问题,于是搁置了一段时间。后来有了一点时间再次尝试,找到查看clangd日志的方法后发现差不多在每个文件indexing的时候日志都会有下面的报错:

unknown target ABI ‘armv7-a’

Couldn’t build compiler instance

根据这个报错信息在clangd github issue 上找到了解决方案,即在workspace目录下创建一个.clangd文件,里面填入下面文本:

CompileFlags:
  Add: --target=armv7-a

64位平台如果报错可以尝试用下面的配置

CompileFlags:
  Add: --target=aarch64-linux-gnu
  Remove: -mabi=lp64

然后删掉旧的index,重新让clangd创建index,这次速度就正常了,完毕后代码跳转功能也正常了。

如果后续遇到其他问题,比如某些函数定义查找不到,无法跳转或只能跳转到声明的地方,可以在clangd的设置项里加上-log=verbose(见clangd配置章节),然后在VSCode的OUTPUT窗口选择clangd日志输出,这样就可以看到一些报错信息,比如像下面这种情况就会导致查看kernel/power/main.c里的函数定义时无法跳转到对应的C文件。问题解决后可以再将-log参数改为info或error以减少日志输出。

手把手教你使用VSCode进行linux内核代码阅读和开发_第10张图片

20210729补充:

一段时间没有连接服务器,后来重新连接发现remote ssh连不上,要么连接的时候报错:

Error: Running the contributed command: '_workbench.downloadResource' failed

要么反复输入密码但就是登不进去,通过搜索上述报错信息同样是在github的issue里找到了解决方案,原因是vscode remote ssh插件自动更新后需要同步更新服务器端,remote ssh连上服务器后会在服务器端执行wget下载服务器端的一个支持包(或者是更新包?),但由于服务器DNS配置有问题导致域名无法解析,下载失败从而导致remote ssh一直报错,修复域名解析就好了(需要先删除remote ssh在远程服务器上创建的文件锁,否则remote ssh会一直等待)。同样问题也可参考这里。也可以尝试在remote ssh的设置里将local server download改成always,这样remote ssh更新的时候就只会通过主机端更新然后将更新包传到远程服务器安装,这样可以解决远程服务器处于内网无外网访问权限时更新,但是我自己有次更新时发现主机端更新时仍然报错,在github issue上似乎也没找到好的解决方法,于是还是改回auto了:(

20210826补充:

之前浏览内核代码的时候发现总有些文件索引时报错无法创建index,或者提示有些编译选项不支持、代码中有些汇编指令无法识别导致报错进而也导致一些文件或文件中部分函数定义无法索引。尝试将compile_commands.json里"cc"全部替换为交叉编译器所在路径比如"/home/xxx/mnt/cv2x_linux_sdk_3.0.5/toolchain/linaro-aarch64-2020.09-gcc10.2-linux5.4/bin/aarch64-linux-gnu-gcc"后(编译是arm64位的平台)这些问题基本都解决掉了。可能之前遇到的unknown target ABI ‘armv7-a’也是因为compile_commands.json里没有给出具体的编译器,clangd默认使用x86的gcc导致报错。至此,该方法应该算完美了:)。

总结

通过上面的介绍相信你已经了解到该方法的强大、好用了。当然该方法也不仅限于浏览Linux内核代码,应用程序的代码也是可以的,前提是要能生成compile_commands.json这个文件,否则clangd无法解析。另外上面介绍的都是利用Remote SSH通过远程访问代码实现的,对于windows本地代码工程clangd也可以实现上述功能,具体请自行摸索。如果是小工程,又没有比较方便的方法生成compile_commands.json的话(比如像单片机程序),可以使用本文参考链接4的方法去实现,但记得在插件管理里面disable clangd并使能C/C++和C++ Intellisense。

你可能感兴趣的:(vscode,linux,ide)