俗话说,工欲善其事 必先利其器。linux kernel是一个非常复杂的系统,初学者会很难入门。
如果有一个方便的调试环境,学习效率至少能有5-10倍的提升。
为了学习linux内核,通常有这两个需要
- 可以摆脱硬件,方便的编译和运行linux
- 可以使用图形化的工具来调试linux
笔者使用VSCode+GDB+Qemu完成了这两个需求
qemu作为虚拟机,用来启动linux。
VSCode+GDB作为调试工具,用来图形化地DEBUG。
最终效果大致如下:
qemu运行界面:
vscode调试界面:
下面将一步一步介绍如何搭建上述环境。
本文所有操作都在Vmware Ubuntu16虚拟机上进行。
安装编译工具链
由于Ubuntu是X86架构,为了编译arm64的文件,需要安装交叉编译工具链
sudo apt-get install gcc-aarch64-linux-gnu
sudo apt-get install libncurses5-dev build-essential git bison flex libssl-dev
制作根文件系统
linux的启动需要配合根文件系统,这里我们利用busybox来制作一个简单的根文件系统
编译busybox
wget https://busybox.net/downloads/busybox-1.33.1.tar.bz2
tar -xjf busybox-1.33.1.tar.bz2
cd busybox-1.33.1
打开静态库编译选项
make menuconfig
Settings --->
[*] Build static binary (no shared libs)
指定编译工具
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
编译
make
make install
编译完成,在busybox目录下生成_install目录
定制文件系统
为了init进程能正常启动, 需要再额外进行一些配置
根目录添加etc、dev和lib目录
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install [1:02:17]
$ mkdir etc dev lib
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install [1:02:17]
$ ls
bin dev etc lib linuxrc sbin usr
在etc分别创建文件:
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:13]
$ cat profile
#!/bin/sh
export HOSTNAME=bryant
export USER=root
export HOME=/home
export PS1="[$USER@$HOSTNAME \W]\# "
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export PATH LD_LIBRARY_PATH
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:16]
$ cat inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:19]
$ cat fstab
#device mount-point type options dump fsck order
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
debugfs /sys/kernel/debug debugfs defaults 0 0
kmod_mount /mnt 9p trans=virtio 0 0
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:26]
$ ls init.d
rcS
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/etc [1:06:30]
$ cat init.d/rcS
mkdir -p /sys
mkdir -p /tmp
mkdir -p /proc
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
这里对这几个文件做一点说明:
- busybox 作为linuxrc启动后, 会读取/etc/profile, 这里面设置了一些环境变量和shell的属性
- 根据/etc/fstab提供的挂载信息, 进行文件系统的挂载
- busybox 会从 /etc/inittab中读取sysinit并执行, 这里sysinit指向了/etc/init.d/rcS
- /etc/init.d/rcS 中 ,mdev -s 这条命令很重要, 它会扫描/sys目录,查找字符设备和块设备,并在/dev下mknod
dev目录:
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/dev [1:17:36]
$ sudo mknod console c 5 1
这一步很重要, 没有console这个文件, 用户态的输出没法打印到串口上
lib目录:拷贝lib库,支持动态编译的应用程序运行:
# bryant @ ubuntu in ~/Downloads/busybox-1.33.1/_install/lib [1:18:43]
$ cp /usr/aarch64-linux-gnu/lib/*.so* -a .
编译内核
配置内核
linux内核源码可以在github上直接下载。
根据arch/arm64/configs/defconfig 文件生成.config
make defconfig ARCH=arm64
将下面的配置加入.config文件中
CONFIG_DEBUG_INFO=y
CONFIG_INITRAMFS_SOURCE="./root"
CONFIG_INITRAMFS_ROOT_UID=0
CONFIG_INITRAMFS_ROOT_GID=0
CONFIG_DEBUG_INFO是为了方便调试
CONFIG_INITRAMFS_SOURCE是指定kernel ramdisk的位置,这样指定之后ramdisk会直接被编译到kernel 镜像中。
我们将之前制作好的根文件系统cp到root目录下:
# bryant @ ubuntu in ~/Downloads/linux-arm64 on git:main x [1:26:56]
$ cp -r ../busybox-1.33.1/_install root
执行编译
make ARCH=arm64 Image -j8 CROSS_COMPILE=aarch64-linux-gnu-
这里指定target为Image 会只编译kernel, 不会编译modules, 这样会增加编译速度
启动qemu
下载qemu
需要注意的,qemu最好源码编译, 用apt-get直接安装的qemu可能版本过低,导致无法启动arm64内核。笔者是使用4.2.1版本的qemu
apt-get install build-essential zlib1g-dev pkg-config libglib2.0-dev binutils-dev libboost-all-dev autoconf libtool libssl-dev libpixman-1-dev libpython-dev python-pip python-capstone virtualenv
wget https://download.qemu.org/qemu-4.2.1.tar.xz
tar xvJf qemu-4.2.1.tar.xz
cd qemu-4.2.1
./configure --target-list=x86_64-softmmu,x86_64-linux-user,arm-softmmu,arm-linux-user,aarch64-softmmu,aarch64-linux-user --enable-kvm
make
sudo make install
编译完成之后,qemu在 /usr/local/bin目录下
$ /usr/local/bin/qemu-system-aarch64 --version
QEMU emulator version 4.2.1
Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developers
启动linux内核
/usr/local/bin/qemu-system-aarch64 -m 512M -smp 4 -cpu cortex-a57 -machine virt -kernel arch/arm64/boot/Image -append "rdinit=/linuxrc nokaslr console=ttyAMA0 loglevel=8" -nographic -s
这里对于参数做一些解释:
-m 512M
内存为512M
-smp 4
4核
-cpu cortex-a57
cpu 为cortex-a57
-kernel
kernel镜像文件
-append
传给kernel 的cmdline参数。其中rdinit指定了init进程;nokaslr 禁止内核起始地址随机化,这个很重要, 否则GDB调试可能有问题;console=ttyAMA0指定了串口,没有这一步就看不到linux的输出;
-nographic
禁止图形输出
-s
监听gdb端口, gdb程序可以通过1234这个端口连上来。
这里说明一下console=ttyAMA0是怎么生效的。
查看linux源码可知ttyAMA0对应的是AMBA_PL011
这个驱动:
config SERIAL_AMBA_PL011_CONSOLE
bool "Support for console on AMBA serial port"
depends on SERIAL_AMBA_PL011=y
select SERIAL_CORE_CONSOLE
select SERIAL_EARLYCON
help
Say Y here if you wish to use an AMBA PrimeCell UART as the system
console (the system console is the device which receives all kernel
messages and warnings and which allows logins in single user mode).
Even if you say Y here, the currently visible framebuffer console
(/dev/tty0) will still be used as the system console by default, but
you can alter that using a kernel command line option such as
"console=ttyAMA0". (Try "man bootparam" or see the documentation of
your boot loader (lilo or loadlin) about how to pass options to the
kernel at boot time.)
AMBA_PL011是arm的一个标准串口设备, qemu 的输出就是模拟的这个串口。
在qemu的源码文件中,也可以看到PL011的相关文件:
# bryant @ ubuntu in ~/Downloads/qemu-4.2.1 [1:46:54]
$ find . -name "*pl011*"
./hw/char/pl011.c
成功启动Linux后, 串口打印如下:
[ 3.401567] usbcore: registered new interface driver usbhid
[ 3.404445] usbhid: USB HID core driver
[ 3.425030] NET: Registered protocol family 17
[ 3.429743] 9pnet: Installing 9P2000 support
[ 3.435439] Key type dns_resolver registered
[ 3.440299] registered taskstats version 1
[ 3.443685] Loading compiled-in X.509 certificates
[ 3.461041] input: gpio-keys as /devices/platform/gpio-keys/input/input0
[ 3.473163] ALSA device list:
[ 3.474432] No soundcards found.
[ 3.485283] uart-pl011 9000000.pl011: no DMA platform data
[ 3.541376] Freeing unused kernel memory: 10752K
[ 3.545897] Run /linuxrc as init process
[ 3.548390] with arguments:
[ 3.550279] /linuxrc
[ 3.551073] nokaslr
[ 3.552216] with environment:
[ 3.554396] HOME=/
[ 3.555898] TERM=linux
[ 3.985835] 9pnet_virtio: no channels available for device kmod_mount
mount: mounting kmod_mount on /mnt failed: No such file or directory
/etc/init.d/rcS: line 8: can't create /proc/sys/kernel/hotplug: nonexistent directory
Please press Enter to activate this console.
[root@bryant ]#
[root@bryant ]#
VSCode+GDB
vscode中集成了GDB功能,我们可以用它来图形化的调试linux kernel
首先我们添加vscode的gdb配置文件(.vscode/launch.json):
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "kernel debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/vmlinux",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"miDebuggerPath":"/usr/bin/gdb-multiarch",
"miDebuggerServerAddress": "localhost:1234"
}
]
}
这里对几个重点参数做一些说明:
program
: 调试的符号文件
miDebuggerPath
:gdb的路径, 这里需要注意的是,由于我们是arm64内核,因此需要用gdb-multiarch来进行调试
miDebuggerServerAddress
:对端地址,qemu会默认使用1234这个端口
配置完成之后,可以直接启动GDB, 连接上linux kernel
在vscode中,可以设置断点,进行单步调试