CTF-All-In-One/doc/4.1_linux_kernel_debug.md
firmianay a0ebe4a208 fix
2018-06-08 20:46:05 +08:00

314 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 4.1 Linux 内核调试
- [准备工作](#准备工作)
- [printk](#printk)
- [QEMU + gdb](#qemu-gdb)
- [kdb](#kdb)
- [参考资料](#参考资料)
## 准备工作
与用户态程序不同为了进行内核调试我们需要两台机器一台调试另一台被调试。在调试机上需要安装必要的调试器如GDB被调试机上运行着被调试的内核。
这里选择用 Ubuntu16.04 来展示,因为该发行版默认已经开启了内核调试支持:
```
$ cat /boot/config-4.13.0-38-generic | grep GDB
# CONFIG_CFG80211_INTERNAL_REGDB is not set
CONFIG_SERIAL_KGDB_NMI=y
CONFIG_GDB_SCRIPTS=y
CONFIG_HAVE_ARCH_KGDB=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
# CONFIG_KGDB_TESTS is not set
CONFIG_KGDB_LOW_LEVEL_TRAP=y
CONFIG_KGDB_KDB=y
```
#### 获取符号文件
下面我们来准备调试需要的符号文件。看一下该版本的 code name
```
$ lsb_release -c
Codename: xenial
```
然后在下面的目录下新建文件 `ddebs.list`其内容如下注意看情况修改Codename
```
$ cat /etc/apt/sources.list.d/ddebs.list
deb http://ddebs.ubuntu.com/ xenial main restricted universe multiverse
deb http://ddebs.ubuntu.com/ xenial-security main restricted universe multiverse
deb http://ddebs.ubuntu.com/ xenial-updates main restricted universe multiverse
deb http://ddebs.ubuntu.com/ xenial-proposed main restricted universe multiverse
```
`http://ddebs.ubuntu.com` 是 Ubuntu 的符号服务器。执行下面的命令添加密钥:
```
$ wget -O - http://ddebs.ubuntu.com/dbgsym-release-key.asc | sudo apt-key add -
```
然后就可以更新并下载符号文件了:
```
$ sudo apt-get update
$ uname -r
4.13.0-38-generic
$ sudo apt-get install linux-image-4.13.0-38-generic-dbgsym
```
完成后,符号文件将会放在下面的目录下:
```
$ file /usr/lib/debug/boot/vmlinux-4.13.0-38-generic
/usr/lib/debug/boot/vmlinux-4.13.0-38-generic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=f00f4b7ef0ab8fa738b6a9caee91b2cbe23fef97, not stripped
```
可以看到这是一个静态链接的可执行文件,使用 gdb 即可进行调试,例如这样:
```
$ gdb -q /usr/lib/debug/boot/vmlinux-4.13.0-38-generic
Reading symbols from /usr/lib/debug/boot/vmlinux-4.13.0-38-generic...done.
gdb-peda$ p init_uts_ns
$1 = {
kref = {
refcount = {
refs = {
counter = 0x2
}
}
},
name = {
sysname = "Linux", '\000' <repeats 59 times>,
nodename = "(none)", '\000' <repeats 58 times>,
release = "4.13.0-38-generic", '\000' <repeats 47 times>,
version = "#43~16.04.1-Ubuntu SMP Wed Mar 14 17:48:43 UTC 2018", '\000' <repeats 13 times>,
machine = "x86_64", '\000' <repeats 58 times>,
domainname = "(none)", '\000' <repeats 58 times>
},
user_ns = 0xffffffff822517a0 <init_user_ns>,
ucounts = 0x0 <irq_stack_union>,
ns = {
stashed = {
counter = 0x0
},
ops = 0xffffffff81e2cc80 <utsns_operations>,
inum = 0xeffffffe
}
}
```
#### 获取源文件
`/etc/apt/sources.list` 里的 `deb-src` 行都取消掉注释:
```
$ sed -i '/^#\sdeb-src /s/^#//' "/etc/apt/sources.list"
```
然后就可以更新并获取 Linux 内核源文件了:
```
$ sudo apt-get update
$ mkdir -p ~/kernel/source
$ cd ~/kernel/source
$ apt-get source $(dpkg-query '--showformat=${source:Package}=${source:Version}' --show linux-image-$(uname -r))
```
```
$ ls linux-hwe-4.13.0/
arch CREDITS debian.master firmware ipc lib net security tools zfs
block crypto Documentation fs Kbuild MAINTAINERS README snapcraft.yaml ubuntu
certs debian drivers include Kconfig Makefile samples sound usr
COPYING debian.hwe dropped.txt init kernel mm scripts spl
```
## printk
在用户态程序中,我们常常使用 `printf()` 来打印信息方便调试在内核中也可以这样做。内核v4.16.3)使用函数 `printk()` 来输出信息,在 `include/linux/kern_levels.h` 中定义了 8 个级别:
```c
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
```
用法是:
```c
printk(KERN_EMERG "hello world!\n"); // 中间没有逗号
```
而当前控制台的日志级别如下所示:
```
$ cat /proc/sys/kernel/printk
4 4 1 4
```
这 4 个数值在文件定义及默认值在如下所示:
```c
// kernel/printk/printk.c
int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
};
// include/linux/printk.h
/* printk's without a loglevel use this.. */
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT
/* We show everything that is MORE important than this.. */
#define CONSOLE_LOGLEVEL_MIN 1 /* Minimum loglevel we let people use */
/*
* Default used to be hard-coded at 7, we're now allowing it to be set from
* kernel config.
*/
#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT
#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])
```
虽然这些数值控制了当前控制台的日志级别,但使用虚拟文件 `/proc/kmsg` 或者命令 `dmesg` 总是可以查看所有的信息。
## QEMU + gdb
QEMU 是一款开源的虚拟机软件可以使用它模拟出一个完整的操作系统参考章节2.1.1)。这里我们介绍怎样使用 QEMU 和 gdb 进行内核调试,关于 Linux 内核的编译可以参考章节 1.5.9。
接下来我们需要借助 BusyBox 来创建用户空间:
```
$ wget -c http://busybox.net/downloads/busybox-1.28.3.tar.bz2
$ tar -xvjf busybox-1.28.3.tar.bz2
$ cd busybox-1.28.3/
```
生成默认配置文件并修改 `CONFIG_STATIC=y` 让它生成的是一个静态链接的 BusyBox这是因为 qemu 中没有动态链接库:
```
$ make defconfig
$ cat .config | grep "CONFIG_STATIC"
CONFIG_STATIC=y
```
编译安装后会出现在 `_install` 目录下:
```
$ make
$ sudo make install
$ ls _install
bin linuxrc sbin usr
```
接下来创建 initramfs 的目录结构:
```
$ mkdir initramfs
$ cd initramfs
$ cp ../_install/* -rf ./
$ mkdir dev proc sys
$ sudo cp -a /dev/null /dev/console /dev/tty /dev/tty2 /dev/tty3 /dev/tty4 dev/
$ rm linuxrc
$ vim init # 创建启动脚本
$ cat init
#!/bin/busybox sh
mount -t proc none /proc
mount -t sysfs none /sys
exec /sbin/init
```
最后把它们打包:
```
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
```
这样 initramfs 根文件系统就做好了其中包含了必要的设备驱动和工具boot loader 会加载 initramfs 到内存,然后内核将其挂载到根目录 `/`,并运行 `init` 脚本,挂载真正的磁盘根文件系统。
QEMU 启动!
```
$ qemu-system-x86_64 -s -S -kernel ~/kernelbuild/linux-4.16.3/arch/x86_64/boot/bzImage -initrd ~/kernelbuild/busybox-1.28.3/initramfs.cpio.gz -nographic -append "console=ttyS0"
```
- `-s``-gdb tcp::1234` 的缩写QEMU 监听在 TCP 端口 1234等待 gdb 的连接。
- `-S`:在启动时冻结 CPU等待 gdb 输入 c 时继续执行。
- `-kernel`:指定内核。
- `-initrd`:指定 initramfs。
- `nographic`:禁用图形输出并将串行 I/O 重定向到控制台。
- `-append "console=ttyS0`:所有内核输出到 ttyS0 串行控制台,并打印到终端。
在另一个终端里使用打开 gdb然后尝试在函数 `cmdline_proc_show()` 处下断点:
```
$ gdb -ex "target remote localhost:1234" ~/kernelbuild/linux-4.16.3/vmlinux
(gdb) b cmdline_proc_show
Breakpoint 1 at 0xffffffff8121ad70: file fs/proc/cmdline.c, line 9.
(gdb) c
Continuing.
Breakpoint 1, cmdline_proc_show (m=0xffff880006701b00, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9
9 seq_printf(m, "%s\n", saved_command_line);
```
可以看到,当我们在内核里执行 `cat /proc/cmdline` 时就被断下来了。
```
/ # id
uid=0 gid=0
/ # echo hello kernel!
hello kernel!
/ # cat /proc/cmdline
console=ttyS0
```
现在我们已经可以对内核代码进行单步调试了。对于内核模块我们同样可以进行调试但模块是动态加载的gdb 不会知道这些模块被加载到哪里,所以需要使用 `add-symbol-file` 命令来告诉它。
来看一个 helloworld 的例子,[源码](../src/others/4.1_linux_kernel_debug)
```c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int hello_init(void)
{
printk(KERN_ALERT "Hello module!\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye module!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple module.");
```
Makefile 如下:
```makefile
BUILDPATH := ~/kernelbuild/linux-4.16.3/
obj-m += hello.o
all:
make -C $(BUILDPATH) M=$(PWD) modules
clean:
make -C $(BUILDPATH) M=$(PWD) clean
```
编译模块并将 `.ko` 文件复制到 initramfs然后重新打包
```
$ make && cp hello.ko ~/kernelbuild/busybox-1.28.3/initramfs
$ cd ~/kernelbuild/busybox-1.28.3/initramfs
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
```
最后重新启动 QEMU 即可:
```
/ # insmod hello.ko
[ 7.887392] hello: loading out-of-tree module taints kernel.
[ 7.892630] Hello module!
/ # lsmod
hello 16384 0 - Live 0xffffffffa0000000 (O)
/ # rmmod hello.ko
[ 24.523830] Goodbye module!
```
三个命令分别用于载入、列出和卸载模块。
再回到 gdb 中,`add-symbol-file` 添加模块的 `.text`、`.data` 和 `.bss` 段的地址,这些地址在类似 `/sys/kernel/<module>/sections` 位置:
```
/ # cat /sys/module/hello/sections/.text
0x00000000fa16acc0
```
在这个例子中,只有 .text 段:
```
(gdb) add-symbol-file ~/kernelbuild/busybox-1.28.3/initramfs/hello.ko 0x00000000fa16acc0
```
然后就可以对该模块进行调试了。
## kdb
## 参考资料
- [KernelDebuggingTricks](https://wiki.ubuntu.com/Kernel/KernelDebuggingTricks)