搭建「Linux」内核调试环境

2,202 阅读12分钟

前言

学习内核 IO 模型的顺序IO、IO多路复用的 epoll 实现等知识点,不亲自去 debug 一下内核是真的非常晦涩难懂,所以这篇文章主要是记录一下如何使用 qemu 加 gdb 的方式去搭建 linux 内核的调试环境。 主要使用得到的东西有:

NameDescriptionUrlNote
Linxu kernel Source CodeLinux 内核的源码ftp.sjtu.edu.cn/sites/ftp.k…国内能快速访问
UbnutuUbnutu 的 iso 镜像文件mirrors.aliyun.com/ubuntu-rele…
Vmware Fusion启动 Ubnutu 系统,构建 Linux 内核的编译环境www.vmware.com/cn/products…
qemu虚拟器,用于运行 Linux 内核看下文
busybox用于制作 Linux 内核启动所需的 initrd开下文
gdb一个强大的程序调试工具,用于调试 qemu 运行的 Linux 内核看下文

流程

首先需要用 vmware fusion 启动 ubnutu ,网络模式选择WI-FI(不过好像选择其他模式也可以) 注意,硬盘容量最好给到 200 G,最少 100 G,我已经遇到只分配了 20 G 的硬盘,编译完内核以后根文件系统只剩下几十M容量后,登录上 ubnutu 后桌面除了鼠标能动后啥都没有的问题了! image.png

1. 在 ubnutu 中下载编译 linux 内核

# 1. vmware 启动 ubnutu ,硬盘建议分配到 100 GB 空间
# 否则后期如果出现了根目录磁盘容量不够的情况,
# 会出现登录上 ubnutu 系统后,桌面只有鼠标能动的坑

# 2. 登录 ubnutu 系统

# 3. 设置 root 密码
sudo passwd

# 4. 切换 root 用户
su root

# 5. 安装 vim、openssh-server
# 安装 openssh 后,可以宿主机用 ssh 连接到 vmware 内的 ubnutu,会方便一点 
apt-get install vim openssh-server -y

# 6. 启动 openssh-server
/etc/init.d/ssh start

# 7. 接下来可以切换到宿主机外用 ssh 连接到 ubnutu 了

# 8. 下载解压 linux 内核
cd /root
wget http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel/v5.x/linux-5.0.1.tar.gz
tar zxf linux-5.0.1.tar.gz

# 9. 进入 linux 内核源码目录
cd linux-5.0.1

# 10. 安装编译工具
apt install build-essential flex bison libssl-dev libelf-dev libncurses-dev -y

# 11. 设置调试的编译菜单。
export ARCH=x86
make x86_64_defconfig
make menuconfig、
# 下面选项如果没有选上的,选上(点击空格键),然后 save 保存设置,退出 exit。
##################################################################
General setup  --->
    [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support

Device Drivers  --->
    [*] Block devices  --->
        <*> RAM block device support
            (65536) Default RAM disk size (kbytes)

Processor type and features  --->
    [*] Randomize the address of the kernel image (KASLR)
    
Kernel hacking  --->
    Compile-time checks and compiler options  ---> 
        [*] Compile the kernel with debug info
            [*] Provide GDB scripts for kernel debugging

Device Drivers --> 
    Network device support --> 
        <*> Universal TUN/TAP device driver support

[*] Networking support --> 
        Networking options --> 
            <*> 802.1d Ethernet Bridging
##################################################################

# 12. 编译 linux 内核
# -j4 是启动 4 个线程进行编译,加快编译速度
make -j4

2. 使用 busybox 制作 initrd

initrd 是 linux 内核启动过程中所需的临时根文件系统,包含了内核初始化过程中所需的设备驱动等。

cd /root

# 下载解压 busybox 源码
wget https://busybox.net/downloads/busybox-1.30.0.tar.bz2 --no-check-certificate
tar xvf busybox-1.30.0.tar.bz2

# 设置静态编译,这样 busybox 在运行时就不需要动态链接其他 ELF 文件
# Busybox Settings  --->
#      Build Options  --->
#            [*] Build BusyBox as a static binary (no shared libs)
make menuconfig

# 编译 busybox,会在源码目录下生成 _install 目录,即 linux 系统的根目录
make && make install

cd _install

# 创建软连接 init 指向 bin/busybox,内核启动到最后会执行 init 进程
ln -sf bin/busybox init
mkdir -p {sys,proc,dev,etc/init.d}

# 启动脚本,相当于 rc.local
touch etc/init.d/rcS 
chmod 755 etc/init.d/rcS
# etc/init.d/rcS内容见下方
vi etc/init.d/rcS

touch etc/fstab
 # etc/fstab内容见下方
vi etc/fstab

# 这里的 pigz 可以多线程压缩,需要安装 pigz,或者使用 gzip 替代。
sudo apt install pigz
find . -print0 | cpio --null -ov --format=newc | pigz -9 > /root/initrd-busybox.img

文件 etc/init.d/rcS 的内容

#!/bin/sh
# RC Script for Tiny Core Linux
# (c) Robert Shingledecker 2004-2012

# Mount /proc.
[ -f /proc/cmdline ] || /bin/mount /proc

# Remount rootfs rw.
/bin/mount -o remount,rw /

# Mount system devices from /etc/fstab.
/bin/mount -a


clear

文件 etc/fstab 的内容

sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
udev /dev devtmpfs rw,nosuid,noexec,relatime,mode=755 0 0

3. 安装 qemu

# 安装 qemu 模拟器,以及相关组件。 
apt install qemu libc6-dev-i386 -y

4. 源码安装 gdb

gdb 是一个强大的通过交互式命令行调试指定程序的工具,在这里可以用于调试通过 qemu 运行的 linux 内核(qemu 自身支持 gdb 调试)。ubnutu 有自带 gdb,但是会有无法调试 qemu 下的 linux 内核的坑,所以需要下载 gdb 的源码并且修改其源码后重新编译。

# 删除 gdb
gdb -v | grep gdb
apt remove gdb -y

# 下载解压 gdb
cd /root
#wget https://mirror.bjtu.edu.cn/gnu/gdb/gdb-8.3.tar.xz
wget http://ftp.gnu.org/gnu/gdb/gdb-8.3.tar.gz
tar zxf gdb-8.3.tar.gz

# 修改 gdb/remote.c 代码。
# 看下方
cd gdb-8.3
vim gdb/remote.c

./configure
make -j4
cp gdb/gdb /usr/bin/
  1. 修改 gdb/remote.c 的代码
/* Further sanity checks, with knowledge of the architecture.  */
// if (buf_len > 2 * rsa->sizeof_g_packet)
//   error (_("Remote 'g' packet reply is too long (expected %ld bytes, got %d "
//      "bytes): %s"),
//    rsa->sizeof_g_packet, buf_len / 2,
//    rs->buf.data ());

if (buf_len > 2 * rsa->sizeof_g_packet) {
    rsa->sizeof_g_packet = buf_len;
    for (i = 0; i < gdbarch_num_regs(gdbarch); i++) {
        if (rsa->regs[i].pnum == -1)
            continue;
        if (rsa->regs[i].offset >= rsa->sizeof_g_packet)
            rsa->regs[i].in_g_packet = 0;
        else
            rsa->regs[i].in_g_packet = 1;
    }
}

5. qemu 启动 linux 内核

cd /root

qemu-system-x86_64 -kernel ./linux-5.0.1/arch/x86/boot/bzImage -initrd ./initrd-busybox.img -append nokaslr -S -s

解释一下,-kernel 参数指定的就是 linux 内核,-initrd 指定的是 linux 内核启动过程中所需的临时根文件系统,由 boot loader 加载到内存中。 -s的是-gdb tcp::1234的别名,即在 1234 端口开启 gdb server 以调试 linux 内核。-S 是可以 debug linux 内核的启动阶段,温馨提示:可以 debug 内核是如何处理 initrd 的,若不加-S参数,则直接就到控制台去了。

-s  Shorthand for -gdb tcp::1234, i.e. open a gdbserver on TCP port 1234.

-S  Do not start CPU at startup (you must type 'c' in the monitor).

上面的命令是一定要在 vmware fusion 启动的 ubnutu 中才可以顺利执行,否则会报错 Could not initialize SDL(No available video device) - exiting 看起来是因为必须用到显示设备驱动之类的gui问题,加上--curses就可以解决。

6. 使用 GDB 调试内核

cd /root
# 读取编译出来的 linux 内核 ELF 文件
gdb linux-5.0.1/vmlinux

# ... 进入到 gdb 到交互式命令行

# 连接到本地 qemu 启动的 linux 内核
# 对应 qemu 启动命令的 -s 参数
target remote :1234

# 即可开始调试 Linux 内核

7. 配置 qemu 网络(可选)

此时用 qemu 启动的 Linux 内核是不具备网络功能的,这里将基于 tun/tap 和 bridge 来实现 qemu 的网络功能,关于 tun/tap 和 bridge 的详情会另开一篇文章来写(因为我也还没完全搞清楚),下面开始配置 qemu 网络。 我们上面使用 vmware 启动的 ubnutu 宿主机默认是支持 tun/tap 驱动的,是作为 ubnutu 的默认模块编译到了内核中的,/dev/net/tun 是 tun/tap 的字符设备,是用户态和内核态之间网络数据收发的桥梁(这一块涉及 tun/tap 的原理相关),即使因为未知的原因 lsmod 并未显示内核已经加载了 tun/tap 模块。 image.png image.png (所以,我假设你的宿主机是支持 tun/tap 的,如果不支持,可能需要重新编译你的宿主机的内核了) 第一步,在宿主机安装相关软件。

# 虚拟网桥工具
apt-get install bridge-utils

# UML(User-mode linux)工具
apt-get install uml-utilities

第二步,修改宿主机的 /etc/network/interfaces 文件,增加下面的内容。

# vim /etc/network/interfaces 文件

# 增加名为 br0 的虚拟网桥,通过 dhcp 获取 ip 地址
auto br0
iface br0 inet dhcp

# 我这台主机的真实物理网卡的 nf 名为 eth0(一般都是叫 eth0 )
bridge_ports eth0
bridge_fd 9
bridge_hello 2
bridge_maxage 12
bridge_stp off

# 增加名为 tap0 的虚拟网卡
auto tap0
iface tap0 inet manual
pre-up tunctl -t tap0 -u root
pre-up ifconfig tap0 0.0.0.0 promisc up

# 把 eth0 和 tap0 通过虚拟网桥连接起来
post-up brctl addif br0 tap0

第三步,重启宿主机以刷新网络。 然后输入 ifconfig 命令,应该可以看到真实物理网卡 eth0、本地回环 lo、虚拟网桥 br0、虚拟网卡 tap0,并且通过 brctl showstp br0 可以看到 eth0 和 tap0 都加入了虚拟网桥 br0 中。 image.png image.png

第四步,在 qemu 的启动命令追加网络相关参数即可。

# -net tap 表示使用 tun/tap 类型的网络,还有一种是 user-stack 类型
# script 是指定内核启动时,用于配置网络的脚本绝对路径
# downscript 时指定内核关闭时,用于注销网络相关配置的脚本绝地路径
-net nic -net tap,ifname=tap0,script=no,downscript=no

# 可调试 linux 内核启动过程的完整命令
qemu-system-x86_64 -kernel ./linux-5.0.1/arch/x86/boot/bzImage -initrd ./initrd-busybox.img -append nokaslr -S -s -net nic -net tap,ifname=tap0,script=no,downscript=no

# 不可调试 linux 内核启动过程的完整命令
qemu-system-x86_64 -kernel ./linux-5.0.1/arch/x86/boot/bzImage -initrd ./initrd-busybox.img -append nokaslr -s -net nic -net tap,ifname=tap0,script=no,downscript=no

image.png 后来发现还没完,网络发现还是无法用,还有第五步,在 qemu 启动的内核里还得进行一些配置。 ifconfig 会看到此时空无一物,linux 不是说没有消息就是好消息嘛,ls /sys/class/net 可以看到是有 eth0 和 lo 的,那问题就简单了。 image.png ping 一下宿主机发现报错了,仔细看上面 ifconfig 的输出,eth0 是没有 ip 地址的,应该是编译内核的时候,没有勾选 dhcp 相关选项,自己配置一下就好了。 image.png 使用 udhcpc 命令可以获取到可用的 ip 地址。 第五步,在 qemu 启动的客户机里,开启 eth0 以及配置其 ip 地址。

# 开启 eth0 网络接口
ifconfig eth0 up
# 顺便把本地回环也开一下
ifconfig lo up

# ping 一下宿主机,发现不通
# 仔细看 ifconfig 的输出,eth0 是没有 ip 地址的,
# 应该是编译内核的时候,没有勾选 dhcp 相关选项,
# 自己配置一下就好了,
# 使用 udhcpc 命令可以获取到可用的 ip 地址
udhcpc

# 加上 udhcpc 获取到的 ip 地址即可,192.168.105 需要替换为你自己获取到的 ip 地址
ifconfig eth0 192.168.105 

qemu 客户机和 ubnutu 宿主机之间的网络是通的,但是,你试一下,ping 百度的 ip 地址(因为现在 qemu 客户机还没有配置好 dns 协议),是不通的。

第六步,在 qemu 客户机增加路由到所在局域网网关的路由项。 首先,通过宿主机查看网关的 ip 地址

root@elvis:~# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.1.1     0.0.0.0         UG    0      0        0 br0
169.254.0.0     0.0.0.0         255.255.0.0     U     1000   0        0 br0
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 br0

可以看到,宿主机上最后所有不是发往局域网的网络包都是路由到 192.168.1.1 这个网关上的, 所在 qemu 客户机也需要增加上这条路由。

     / # route -n
     Kernel IP routing table
     Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
     192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0
     / #
     / # ip route add 0.0.0.0/0 via 192.168.1.1 dev eth0
     / # route -n
     Kernel IP routing table
     Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
     0.0.0.0         192.168.1.1     0.0.0.0         UG    0      0        0 eth0
     192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0
     / #

接着,试一下 ping 百度的 ip,ok。 image.png 至此,你已经有了一台拥有网络功能的可以 debug 内核的 Linux 机器了!

命令汇总

完整的 qemu 启动过程

kernel_path=./linux-5.0.1/arch/x86/boot/bzImage
initrd_path=./initrd-busybox.img
qemu-system-x86_64 -kernel ${kernel_path} -initrd ${initrd_path} -append nokaslr -s -net nic -net tap,ifname=tap0,script=no,downscript=no --curses

qemu 启动后,配置网络的过程

# 启动 eth0 网卡
ifconfig eth0 up
# 通过 dhcp 协议获取 ip 地址
udhcpc 
# 给 eth0 网卡配置ip地址
ifconfig eth0 ${获取到的 ip 地址}
# 添加网关路由,注意你需要到宿主机上确认 192.168.1.1 是不是你宿主机所在局域网的网关 ip 地址
ip route add 0.0.0.0/0 via 192.168.1.1 dev eth0
# 测试一下 tcp 能否到百度,因为没有配置 dns,所以直接用 ip
ping 14.215.177.39

补习

主机上电到 Linux 内核的启动过程

  1. POST (Power On And Self Test) : 主机开机上电自检 主板上有一块CMOS芯片,上面刻录了一段程序称之为 BIOS (Basic Input/Output System) 基本输入输出系统,BIOS 会对主机的核心设备如CPU、键盘、存储设备如硬盘进行检查和初始化。
  2. MBR (Master Boot Record) : 主启动记录 POST 结束后,BIOS 程序会让用户指定启动顺序,即从主机上的哪一块存储设备中获取 MBR,MBR 中存储了存储设备的分区信息以及 Boot Loader 引导程序。
  3. Boot Loader:引导程序 引导程序负责从存储设备中读取操作系统内核到内存中,把主机的控制权转交给操作系统内核。以 qemu 启动内核的命令举例,-kernel 参数指定的即操作系统内核的可执行程序。 目前主流的引导程序有 GRUB ,引导程序也可以视作是一个小型的操作系统,也具有操作硬盘和内存的能力,可以把内核从硬盘加载到内存中。
  4. initrd/initramfs: 初始化RAM磁盘 / 初始化RAM文件系统 引导程序启动的内核仅仅是内核核心而已... 它不包含各种驱动模块(包括硬盘控制器的驱动)。所以内核被引导以后其实无法完成对 root 的挂载... 无法挂载文件系统便不能读取并载入内核驱动模块,没有驱动支持就不能管理硬盘(完成对文件系统的挂载)... 所以这些如硬盘驱动等核心部件需要通过 initrd/initramfs 来提供,否则,就需要把所有可能出现在主机上的硬盘的驱动全都预先的编译到内核中(这显然是一个不可能完成的任务)。 initrd/initramfs 可以通过由 boot loader 加载到内存中,并且把其内存起始&结束地址传递给 linux 内核,也可以把 initrd/initramfs 编译到 linux 内核的 ELF 文件中。 intird 和 initramfs 本质是一个东西,initrd 是在内存中模拟一个磁盘设备,inintramfs 是在内存中模拟一个文件系统,initramfs 基于 tmpfs,由 linux 的 virtual memory 管理,而 initrd 模拟的是硬盘,和 kernel 间隔着一层 virtual memory。

tun/tap 虚拟网络设备

下图是 qemu 使用 tun/tap 进行联网的原理,了解一下即可,我暂时也未深入研究。 tuntap_qemu.png

参考

搭建 Linux 内核网络调试环境(vscode + gdb + qemu)

Linux内核Ramdisk(initrd)机制

initrd和initramfs的区别是什么

qemu网络配置官方文档

网络虚拟化之tun/tap