返回首页

VSCode,Linux内核select调试,粗看select

VSCode,Linux内核select调试,粗看select

Record for debug program on linux

网上记录 vscode+gdb+qemu 调试Linux已经有很多文章了,我只是自己记录一下防止失忆我干了什么,以及一些还未解决的问题。

首先要说的是,本来我只是想看看select的实现,许多人说的bitmap,只是一个long类型数组,通过这个16长度的long数组来表示1024位的bitmap。因为写了一个小的select程序,想看看内核如何实现。毕竟纸上得来终觉浅,网上有很多解释说明的了。但我只是想看看。本来我的主机就是arch linux,所以我不需要虚拟机来搭建环境(但是我还是在arch上创建了ubuntu虚拟机并且搭建了环境😁),虚拟机还是挺多的,网上也有帖子说打docker, 现在没时间,有时间再打docker吧。by the way,记录一下我在arch上的虚拟机。

准备阶段

我的虚拟机一般是拿来自己做实验,这次我只是嫌弃麻烦,直接就在本机(arch)上搭建环境了。

Linux内核

  1. 下载Linux内核
wget https://mirrors.edge.kernel.org/pub/linux/kernel/v6.x/linux-6.5.9.tar.xz
tar -xvJf linux-6.5.9.tar.xz
  1. 配置Linux内核

主要是debug相关。其中有个要说下。就是KASLR(Kernel Address Space Layout Randomization 内核地址空间随机化)这个选项,这个选项是为了防止内核被攻击者利用,通过随机化内核地址空间,使得攻击者难以获得内核实际运行地址。但是这个选项会导致gdb无法调试内核,即在编译时嵌入的调试信息的相关内存地址,在运行之后大概率是无效的,因为此时内存地址重新随机分配了,所以这里要关闭这个选项。

make menuconfig

# 基本上不用选什么东西
Kernel hacking --->
  Compile-time checks and compiler options --->
    Debug information(Generate DWARFe Version 5 debuginfo) --->
      打开后选(Generate DWARF Version 5 debuginfo)
    [ ] Reduce debugging information
    [*] Provide GDB scripts for kernel debugging

# 关闭KASLR
Processor type and features --->
-*- Build a relocatable kernel
  [ ] Randomize the address of the kernel image (KASLR)

保存退出之后可以开始编译。

  1. 编译内核生成bzImage以及vmlinux
# 给你的机子热热身没毛病吧家人们😎
make -j$(nproc)

这里会生产对应的镜像和可执行文件分别在当前目录的arch/x86/boot/bzImage(我自己的机器是x86架构)以及当前目录下的vmlinux至于这两个文件干啥的,懂得都懂,不懂你自己忘了就去查o~。

  1. 安装相关依赖qemu等

在arch上安装qemu相关以来挺容易的,一个命令就解决了。gdb就不说了哈,既然是debug,肯定是要安装的。有些文章用的ubuntu老版本,还得自己从源码编译gdb着实麻烦。

sudo pacman -S qemu-full

程序准备

这里用busybox准备一个简易的文件系统,先来下载busybox源码。网上挺多人下载老版本,那还是算了,因为我是arch,我就要新版。(但是也可以使用别的啦,或者直接啥也不用把程序重命名为init,放在initramfs的镜像里就行,就是这样网络就不太好配置了,毕竟我一开始只是想看看内核在select函数调用的时候在干嘛(你干嘛哎呦~🐔))

  1. 下载busybox并且配置一下编译选项
wget https://www.busybox.net/downloads/busybox-1.36.1.tar.bz2
# 我就懒得验证了,直接解压,我在note.md里说过,o~,note.md我还没放出来
bzip2 -cd busybox-1.36.1.tar.bz2 | tar -xvf -
cd busybox-1.36.1

具体配置我也不太记得了,有需要静态链接的,但是也没几个要选的,大概是下面这几个吧。

make menuconfig

Settings --->
--- Build Options
[*] Build static binary (no shared libs)
# 保存退出并编译
make -j$(nproc)
# install一下,默认安装到 ./_install目录下

好了,一开始做一个简单的根文件系统了,其实也可以不做,单纯把程序放到镜像里也行,下面是单纯把程序放到镜像里的命令,提一嘴。

# 编译的时候记得加-g选项o~,最后重命名为init程序
find init | cpio -o -Hnewc | gzip -9 > ../rootfs.img
# 可以直接开始启动
qemu-system-x86_64 -kernel ../../arch/x86/boot/bzImage -initrd ../rootfs.img -device e1000,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5005-:5005

开始做一个镜像

dd if=/dev/zero of=rootfs.img bs=1M count=256
mkfs.ext3 rootfs.img

然后就是往里面填装东西了。没什么好说的,具体目录看自己来,不要呆呆的。(完蛋了,整个人都烦躁了,最近烦心事很多,看了看键盘,还可以,待会放在文末给大家看看)

sudo mkdir /tmp/rootfs-busybox
sudo mount -o loop rootfs.img

sudo cp -a busybox-1.36.1/_install/* /tmp/rootfs-busybox/
# 也可以用pushd /tmp/rootfs-busybox/ 待会用popd回来就是了
cd /tmp/rootfs-busybox
sudo mkdir -pv dev sys proc etc lib mnt
# cd到上次的目录
# popd
cd -
# 将一些初始化文件放到/tmp/rootfs-busybox/etc
sudo cp -a busybox-1.36.1/examples/bootfloppy/etc/* /tmp/rootfs-busybox/etc/

修改rcS,为了后面宿主机器与虚拟机通过网桥通信做准备,可以额外添加ip相关命令,我就直接放在这了(我自己要是忘了,我记得去搜索)

# 将多的内容添加进rcS
cat /tmp/rootfs-busybox/etc/init.d/rcS
/bin/mount -a
/bin/mount -t sysfs sysfs /sys
/bin/mount -t tmpfs tmpfs /dev
/sbin/mdev -s
ip addr add 192.168.0.2/24 dev eth0
ip link set eth0 up

然后卸载对应目录

sudo umount /tmp/rootfs-busybox

Bridge

这里先创建网桥以及对应接口,以便于待会通信用。 这里还一个问题就是,我自己的有线网卡模块可能没有加载,要手动start对应的service才能看到设备 这个service是自己写的

[Unit]
Description=Load r8169 module at startup

[Service]
Type=oneshot
ExecStart=/usr/bin/modprobe r8169

[Install]
WantedBy=multi-user.target
# 以下内容在宿主机上执行, 防止我失忆提醒以下
# 先安装bridge-utils,虽然但是这个工具我好早就安装过了,我也不知道为什么
sudo pacman -S bridge-utils

# 添加网桥
sudo brctl addbr br0
# 添加接口,这里我用的是eno1,具体可以自己用ifconfig或者ip a看看,也有eth0的,看你有什么设备
sudo brctl addif br0 eno1

sudo ip tuntap add tap0 mode tap
sudo ip link set tap0 up
sudo brctl addif br0 tap0

sudo ifconfig eno1 0
sudo dhclient br0

# 可以看看br0上是不是有tap0和eno1两个接口了
sudo brctl show

sudo ip addr add 192.168.0.1/24 dev br0

qemu,启动!(虽然但是我不玩)

qemu-system-x86_64 -kernel ../arch/x86/boot/bzImage -append 'root=/dev/sda console=ttyS0' -boot c -hda ../../busybox-1.36.1/initramfs/rootfs.img -k en-us -net nic -net tap,ifname=tap0,script=no -nographic

这里先不用-s -S这种等待gdb连上来再运行的操作,我们先来看看情况。下面是我运行的情况

接着双方(主机和虚拟机)ping一下,看看情况。

VSCode准备

可以开始简单配置一下VSCode了。可以在左侧点击debug按钮,然后有个add configuration,也可以直接在当前目录(这里是linux内核源代码目录)下创建.vscode目录。先来解决一些定义报错问题。在linux内核源码目录下使用内核自带的./scripts/clang-tools/gen_compile_commands.py 在源代码根目录下生成compile_commands.json文件,然后在.vscode目录下添加c_cpp_prpperties.json文件,文件内容如下

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c17",
            "cppStandard": "c++17",
            "intelliSenseMode": "linux-clang-x64",
            "compileCommands": "${workspaceFolder}/compile_commands.json"
        }
    ],
    "version": 4
}

然后是tasks配置,这个其实也可以不配置,但是单纯用命令行启动也行,我这里配置了一下task,可以在顶部Terminal按钮下点击Configure Task来创建Task,或者直接在.vscode下创建tasks.json文件,然后填充一下内容。下面是我的tasks.json文件内容,其实就是把命令放进去。把要debug的虚拟机先启动起来。如果报错,直接Debug Anyway别管。

{
	"version": "2.0.0",
	"tasks": [
		{
			"type": "shell",
			"label": "vm",
			"command": "qemu-system-x86_64 -s -S -kernel ${workspaceFolder}/arch/x86/boot/bzImage -append 'root=/dev/sda console=ttyS0' -boot c  -hda ../../../busybox-1.36.1/initramfs/rootfs.img -k en-us -net nic -net tap,ifname=tap0,script=no -nographic",
			"args": [],
			"isBackground": true,
			"options": {
				"cwd": "${fileDirname}"
			},
			"detail": "compiler: /usr/bin/clang"
		},
	]
}

这个启动虚拟机的任务是debug的前置任务,所以还需要配置一下launch.json文件,把这个以vm为label的任务添加到作为debug的前置任务。下面是我的launch.json(也在.vscode目录下)文件

{
    "configurations": [
        {
            "type": "cppdbg",
            "request": "launch",
            "name": "kerneldebug",
            "preLaunchTask": "vm",
            "program": "${workspaceFolder}/vmlinux",
            "args": [],
            "stopAtEntry": true,
            "cwd": "${workspaceFolder}",
            // "miDebuggerServerAddress": "127.0.0.1:1234",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "miDebuggerPath": "/usr/bin/gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                },
                {
                    "description": "set my self program",
                    // "text": "add-symbol-file ${workspaceFolder}/mytest/test_select/init",
                    "text": "add-symbol-file ${workspaceFolder}/mytest/test_select/test_select",
                    "ignoreFailures": false,
                },
                {
                    "description": "set remote",
                    "text": "target remote 127.0.0.1:1234",
                    "ignoreFailures": false
                }
            ],
        }
    ]
}

这里说下这个setupCommands 是在启动gdb之后,在gdb内执行的命令,这里的add-symbol-file是使用initrd这个版本启动qemu的时候我添加的。这样就能debug用户态的程序了,但是貌似内核态进不去了,而且网络不好配置,算是遗留问题,不管了。还有这个里面的target remote 127.0.0.1:1234 等价于 target remote :1234 也可以直接删掉这整个命令,直接把上面注释里面miDebuggerServerAddress 打开就行,这也是我之前测试留下的,二者选一个就行。

对了preLaunchTask的值应该是前面那个task的标签。好了现在可以先起来看看情况了,待会再把程序放进去,那里也还得说下,防止我忘记。(你说是吧,还有很多笔记,要不是以前写了,我都忘了原来接触过很多东西,好记性不如烂笔头) 这个时候你找到位置与launch内描述的位置相同的找个c文件,点击右上角debug启动就行。选择那个我们配置的launch,也就是kerneldebug,至于运行具体程序,待会放在虚拟机里面再说。

Prepare Program

PreDebug

因为使用的busybox文件系统太简单了,不能运行第三方程序,所以这里我直接把对应我需要测试的程序需要的动态链接库so文件复制到busybox里面。下面是具体操作 我先写个Makefile,待会编译方便

CC = gcc
CFLAGS = -g -O0 -W -Wall

test_select:
	$(CC) $(CFLAGS) -o test_select select.c

clean:
	rm -f test_select
# 当前源代码目录make一下
make

然后先挂载之前做好的busybox的镜像

sudo mount -o loop rootfs.img /tmp/rootfs-busybox

然后查看一下我这个程序需要那些so文件,之后将对应的so复制到busybox对应的文件夹下面

ldd test_select
# 运行结果
#linux-vdso.so.1 (0x00007ffd646eb000)
#libc.so.6 => /usr/lib/libc.so.6 (0x00007f87414dd000)
#/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f87416f6000)

所以只要把libc.so.6以及ld-linux-x86-64.so.2复制到对应的/usr/lib和/lib64下即可, 先在/tmp/rootfs-busybox的/usr下创建文件夹然后把本地的so文件复制过去

sudo mkdir -pv /tmp/rootfs-busybox/{usr/lib,lib64}
sudo cp -a /usr/lib/libc.so.6 /tmp/rootfs-busybox/usr/lib/
sudo cp -a /usr/lib64/ld-linux-x86-64.so.2 /tmp/rootfs-busybox/lib64/

最后将我们这个程序复制过去

# 我这里就直接放到根目录了
sudo cp -a test_select /tmp/rootfs-busybox/
# 最后卸载
sudo umount /tmp/rootfs-busybox

这个时候再启动vscode进行debug,就能看到刚刚的test_select,程序并且可以直接运行了,顺便在宿主机写了个客户端程序与虚拟机里面的服务端进行通信试试。也是先运行起来再说。

Debug (开始真的打断点了啊😀)

调通我的select(简单看看)

目前我没有找到什么好的方法可以在用户态程序上打断点,然后转跳到内核态的代码。所以目前断点只能打在内核态,但是对于稍微研究I/O多路复用以及未来可能查看tcp协议栈的实现应该是够用了(可能吧)。

因为我的程序使用的是select系统调用,所以在调用select的时候,会触发系统调用,我们在vscode里面搜索系统调用的时候,可以直接搜SYSCALL_DEFINE.这里的点是一个正则,表示一个数字,(虽然可以匹配任何字符,我懒得打了), 这些函数内部会有对应系统调用的实现,这里我想要的是select,所以可以搜到下面的。其实最终会内核会运行到core_sys_select 然后到 do_select这个函数,所以我们先在这直接打断点。

当然这个可以在运行途中打断点,这个就不说了,不会不知道吧☺️ 。接下来可以按照上面的步骤直接开启debug了,在开启之后可以用客户端进行连接看看。

先来看我自己写的源代码o,整体上只是一个简单使用select的echo服务端。对于select的常用操作,就不说了,我是来通过debug学习的,没有很牛的能力去完全解析。我这里将listensock,也就是服务端的用于监听的socket的fd放入可读fds,这里说一下,除了发送数据,譬如接受客户端连接,客户端断开连接,客户端发送数据过来,这些事件都属于可读事件握。设置maxfd便于后面在代码中遍历fd。

此时从用户态代码我们直接跳到之前打了断点的地方,也就是core_sys_select 函数内, 简单看一下select吧设置超时时间和等待队列啥的,以后我再看,这里调用了do_select(暂且先不看他前面干了什么,因为我还没看哈哈哈哈哈),右边的调用占可以看到全部是内核态的代码。这里我们追踪进do_select函数,发现在做没有条件的for循环,并且,先把fds内部的输入和输出的fds(这里包括异常fds)指针拿到。然后再次进行循环,该循环的最大值即为我们之前设置的maxfd,因为0,1,2分别为进程的标准输入,标准输出,标准错误输出。所以这里在listensock的值为3, 而maxfd的值为listensock+1, 所以这里的n也就是4了。在左侧的变量查看窗口我们也能看到。

内部第一个循环,将从0,到n也就是遍历所有的文件描述符。对于每个描述符,遍历一边每一个位。(BITS_PER_LONG这里是64)在下面的if语句中,通过左移bit来确定all_bits中哪一位需要处理(毕竟64位嘛),这里我们只有一个值为8也就是在第四个比特位的listensock,当bit左移到此之后,就会与all_bits &一下,则会非0,之后取反跳出if,继续执行下面的语句。 EPOLLNVAL 将mask设置为一个无效值,vfs_poll 貌似检查是否有事件发生。下面的一堆if就是判断事件是不是in,out,ex并且是不是对应的fd。

因为此时最大的fd就是4了,i到4,内部没必要继续检测了省去了不必要的迭代,这个也说明了使用select时,记得更新maxfd啊!因为啥事件也没有,这里的res_in, res_out, res_ex 全部为0。接下来是cond_sched应该是个调度函数,先不管了。由于没设置超时时间,所以直接运行到了poll_schedule_timeout,这里的expires参数也是0,这里面我就不追了,在这里进程就陷入等待了。等待一个软中断的唤醒。

vscode中左边啥也没有了。接下来就是启动客户端了。

启动客户端之后看到poll_schedule_timeout函数返回咯vsc也可以看到变量情况了

接下来就是重复之前的步骤,检测对应的事件,这次检测到POLLIN_SET了。res_in也被设置为了bit(此时), 也该结束do_select了,返回retval(==1)

在结束之后,res_in被设置为对应的fd,也就是位置为3的listensock,即该socket有读事件了。这里的inp指向的是用户传进来的long数组(bitmap就long数组)。

下面调用set_fd_set, 里面调用__copy_to_user函数再继续追,调到raw_copy_to_user函数,懒得追了,就最里面调用asm执行汇编了貌似,之后poll_select_finish,会结束kernel的代码,最终回到用户态代码。

暂时到这

所以粗略看了看select的执行流程,后面有时间再仔细看看,主要是搭建了一个可以调试kernel的环境,便于后续对于kernel代码的研究。

还存在的问题

  • 目前只能调试一个程序,该怎么调试多进程呢?
  • vscode有些变量貌似无法查看,有些代码是unknown source

最后发一下感觉还蛮好看的我正在用的键盘

评论 (...)

加载中...