gdb是GNU debugger的缩写,是编程调试工具。
目前release的最新版本为8.0,GDB可以运行在Linux 和Windows 操作系统上。
gdb -v 检查是否安装成功,未安装成功则安装(必须确保编译器已经安装,如 gcc) 。
启动 gdb
gdb test_file.exe 来启动 gdb 调试, 即直接指定需要调试的可执行文件名
直接输入 gdb 启动,进入 gdb 之后采用命令 file test_file.exe 来指定文件名
如果目标执行文件要求出入参数(如 argv[] 接收参数),则可以通过三种方式指定参数:
运行程序
run(r)运行程序,如果要加参数,则是run arg1 arg2 ...
查看源代码
list(l):查看最近十行源码
list fun:查看fun函数源代码
list file:fun:查看flie文件中的fun函数源代码
设置断点与观察断点
break 行号/fun设置断点。
break file:行号/fun设置断点。
break if:条件成立时程序停住。
info break(缩写:i b):查看断点。
watch expr:一旦expr值发生改变,程序停住。
delete n:删除断点。
单步调试
continue(c):运行至下一个断点。
step(s):单步跟踪,进入函数,类似于VC中的step in。
next(n):单步跟踪,不进入函数,类似于VC中的step out。
finish:运行程序,知道当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
until:当厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序知道退出循环体。
查看运行时数据
print(p):查看运行时的变量以及表达式。
ptype:查看类型。
print array:打印数组所有元素。
print *array@len:查看动态内存。len是查看数组array的元素个数。
print x=5:改变运行时数据。
什么是段错误?段错误是由于访问非法地址而产生的错误。
gdb调试段错误,可以直接运行程序,当程序运行崩溃后,gdb会打印运行的信息,比如:收到了SIGSEGV信号,然后可以使用bt
命令,打印栈回溯信息,然后根据程序发生错误的代码,修改程序。
6.1 core文件
在程序崩溃时,一般会生成一个文件叫core
文件。core文件记录的是程序崩溃时的内存映像,并加入调试信息,core文件生成过程叫做core dump(核心已转储)
。系统默认不会生成该文件。
6.2 设置生成core文件
ulimit -c
:查看core-dump状态。ulimit -c xxxx
:设置core文件的大小。ulimit -c unlimited
:core文件无限制大小。6.3 gdb调试core文件
当设置完ulimit -c xxxx
后,再次运行程序发生段错误,此时就会生成一个core
文件,使用gdb core
调试core文件,使用bt
命令打印栈回溯信息。
list [函数名][行数]
(1)设置断点:
删除断点
(gdb) clear location:参数 location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时,表示删除位于该函数入口处的所有断点。
(gdb) delete [breakpoints] [num]:breakpoints 参数可有可无,num 参数为指定断点的编号,其可以是 delete 删除某一个断点,而非全部。
禁用断点
**disable [breakpoints] [num…]:**breakpoints 参数可有可无;num… 表示可以有多个参数,每个参数都为要禁用断点的编号。如果指定 num…,disable 命令会禁用指定编号的断点;反之若不设定 num…,则 disable 会禁用当前程序中所有的断点。
激活断点
break(b): 打的是普通断点,打断点有两种形式
(gdb) break location // b location,location 代表打断点的位置
(gdb) break … if cond // b … if cond,代表如果 cond 条件为true,则在 “…” 处打断点
通过借助 condition 命令为不同类型断点设置条件表达式,只有当条件表达式成立(值为 True)时,相应的断点才会触发从而使程序暂停运行。
tbreak: tbreak 命令可以看到是 break 命令的另一个版本,tbreak 和 break 命令的用法和功能都非常相似,唯一的不同在于,使用 tbreak 命令打的断点仅会作用 1 次,即使程序暂停之后,该断点就会自动消失。
rbreak: 和 break 和 tbreak 命令不同,rbreak 命令的作用对象是 C、C++ 程序中的函数,它会在指定函数的开头位置打断点。
(gdb) tbreak regex
tbreak 命令打的断点和 break 命令打断点的效果是一样的,会一直存在,不会自动消失。
watch: 此命令打的是观察断点,可以监控某个变量或者表达式的值。只有当被监控变量(表达式)的值发生改变,程序才会停止运行。
(gdb) watch cond
rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。
catch: 捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。
(2)观察断点:
(3)设置捕捉点:
catch + event 当event发生时,停住程序。
event可以是下面的内容:
(4)捕获信号:
handle + [argu] + signals
signals:是Linux/Unix定义的信号,SIGINT表示中断字符信号,也就是Ctrl+C的信号,SIGBUS表示硬件故障的信号;SIGCHLD表示子进程状态改变信号; SIGKILL表示终止程序运行的信号,等等。
argu:
(5)线程中断:
break [linespec] thread [threadno] [if …]
linespec 断点设置所在的源代码的行号。如: test.c:12表示文件为test.c中的第12行设置一个断点。
threadno 线程的ID。是GDB分配的,通过输入info threads来查看正在运行中程序的线程信息。
if … 设置中断条件。
查看信息:
(1)查看数据:
print variable 查看变量
print *array@len 查看数组(array是数组指针,len是需要数据长度)
可以通过添加参数来设置输出格式:
/ 按十六进制格式显示变量。
/d 按十进制格式显示变量。
/u 按十六进制格式显示无符号整型。
/o 按八进制格式显示变量。
/t 按二进制格式显示变量。
/a 按十六进制格式显示变量。
/c 按字符格式显示变量。
/f 按浮点数格式显示变量。
(2)查看内存
examine /n f u + 内存地址(指针变量)
如:x /10cw pFilePath (pFilePath为一个字符串指针,指针占4字节)
x 为examine命令的简写。
(3)查看栈信息
backtrace [-n][n]
run®
continue©
next(n)
step(s)
until(u)
(gdb) until location:参数 location 为某一行代码的行号
查看变量的值
print§
isplay
GDB handle 命令: 信号处理
→(gdb) handle signal mode其中,signal 参数表示要设定的目标信号,它通常为某个信号的全名(SIGINT)或者简称(去除‘SIG’后的部分,如 INT);如果要指定所有信号,可以用 all 表示。
mode 参数用于明确 GDB 处理该目标信息的方式,其值可以是如下几个:
可以在 gdb 模式下,通过 info signals 或者 info signals
GDB frame和backtrace命令:查看栈信息
(gdb) frame spec 该命令可以将 spec 参数指定的栈帧选定为当前栈帧。spec 参数的值,常用的指定方法有 3 种:
(gdb) info frame 我们可以查看当前栈帧中存储的信息
该命令会依次打印出当前栈帧的如下信息:
除此之外,还可以使用 info args
命令查看当前函数各个参数的值;使用 info locals
命令查看当前函数中各局部变量的值。
(gdb) backtrace [-full] [n] 用于打印当前调试环境中所有栈帧的信息
其中,用 [ ] 括起来的参数为可选项,它们的含义分别为:
GDB编辑和搜索源码
GDB edit命令:编辑文件
(gdb) edit [location]
(gdb) edit [filename] : [location]
GDB search命令:搜索文件
search
reverse-search
一般来说,GDB主要帮忙你完成下面四个方面的功能:
1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
3、当程序被停住时,可以检查此时你的程序中所发生的事。
4、动态的改变你程序的执行环境。
从上面看来,GDB和一般的调试工具没有什么两样,基本上也是完成这些功能,不过在细节上,你会发现GDB这个调试工具的强大,大家可能比较习惯了图形化的调试工具,但有时候,命令行的调试工具却有着图形化工具所不能完成的功能。让我们一一看来。
一个调试示例:
源程序:tst.c
1 #include
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i
编译生成执行文件:(Linux下)
hchen/test> cc -g tst.c -o tst
使用GDB调试:
hchen/test> gdb tst <---------- 启动GDB
GNU gdb 5.1.1
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-SUSE-linux"...
(gdb) l <-------------------- l命令相当于list,从第一行开始例出原码。
1 #include
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i
好了,有了以上的感性认识,还是让我们来系统地认识一下gdb吧。
基本gdb命令:
GDB常用命令 格式 含义 简写
list List [开始,结束] 列出文件的代码清单 l
prit Print 变量名 打印变量内容 p
break Break [行号或函数名] 设置断点 b
continue Continue [开始,结束] 继续运行 c
info Info 变量名 列出信息 i
next Next 下一行 n
step Step 进入函数(步入) S
display Display 变量名 显示参数
file File 文件名(可以是绝对路径和相对路径) 加载文件
run Run args 运行程序 r
下面是一个使用了上述命令的实战例子:
[[email protected] bufbomb]# gdb bufbomb
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-RedHat-linux-gnu".
For bug reporting instructions, please see:
...
Reading symbols from /root/Temp/bufbomb/bufbomb...done.
(gdb) b getbuf
Breakpoint 1 at 0x8048ad6
(gdb) run -t cdai
Starting program: /root/Temp/bufbomb/bufbomb -t cdai
Team: cdai
Cookie: 0x5e5ee04e
Breakpoint 1, 0x08048ad6 in getbuf ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.149.el6_6.4.i686
(gdb) bt
#0 0x08048ad6 in getbuf ()
#1 0x08048db2 in test ()
#2 0x08049085 in launch ()
#3 0x08049257 in main ()
(gdb) info frame 0
Stack frame at 0xffffb540:
eip = 0x8048ad6 in getbuf; saved eip 0x8048db2
called by frame at 0xffffb560
Arglist at 0xffffb538, args:
Locals at 0xffffb538, Previous frame's sp is 0xffffb540
Saved registers:
ebp at 0xffffb538, eip at 0xffffb53c
(gdb) info registers
eax 0xc 12
ecx 0xffffb548 -19128
edx 0xc8c340 13157184
ebx 0x0 0
esp 0xffffb510 0xffffb510
ebp 0xffffb538 0xffffb538
esi 0x804b018 134524952
edi 0xffffffff -1
eip 0x8048ad6 0x8048ad6
eflags 0x282 [ SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) x/10x $sp
0xffffb510: 0xf7ffc6b0 0x00000001 0x00000001 0xffffb564
0xffffb520: 0x08048448 0x0804a12c 0xffffb548 0x00c8aff4
0xffffb530: 0x0804b018 0xffffffff
(gdb) si
0x08048ad9 in getbuf ()
(gdb) si
0x08048adc in getbuf ()
(gdb) si
0x080489c0 in Gets ()
(gdb) n
Single stepping until exit from function Gets,
which has no line number information.
Type string:123
0x08048ae1 in getbuf ()
(gdb) si
0x08048ae2 in getbuf ()
(gdb) c
Continuing.
Dud: getbuf returned 0x1
Better luck next time
Program exited normally.
(gdb) quit
GDB 7.0后加入了Reversal Debugging功能。具体来说,比如我在getbuf()和main()上设置了断点,当启动程序时会停在main()函数的断点上。此时敲入record后continue到下一断点getbuf(),GDB就会记录从main()到getbuf()的运行时信息。现在用rn就可以逆向地从getbuf()调试到main()。就像《X战警:逆转未来》里一样,挺神奇吧!
这种方式适合从bug处反向去找引起bug的代码,实用性因情况而异。当然,它也是有局限性的。像程序假如有I/O输出等外部条件改变时,GDB是没法“逆转”的。
[[email protected] bufbomb]# gdb bufbomb
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
...
Reading symbols from /root/Temp/bufbomb/bufbomb...done.
(gdb) b getbuf
Breakpoint 1 at 0x8048ad6
(gdb) b main
Breakpoint 2 at 0x80490c6
(gdb) run -t cdai
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/Temp/bufbomb/bufbomb -t cdai
Breakpoint 2, 0x080490c6 in main ()
(gdb) record
(gdb) c
Continuing.
Team: cdai
Cookie: 0x5e5ee04e
Breakpoint 1, 0x08048ad6 in getbuf ()
(gdb) rn
Single stepping until exit from function getbuf,
which has no line number information.
0x08048dad in test ()
(gdb) rn
Single stepping until exit from function test,
which has no line number information.
0x08049080 in launch ()
(gdb) rn
Single stepping until exit from function launch,
which has no line number information.
0x08049252 in main ()
linux kernel是一个非常复杂的系统,初学者会很难入门。如果有一个方便的调试环境,学习效率至少能有5-10倍的提升。
为了学习linux内核,通常有这两个需要:
笔者使用VSCode+GDB+Qemu完成了这两个需求:
最终效果大致如下:
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
这里对这几个文件做一点说明:
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
这里对于参数做一些解释:
-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中,可以设置断点,进行单步调试
版权声明:本文为知乎博主「玩转Linux内核」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接: https://zhuanlan.zhihu.com/p/639365490