你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

采用 Qemu + GDB 进行单步调试Linux 内核

[复制链接]
gaosmile 发布时间:2021-3-1 14:54
1. 概述
- f1 Q, T* I. \' m7 d6 |( ?7 T9 L* J; e8 N; x

在某些情况下,我们需要对于内核中的流程进行分析,虽然通过 BPF 的技术可以对于函数传入的参数和返回结果进行展示,但是在流程的调试上还是不如直接 GDB 单步调试来的直接。本文采用的编译方式如下,在一台 16 核 CentOS 7.7 的机器上进行内核源码相关的编译(主要是考虑编译效率),调试则是基于 VirtualBox 的 Ubuntu 20.04 系统中,采用 Qemu + GDB 进行单步调试,网上查看了很多文章,在最终进行单步跟踪的时候,始终不能够在断点处停止,进行过多次尝试和查询文档,最终发现需要在内核启动参数上添加 nokaslr ,本文是对整个搭建过程的总结。

微信图片_20210301145151.png
2. Linux 内核编译和文件系统制作
Linux 内核编译

编译内核和制作文件系统在 CentOS 7.7 的机器上。源码从国内清华的源下载:http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel/, 此处选择 linux-4.19.172.tar.gz 版本。详细编译步骤如下:

$ sudo yum group install "Development Tools"$ H+ L& T" X! E$ G( V, A
$ yum install ncurses-devel bison flex elfutils-libelf-devel openssl-devel
: j; g2 j0 S2 W' B) D% }, h3 ^3 x: K9 ^9 a0 O4 w
$ wget http://ftp.sjtu.edu.cn/sites/ftp ... nux-4.19.172.tar.gz
6 ]8 }: ?# l( s- g4 s. O1 D6 |$ tar xzvf linux-4.19.172.tar.gz7 I$ J) ]& n- i% l9 E" u; |' ]
$ cd linux-4.19.172/7 v3 j! X1 u+ y( A1 q' l) j+ A+ }
$ make menuconfig! m6 A2 ~  O7 {3 i9 K/ X+ F5 S5 L

在内核编译选项中,开启如下 “Compile the kernel with debug info”, 4.19.172 中默认已经选中:

Kernel hacking —> Compile-time checks and compiler options —> [ ] Compile the kernel with debug info

微信图片_20210301145155.png

以上配置完成后会在当前目录生成 .config 文件,我们可以使用 grep 进行验证:

# grep CONFIG_DEBUG_INFO .config0 K  f# c9 M0 P4 h& n( |
CONFIG_DEBUG_INFO=y8 C4 Q( h, S- ?) c

接着我们进行内核编译:

$ nproc       # 查看当前的系统核数
" z$ @2 a8 O; G# [/ l$ _$ make -j 12  # 或者采用 make bzImage 进行编译, -j N,表示使用多少核并行编译) n0 b7 j  V( O% o9 ]' T/ x
7 }) Z- ~- \) n% y
# 未压缩的内核文件,这个在 gdb 的时候需要加载,用于读取 symbol 符号信息,由于包含调试信息所以比较大
' F1 ], a# s3 M* ^" }! v. l$ ls -hl vmlinux
2 f# a& F' r: Y4 H9 S9 E. m-rwxr-xr-x 1 root root 449M Feb  3 14:46 vmlinux
9 t1 ]' H: i& H% s$ _0 l: `* O+ G. V% x; p" x" |
# 压缩后的镜像文件/ \6 @: Y3 U; y0 J
$ ls -hl ./arch/x86_64/boot/bzImage$ C  q( Q1 |2 f; Z" I! n9 X
lrwxrwxrwx 1 root root 22 Feb  3 14:47 ./arch/x86_64/boot/bzImage -> ../../x86/boot/bzImage
. w# g0 {/ h+ O, {8 i5 X. z0 |5 C" i' L7 M, f1 I
$ ls -hl ./arch/x86/boot/bzImage
$ R/ e) A& c$ B1 ~& A-rw-r--r-- 1 root root 7.6M Feb  3 14:47 ./arch/x86/boot/bzImage
( B% V4 H5 A2 V7 D* [3 K

不同发行版本下的内核的详细编译文档可以参考这里[1]。

启动内存文件系统制作# 首先安装静态依赖,否则会有报错,参见后续的排错章节2 h8 }8 T; Z4 _: x$ V0 S1 t* ]3 ?
$ yum install -y glibc-static.x86_64 -y
5 _5 L7 {$ s" n  `$ x; c" D5 z; z8 w( [
$ wget https://busybox.net/downloads/busybox-1.32.1.tar.bz2
' N5 Y$ ]& @. R1 I1 e! G7 A$ tar -xvf busybox-1.32.1.tar.bz2
' `# m1 O" _5 F) Z  i% i$ cd busybox-1.32.1/" y# e& P) p# p

2 l  J2 P( Z$ D$ make menuconfig8 r9 n( ]2 t5 s; {4 P: k
! A* S, h5 I0 i
# 安装完成后生成的相关文件会在 _install 目录下
! h& _; v9 _+ `# ~$ make && make install
" G0 ]. ~" T, I9 }& k2 |) o( `
! ~! Y' I: u! o) Z) ]# ?$ cd _install
/ O6 I0 d5 j2 }" {2 ]: }$ mkdir proc6 y  I0 n6 t9 ?4 T( F
$ mkdir sys
2 G; ^0 \3 S9 M2 `& F- G) p2 v5 e! J$ touch init
5 U! b* t. I3 H8 @! M* Y" }3 r" j2 r, s8 {9 a) u% W9 W9 z% h
#  init 内容见后续章节,为内核启动的初始化程序- F- K' h# C. o2 a+ r1 G2 ]0 }2 ?/ R
$ vim init
3 l1 D2 y; k0 i
9 g4 X: T( [3 X: t9 ]6 H# 必须设置成可执行文件* g+ _! v  L$ M% J( O6 {
$ chmod +x init
- I- ]  W1 I4 N1 y
" f) c* A  k6 U: K7 |$ find . | cpio -o --format=newc > ./rootfs.img9 q; P: }$ f4 g7 v; G* n9 ?9 z
cpio: File ./rootfs.img grew, 2758144 new bytes not copied4 k+ M# E3 e: X; \1 k4 e: M$ ^
10777 blocks/ k1 H5 ?$ \* F7 t& j) L
' d$ i' D6 {4 c; E
$ ls -hl rootfs.img1 \  a; A( Y" ~% Z$ Y9 ^2 ~. V5 N6 E( T
-rw-r--r-- 1 root root 5.3M Feb  2 11:23 rootfs.img
, [. P1 b, c7 D5 n

其中上述的 init 文件内容如下,打印启动日志和系统的整个启动过程花费的时间:

#!/bin/sh/ u+ d. t+ U: N3 y
echo "{==DBG==} INIT SCRIPT"
' y% T$ O) x% l/ lmkdir /tmp
) V+ g3 r; c0 W0 X' mmount -t proc none /proc  M! x  r' t; O( Y: [; i
mount -t sysfs none /sys
# v' x$ E& L9 S: Y4 m3 |$ x9 c% ?mount -t debugfs none /sys/kernel/debug
0 r" E/ L3 ]6 N: k+ }% |mount -t tmpfs none /tmp( k% Y! k- y+ l+ V7 W7 a$ H

' U$ C5 S6 V& o4 T) t( P7 B* M, Cmdev -s8 U& O. ?0 \" F' D- I6 i! K+ c) q- y
echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds": @; W$ N# ^1 b, ~5 T; ^. t
* u1 h  Z. ^* e- x
# normal user$ s1 W! O. I& C
setsid /bin/cttyhack setuidgid 1000 /bin/sh
9 M' J2 O) s8 A8 N0 N' T! [* y

到此为止我们已经编译了好了 Linux 内核(vmlinux 和 bzImage)和启动的内存文件系统(rootfs.img)。

错误排查

在编译过程中出现以下报错:

/bin/ld: cannot find -lcrypt
) C  s" ]  o/ L4 C/bin/ld: cannot find -lm+ ?: s5 i1 ?" K0 A& }- k% W9 B7 W
/bin/ld: cannot find -lresolv
- P3 Z, s4 c$ r, y$ a; W$ ^! P; q' O" \/bin/ld: cannot find -lrt- H: V6 w( J4 q7 ?# |8 _
collect2: error: ld returned 1 exit status8 K8 y$ C" x1 ~1 Y1 ]# K
Note: if build needs additional libraries, put them in CONFIG_EXTRA_LDLIBS.% m+ Z: [6 A8 J: n! \3 |! r6 ]- ]
Example: CONFIG_EXTRA_LDLIBS="pthread dl tirpc audit pam"6 i! U% H- ]3 I& ^" Q: T' g0 x

出错的原因是因为我们采用静态编译依赖的底层库没有安装,如果不清楚这些库有哪些 rpm 安装包提供,则可以通过 yum provides 命令查看,然后安装相关依赖包重新编译即可。

$ yum provides */libm.a( X) s7 o: o- U0 A+ _
// ...  J8 T: D  L% {; u2 g% l0 ^$ w; T/ B
glibc-static-2.17-317.el7.x86_64 : C library static libraries for -static linking.- z& z$ G2 u% u9 g7 Q
Repo        : base9 U. F9 J: }3 d4 x
Matched from:5 l0 [+ L. B& a% z, b8 Z
Filename    : /usr/lib64/libm.a6 X) R: b: l" j. V/ b' L
3. Qemu 启动内核

在上述步骤准备好以后,我们需要在调试的 Ubuntu 20.04 的系统中安装 Qemu 工具,其中调测的 Ubuntu 系统使用 VirtualBox 安装。

$ apt install qemu qemu-utils qemu-kvm virt-manager libvirt-daemon-system libvirt-clients bridge-utils
+ u8 V; c/ o/ \, b4 q1 U; f# o+ Y

把上述编译好的 vmlinux、bzImage、rootfs.img 和编译的源码拷贝到我们当前 Unbuntu 机器中。

拷贝 Linux 编译的源码主要是在 gdb 的调试过程中查看源码,其中 vmlinux 和 linux 源码处于相同的目录,本例中 vmlinux 位于 linux-4.19.172 源目录中。

$ qemu-system-x86_64 -kernel ./bzImage -initrd  ./rootfs.img -append "nokaslr console=ttyS0" -s -S -nographic. t8 b5 |; I, j. P4 p

使用上述命令启动调试,启动后会停止在界面处,并等待远程 gdb 进行调试,在使用 GDB 调试之前,可以先使用以下命令进程测试内核启动是否正常。

$ qemu-system-x86_64 -kernel ./bzImage -initrd  ./rootfs.img -append "nokaslr console=ttyS0" -nographic
* k8 r" d/ \  h+ b

其中命令行中各参数如下:

  • -kernel ./bzImage:指定启用的内核镜像;
  • -initrd ./rootfs.img:指定启动的内存文件系统;
  • -append "nokaslr console=ttyS0" :附加参数,其中 nokaslr 参数必须添加进来,防止内核起始地址随机化,这样会导致 gdb 断点不能命中;参数说明可以参见这里[2]。
  • -s :监听在 gdb 1234 端口;
  • -S :表示启动后就挂起,等待 gdb 连接;
  • -nographic:不启动图形界面,调试信息输出到终端与参数 console=ttyS0 组合使用;# Q6 V, t4 s* y) l0 O4 N
微信图片_20210301145158.png
4. GDB 调试

在使用 qemu-system-x86_64 命令启动内核以后,进入到我们从编译机器上拷贝过来的 Linux 内核源代码目录中,在另外一个终端我们来启动 gdb 命令:

[linux-4.19.172]$ gdb
  m  T" m) h% i8 X$ T(gdb) file vmlinux           # vmlinux 位于目录 linux-4.19.172 中
- c- b( f- J+ f3 j$ q* H, c& T(gdb) target remote :1234) ?( O. B3 k% E% r9 N& n
(gdb) break start_kernel     # 有些文档建议使用 hb 硬件断点,我在本地测试使用 break 也是 ok 的8 e7 O( }+ \/ d  T" O  g
(gdb) c             # 启动调试,则内核会停止在 start_kernel 函数处7 {; p9 R, {2 D

整体运行界面如下:

微信图片_20210301145201.png
5. Eclipse 图像化调试

我们可以通过 eclipse-cdt 进行可视化项目调试。

”File“ -> “New” -> “Project” ,然后选择 ”Makefile Project with Existing Code“ 选项,后续按照向导导入代码。

微信图片_20210301145203.png

在 “Run” -> “Debug Configurations” 选项中,创建一个 ”C/C++ Attach to Application“ 的调试选项。

  • Project:选择我们刚才创建的项目名字;
  • C/C++ Application:选择编译 Linux 内核带符号信息表的 vmlinux;
  • Build before launching:选择 ”Disable auto build“;
  • Debugger:选择 gdbserver,具体设置如下图;
  • 在 Debugger 中的 Connection 信息中选择 ”TCP“,并填写端口为 ”1234“;
    4 c9 X! c. g" I* h+ ~2 A

启动 Debug 调试,即可看到与 gdb 类似的窗口。

微信图片_20210301145206.png

启动 ”Debug“ 调试以后的窗口如下,在 Debug 窗口栏中,设置与 gdb 调试相同的步骤即可。

微信图片_20210301145209.png
8 s. J6 A. R% S1 i8 W7 A  |+ f$ ?) F  s1 Y. `* B
收藏 评论0 发布时间:2021-3-1 14:54

举报

0个回答

所属标签

相似分享

关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版