Linux内核0-使用QEMU和GDB调试Linux内核

简介: Linux内核0-使用QEMU和GDB调试Linux内核

(文章大部分转载于:https://consen.github.io/2018/01/17/debug-linux-kernel-with-qemu-and-gdb/

排查Linux内核Bug,研究内核机制,除了查看资料阅读源码,还可通过调试器,动态分析内核执行流程。

QEMU模拟器原生支持GDB调试器,这样可以很方便地使用GDB的强大功能对操作系统进行调试,如设置断点;单步执行;查看调用栈、查看寄存器、查看内存、查看变量;修改变量改变执行流程等。


编译调试版内核


对内核进行调试需要解析符号信息,所以得编译一个调试版内核。

$ cd linux-4.14
$ make menuconfig
$ make -j 20

这里需要开启内核参数CONFIG_DEBUG_INFOCONFIG_GDB_SCRIPTS。GDB提供了Python接口来扩展功能,内核基于Python接口实现了一系列辅助脚本,简化内核调试,开启CONFIG_GDB_SCRIPTS参数就可以使用了。

Kernel hacking  ---> 
    [*] Kernel debugging
    Compile-time checks and compiler options  --->
        [*] Compile the kernel with debug info
        [*]   Provide GDB scripts for kernel debugging


构建initramfs根文件系统

Linux系统启动阶段,boot loader加载完内核文件vmlinuz后,内核紧接着需要挂载磁盘根文件系统,但如果此时内核没有相应驱动,无法识别磁盘,就需要先加载驱动,而驱动又位于/lib/modules,得挂载根文件系统才能读取,这就陷入了一个两难境地,系统无法顺利启动。于是有了initramfs根文件系统,其中包含必要的设备驱动和工具,boot loader加载initramfs到内存中,内核会将其挂载到根目录/,然后运行/init脚本,挂载真正的磁盘根文件系统。

这里借助BusyBox构建极简initramfs,提供基本的用户态可执行程序。

编译BusyBox,配置CONFIG_STATIC参数,编译静态版BusyBox,编译好的可执行文件busybox不依赖动态链接库,可以独立运行,方便构建initramfs。

$ cd busybox-1.28.0
$ make menuconfig

选择配置项:

Settings  --->
    [*] Build static binary (no shared libs)

执行编译、安装:

$ make -j 20
$ make install

会安装在_install目录:

$ ls _install 
bin  linuxrc  sbin  usr

创建initramfs,其中包含BusyBox可执行程序、必要的设备文件、启动脚本init。这里没有内核模块,如果需要调试内核模块,可将需要的内核模块包含进来。init脚本只挂载了虚拟文件系统procfssysfs,没有挂载磁盘根文件系统,所有调试操作都在内存中进行,不会落磁盘。

$ mkdir initramfs
$ cd initramfs
$ cp ../_install/* -rf ./
$ mkdir dev proc sys
$ sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
$ rm linuxrc
$ vim init
$ chmod a+x init
$ ls
$ bin   dev  init  proc  sbin  sys   usr

init文件内容:

#!/bin/busybox sh         
mount -t proc none /proc  
mount -t sysfs none /sys  
exec /sbin/init

打包initramfs:

$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz


调试

$ cd busybox-1.28.0
$ qemu-system-i386 -s -kernel ./linux-4.4.203/arch/i386/boot/bzImage -initrd ./initramfs.cpio.gz -nographic -append "console=ttyS0"
  • -s-gdb tcp::1234缩写,监听1234端口,在GDB中可以通过target remote localhost:1234连接;
  • -kernel指定编译好的调试版内核;
  • -initrd指定制作的initramfs;
  • -nographic取消图形输出窗口,使QEMU成简单的命令行程序;
  • -append "console=ttyS0"将输出重定向到console,将会显示在标准输出stdio。

启动后的根目录, 就是initramfs中包含的内容:

/ # ls                    
bin   dev  init  proc  root  sbin  sys   usr

由于系统自带的GDB版本为7.2,内核辅助脚本无法使用,重新编译了一个新版GDB。我的系统比较新,所以gdb版本是7.11,所以不需要重新编译。

$ cd gdb-7.9.1
$ ./configure --with-python=$(which python2.7)
$ make -j 20
$ sudo make install

启动GDB:

$ cd linux-4.14
$ /usr/local/bin/gdb vmlinux
(gdb) target remote localhost:1234

使用内核提供的GDB辅助调试功能:

(gdb) apropos lx                                    
function lx_current -- Return current task          
function lx_module -- Find module by name and return the module variable                                 
function lx_per_cpu -- Return per-cpu variable      
function lx_task_by_pid -- Find Linux task by PID and return the task_struct variable                    
...(此处省略若干行)                      
lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules                             
lx-version --  Report the Linux Version of the current kernel
(gdb) lx-cmdline 
console=ttyS0

在函数cmdline_proc_show设置断点,虚拟机中运行cat /proc/cmdline命令即会触发。

(gdb) b cmdline_proc_show                           
Breakpoint 1 at 0xffffffff81298d99: file fs/proc/cmdline.c, line 9.                                      
(gdb) c                                             
Continuing.                                         
Breakpoint 1, cmdline_proc_show (m=0xffff880006695000, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9
9               seq_printf(m, "%s\n", saved_command_line);                                              
(gdb) bt
#0  cmdline_proc_show (m=0xffff880006695000, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9
#1  0xffffffff81247439 in seq_read (file=0xffff880006058b00, buf=<optimized out>, size=<optimized out>, ppos=<optimized out>) at fs/seq_file.c:234
......(此处省略)
(gdb) p saved_command_line
$2 = 0xffff880007e68980 "console=ttyS0"


获取当前进程

《深入理解Linux内核》第三版第三章–进程,讲到内核采用了一种精妙的设计来获取当前进程。

Linux把跟一个进程相关的thread_info和内核栈stack放在了同一内存区域,内核通过esp寄存器获得当前CPU上运行进程的内核栈栈底地址,该地址正好是thread_info地址,由于进程描述符指针task字段在thread_info结构体中偏移量为0,进而获得task。相关汇编指令如下:

movl $0xffffe000, %ecx      /* 内核栈大小为8K,屏蔽低13位有效位。
andl $esp, %ecx
movl (%ecx), p

指令运行后,p就获得当前CPU上运行进程的描述符指针。

然而在调试器中调了下,发现这种机制早已经被废弃掉了。thread_info结构体中只剩下一个字段flags,进程描述符字段task已经删除,无法通过thread_info获取进程描述符了。

而且进程的thread_info也不再位于进程内核栈底了,而是放在了进程描述符task_struct结构体中,见提交sched/core: Allow putting thread_info into task_struct和x86: Move thread_info into task_struct,这样也无法通过esp寄存器获取thread_info地址了。

(gdb) p $lx_current().thread_info
$5 = {flags = 2147483648}

thread_info这个变量好像没有了,打印结果显示没有这个成员

这样做是从安全角度考虑的,一方面可以防止esp寄存器泄露后进而泄露进程描述符指针,二是防止内核栈溢出覆盖thread_info

Linux内核从2.6引入了Per-CPU变量,获取当前指针也是通过Per-CPU变量实现的。

(gdb) p $lx_current().pid
$50 = 77
(gdb) p $lx_per_cpu("current_task").pid
$52 = 77


补充

在gdb中输入命令apropos lx,没有任何输出,说明无法调用python辅助函数。

(gdb) apropos lx

从stackoverflow网站上找到一篇文章gdb-lx-symbols-undefined-command,里边提到:

gdb -ex add-auto-load-safe-path /path/to/linux/kernel/source/root
Now the GDB scripts are automatically loaded, and lx-symbols is available.

但是,按照上面进行操作后,进入gdb调试画面后,提示:

To enable execution of this file add
    add-auto-load-safe-path /home/qemu2/qemu/linux-4.4.203/scripts/gdb/vmlinux-gdb.py
line to your configuration file "/home/qemu2/.gdbinit".
To completely disable this security protection add
    set auto-load safe-path /
line to your configuration file "/home/qemu2/.gdbinit".

上面的意思是,为了能够使能vmlinux-gdb.py的执行,需要添加

add-auto-load-safe-path /home/qemu2/qemu/linux-4.4.203/scripts/gdb/vmlinux-gdb.py

这行代码到我的配置文件/home/qemu2/.gdbinit中。但是,查看我的系统环境没有这个文件,于是自己新建了一个文件,并把上面的代码加入进入。但是在执行source ./.gdbinit命令时,提示add-auto-load-safe-path这个命令找不到,于是干脆把

set auto-load safe-path /

这行代码添加到配置文件/home/qemu2/.gdbinit中,再执行source ./.gdbinit命令,没有错误发生。

于是启动内核代码,然后在另一个命令行窗口中执行gdb调试,就像上面的操作一样,显示:

function lx_current -- Return current task
function lx_module -- Find module by name and return the module variable
function lx_per_cpu -- Return per-cpu variable
function lx_task_by_pid -- Find Linux task by PID and return the task_struct variable
function lx_thread_info -- Calculate Linux thread_info from task variable
lx-dmesg -- Print Linux kernel log buffer
lx-list-check -- Verify a list consistency
lx-lsmod -- List currently loaded modules
lx-ps -- Dump Linux tasks
lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules

至此,终于可以安心调试内核了。


参考:

  1. Tips for Linux Kernel Development
  2. How to Build A Custom Linux Kernel For Qemu
  3. Linux Kernel System Debugging
  4. Debugging kernel and modules via gdb
  5. BusyBox simplifies embedded Linux systems
  6. Custom Initramfs
  7. Per-CPU variables
  8. Linux kernel debugging with GDB: getting a task running on a CPU
  9. gdb-kernel-debugging


相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
5天前
|
Ubuntu Linux 开发者
Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
使用上述U-Boot命令配置并启动嵌入式设备。如果配置正确,设备将通过TFTP加载内核和设备树,并通过NFS挂载根文件系统。
33 15
|
10天前
|
存储 NoSQL Linux
linux之core文件如何查看和调试
通过设置和生成 core 文件,可以在程序崩溃时获取详细的调试信息。结合 GDB 等调试工具,可以深入分析 core 文件,找到程序崩溃的具体原因,并进行相应的修复。掌握这些调试技巧,对于提高程序的稳定性和可靠性具有重要意义。
53 6
|
25天前
|
运维 监控 Linux
BPF及Linux性能调试探索初探
BPF技术从最初的网络数据包过滤发展为强大的系统性能优化工具,无需修改内核代码即可实现实时监控、动态调整和精确分析。本文深入探讨BPF在Linux性能调试中的应用,介绍bpftune和BPF-tools等工具,并通过具体案例展示其优化效果。
47 14
|
1月前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
1月前
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
1月前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
1月前
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####
|
1月前
|
负载均衡 算法 Linux
深入探索Linux内核调度机制:公平与效率的平衡####
本文旨在剖析Linux操作系统内核中的进程调度机制,特别是其如何通过CFS(完全公平调度器)算法实现多任务环境下资源分配的公平性与系统响应速度之间的微妙平衡。不同于传统摘要的概览性质,本文摘要将直接聚焦于CFS的核心原理、设计目标及面临的挑战,为读者揭开Linux高效调度的秘密。 ####
37 3
|
2月前
|
负载均衡 算法 Linux
深入探索Linux内核调度器:公平与效率的平衡####
本文通过剖析Linux内核调度器的工作机制,揭示了其在多任务处理环境中如何实现时间片轮转、优先级调整及完全公平调度算法(CFS),以达到既公平又高效地分配CPU资源的目标。通过对比FIFO和RR等传统调度策略,本文展示了Linux调度器如何在复杂的计算场景下优化性能,为系统设计师和开发者提供了宝贵的设计思路。 ####
43 6
|
1月前
|
消息中间件 安全 Linux
深入探索Linux操作系统的内核机制
本文旨在为读者提供一个关于Linux操作系统内核机制的全面解析。通过探讨Linux内核的设计哲学、核心组件、以及其如何高效地管理硬件资源和系统操作,本文揭示了Linux之所以成为众多开发者和组织首选操作系统的原因。不同于常规摘要,此处我们不涉及具体代码或技术细节,而是从宏观的角度审视Linux内核的架构和功能,为对Linux感兴趣的读者提供一个高层次的理解框架。