掌握GDB调试工具,轻松排除bug!

一、什么是GDB

gdb是GNU debugger的缩写,是编程调试工具。

  • GDB官网: https://www.gnu.org/software/gdb/
  • GDB适用的编程语言: Ada / C / C++ / objective-c / Pascal 等。
  • GDB的工作方式: 本地调试和远程调试。

目前release的最新版本为8.0,GDB可以运行在Linux 和Windows 操作系统上。

1.1安装与启动GDB

  1. gdb -v 检查是否安装成功,未安装成功则安装(必须确保编译器已经安装,如 gcc) 。

  2. 启动 gdb

    1. gdb test_file.exe 来启动 gdb 调试, 即直接指定需要调试的可执行文件名

    2. 直接输入 gdb 启动,进入 gdb 之后采用命令 file test_file.exe 来指定文件名

    3. 如果目标执行文件要求出入参数(如 argv[] 接收参数),则可以通过三种方式指定参数:

      1. 在启动 gdb 时,gdb --args text_file.exe
      2. 在进入gdb 之后,运行 set args param_1
      3. 在 进入 gdb 调试以后,run param_1 或者 start para_1

1.2gdb的功能

  • 启动程序,可以按照用户自定义的要求随心所欲的运行程序。
  • 可让被调试的程序在用户所指定的调试断点处停住(断点可以是条件表达式)。
  • 当程序停住时,可以检查此时程序中所发生的事。比如,可以打印变量的值。
  • 动态改变变量程序的执行环境。

1.3gdb的使用

运行程序

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:改变运行时数据。

1.4程序错误

  • 编译错:编写程序的时候没有符合语言规范导致编译错误。比如:语法错误。
  • 运行时错误:编译器检查不出这种错误,但在运行时候可能会导致程序崩溃。比如:内存地址非法访问。
  • 逻辑错误:编译和运行都很顺利,但是程序没有干我们期望干的事情。

1.5gdb调试段错误

什么是段错误?段错误是由于访问非法地址而产生的错误。

  • 访问系统数据区,尤其是往系统保护的内存地址写数据。比如:访问地址为0的地址。
  • 内存越界(数组越界,变量类型不一致等)访问到不属于当前程序的内存区域。

gdb调试段错误,可以直接运行程序,当程序运行崩溃后,gdb会打印运行的信息,比如:收到了SIGSEGV信号,然后可以使用bt命令,打印栈回溯信息,然后根据程序发生错误的代码,修改程序。

1.6.core文件调试

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命令打印栈回溯信息。

二、GDB常用命令

  • 以下以 test_file.c 作为源程序例子的名字,test_file.exe 作为可执行文件例子的名字, 以param_1 作为参数的例子的名字。
  • (gdb) 表示是在 gdb 调试模式下运行
  • 一般常用的方法有两种,即打断点调试单步调试
  • list(l): 列出源代码
  • quit(q): 退出 gdb 调试模式
  • 进入 gdb 之后,输入 help 可以查看所有命令的使用说明

2.1查看源码

list [函数名][行数]

2.2打断点调试

(1)设置断点:

  • a、break + [源代码行号][源代码函数名][内存地址]
  • b、break … if condition …可以是上述任一参数,condition是条件。例如在循环体中可以设置break … if i = 100 来设置循环次数

删除断点

(gdb) clear location:参数 location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时,表示删除位于该函数入口处的所有断点。

(gdb) delete [breakpoints] [num]:breakpoints 参数可有可无,num 参数为指定断点的编号,其可以是 delete 删除某一个断点,而非全部。

禁用断点

**disable [breakpoints] [num…]:**breakpoints 参数可有可无;num… 表示可以有多个参数,每个参数都为要禁用断点的编号。如果指定 num…,disable 命令会禁用指定编号的断点;反之若不设定 num…,则 disable 会禁用当前程序中所有的断点。

激活断点

  1. enable [breakpoints] [num…]激活用 num… 参数指定的多个断点,如果不设定 num…,表示激活所有禁用的断点
  2. enable [breakpoints] once num… 临时激活以 num… 为编号的多个断点,但断点只能使用 1 次,之后会自动回到禁用状态
  3. enable [breakpoints] count num… 临时激活以 num… 为编号的多个断点,断点可以使用 count 次,之后进入禁用状态
  4. enable [breakpoints] delete num… 激活 num… 为编号的多个断点,但断点只能使用 1 次,之后会被永久删除。

break(b): 打的是普通断点,打断点有两种形式

(gdb) break location // b location,location 代表打断点的位置

img

(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

    • regex 代表一个正则表达式,会在匹配到的函数的内部的开头位置打断点
  • tbreak 命令打的断点和 break 命令打断点的效果是一样的,会一直存在,不会自动消失。

watch: 此命令打的是观察断点,可以监控某个变量或者表达式的值。只有当被监控变量(表达式)的值发生改变,程序才会停止运行。

  • (gdb) watch cond

    • cond 代表的就是要监控的变量或者表达式

rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;

awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。

catch: 捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。

(2)观察断点:

  • a、watch + [变量][表达式] 当变量或表达式值改变时即停住程序。
  • b、rwatch + [变量][表达式] 当变量或表达式被读时,停住程序。
  • c、awatch + [变量][表达式] 当变量或表达式被读或被写时,停住程序。

(3)设置捕捉点:

catch + event 当event发生时,停住程序。

event可以是下面的内容:

  • a、throw 一个C++抛出的异常。(throw为关键字)
  • b、catch 一个C++捕捉到的异常。(catch为关键字)
  • c、exec 调用系统调用exec时。(exec为关键字,目前此功能只在HP-UX下有用)
  • d、fork 调用系统调用fork时。(fork为关键字,目前此功能只在HP-UX下有用)
  • e、vfork 调用系统调用vfork时。(vfork为关键字,目前此功能只在HP-UX下有用)
  • f、load 或 load 载入共享库(动态链接库)时。(load为关键字,目前此功能只在HP-UX下有用)
  • g、unload 或 unload 卸载共享库(动态链接库)时。(unload为关键字,目前此功能只在HP-UX下有用)

(4)捕获信号:

handle + [argu] + signals

signals:是Linux/Unix定义的信号,SIGINT表示中断字符信号,也就是Ctrl+C的信号,SIGBUS表示硬件故障的信号;SIGCHLD表示子进程状态改变信号; SIGKILL表示终止程序运行的信号,等等。

argu:

  • nostop 当被调试的程序收到信号时,GDB不会停住程序的运行,但会打出消息告诉你收到这种信号。
  • stop 当被调试的程序收到信号时,GDB会停住你的程序。
  • print 当被调试的程序收到信号时,GDB会显示出一条信息。
  • noprint 当被调试的程序收到信号时,GDB不会告诉你收到信号的信息。
  • pass or noignore 当被调试的程序收到信号时,GDB不处理信号。这表示,GDB会把这个信号交给被调试程序会处理。
  • nopass or ignore 当被调试的程序收到信号时,GDB不会让被调试程序来处理这个信号。

(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 + 内存地址(指针变量)

  • n 表示显示内存长度
  • f 表示输出格式(见上)
  • u 表示字节数制定(b 单字节;h 双字节;w 四字节;g 八字节;默认为四字节)
  如:x /10cw pFilePath  (pFilePath为一个字符串指针,指针占4字节)
     x 为examine命令的简写。

(3)查看栈信息

backtrace [-n][n]

  • n 表示只打印栈顶上n层的栈信息。
  • -n 表示只打印栈底上n层的栈信息。
  • 不加参数,表示打印所有栈信息。

2.3单步调试

run®

continue©

next(n)

  • 命令格式: (gdb) next count:count 表示单步执行多少行代码,默认为 1 行
  • 其最大的特点是当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next 指令都会一步执行完。也就是说,对于调用的函数来说,next 命令只会将其视作一行代码

step(s)

  • (gdb) step count:参数 count 表示一次执行的行数,默认为 1 行。
  • 通常情况下,step 命令和 next 命令的功能相同,都是单步执行程序。不同之处在于,当 step 命令所执行的代码行中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。

until(u)

  • (gdb) until:不带参数的 until 命令,可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序

(gdb) until location:参数 location 为某一行代码的行号

查看变量的值

print§

  • p num_1:参数 num_1 用来代指要查看或者修改的目标变量或者表达式
  • 它的功能就是在 GDB 调试程序的过程中,输出或者修改指定变量或者表达式的值

isplay

  • (gdb) display expr
  • (gdb) display/fmt expr
  • expr 表示要查看的目标变量或表达式;参数 fmt 用于指定输出变量或表达式的格式

掌握GDB调试工具,轻松排除bug!_第1张图片

  • (gdb) undisplay num…
  • (gdb) delete display num…
  • 参数 num… 表示目标变量或表达式的编号,编号的个数可以是多个
  • (gdb) disable display num…
  • 禁用自动显示列表中处于激活状态下的变量或表达式
  • (gdb) enable display num…
  • 也可以激活当前处于禁用状态的变量或表达式
  • 和 print 命令一样,display 命令也用于调试阶段查看某个变量或表达式的值
  • 它们的区别是,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会

GDB handle 命令: 信号处理

→(gdb) handle signal mode其中,signal 参数表示要设定的目标信号,它通常为某个信号的全名(SIGINT)或者简称(去除‘SIG’后的部分,如 INT);如果要指定所有信号,可以用 all 表示。

mode 参数用于明确 GDB 处理该目标信息的方式,其值可以是如下几个:

  • ostop:当信号发生时,GDB 不会暂停程序,其可以继续执行,但会打印出一条提示信息,告诉我们信号已经发生;
  • stop:当信号发生时,GDB 会暂停程序执行。
  • noprint:当信号发生时,GDB 不会打印出任何提示信息;
  • print:当信号发生时,GDB 会打印出必要的提示信息;
  • nopass(或者 ignore):GDB 捕获目标信号的同时,不允许程序自行处理该信号;
  • pass(或者 noignore):GDB 调试在捕获目标信号的同时,也允许程序自动处理该信号。

可以在 gdb 模式下,通过 info signals 或者 info signals (例如 info signals SIGINT) 查看不同 signal 的信息。

GDB frame和backtrace命令:查看栈信息

(gdb) frame spec 该命令可以将 spec 参数指定的栈帧选定为当前栈帧。spec 参数的值,常用的指定方法有 3 种:

  1. 通过栈帧的编号指定。0 为当前被调用函数对应的栈帧号,最大编号的栈帧对应的函数通常就是 main() 主函数;
  2. 借助栈帧的地址指定。栈帧地址可以通过 info frame 命令(后续会讲)打印出的信息中看到;
  3. 通过函数的函数名指定。注意,如果是类似递归函数,其对应多个栈帧的话,通过此方法指定的是编号最小的那个栈帧。

(gdb) info frame 我们可以查看当前栈帧中存储的信息

该命令会依次打印出当前栈帧的如下信息:

  • 当前栈帧的编号,以及栈帧的地址;
  • 当前栈帧对应函数的存储地址,以及该函数被调用时的代码存储的地址
  • 当前函数的调用者,对应的栈帧的地址;
  • 编写此栈帧所用的编程语言;
  • 函数参数的存储地址以及值;
  • 函数中局部变量的存储地址;
  • 栈帧中存储的寄存器变量,例如指令寄存器(64位环境中用 rip 表示,32为环境中用 eip 表示)、堆栈基指针寄存器(64位环境用 rbp 表示,32位环境用 ebp 表示)等。

除此之外,还可以使用 info args 命令查看当前函数各个参数的值;使用 info locals 命令查看当前函数中各局部变量的值。

(gdb) backtrace [-full] [n] 用于打印当前调试环境中所有栈帧的信息

其中,用 [ ] 括起来的参数为可选项,它们的含义分别为:

  • n:一个整数值,当为正整数时,表示打印最里层的 n 个栈帧的信息;n 为负整数时,那么表示打印最外层 n 个栈帧的信息;
  • -full:打印栈帧信息的同时,打印出局部变量的值。

GDB编辑和搜索源码

GDB edit命令:编辑文件

  • (gdb) edit [location]

  • (gdb) edit [filename] : [location]

    • location 表示程序中的位置。这个命令表示激活文件的指定位置,然后进行编辑。
    • 如果遇到报错 “bash: /bin/ex: 没有那个文件或目录”, 因为 GDB 的默认编辑器是 ex , 则需要指定编辑器,如 export EDITOR=/usr/bin/vim or export EDITOR=/usr/bin/vi

GDB search命令:搜索文件

  • search

  • reverse-search

    • 第一项命令格式表示从当前行的开始向前搜索,后一项表示从当前行开始向后搜索。其中 regexp 就是正则表达式,正则表达式描述了一种字符串匹配的模式,可以用来检查一个串中是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串。很多的编程语言都支持使用正则表达式。

三、GDB调试程序用法

一般来说,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

四、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) 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

4.1逆向调试

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 ()

4.2VSCode+GDB+Qemu调试ARM64 linux内核

linux kernel是一个非常复杂的系统,初学者会很难入门。如果有一个方便的调试环境,学习效率至少能有5-10倍的提升。

为了学习linux内核,通常有这两个需要:

  1. 可以摆脱硬件,方便的编译和运行linux
  2. 可以使用图形化的工具来调试linux

笔者使用VSCode+GDB+Qemu完成了这两个需求:

  • qemu作为虚拟机,用来启动linux。
  • VSCode+GDB作为调试工具,用来图形化地DEBUG。

最终效果大致如下:

qemu运行界面:

掌握GDB调试工具,轻松排除bug!_第2张图片

vscode调试界面:

掌握GDB调试工具,轻松排除bug!_第3张图片

下面将一步一步介绍如何搭建上述环境。本文所有操作都在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

这里对这几个文件做一点说明:

  1. busybox 作为linuxrc启动后, 会读取/etc/profile, 这里面设置了一些环境变量和shell的属性
  2. 根据/etc/fstab提供的挂载信息, 进行文件系统的挂载
  3. busybox 会从 /etc/inittab中读取sysinit并执行, 这里sysinit指向了/etc/init.d/rcS
  4. /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

这里对于参数做一些解释:

  • -m 512M 内存为512M
  • -smp 4 4核
  • -cpu cortex-a57cpu 为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

掌握GDB调试工具,轻松排除bug!_第4张图片

在vscode中,可以设置断点,进行单步调试

掌握GDB调试工具,轻松排除bug!_第5张图片

掌握GDB调试工具,轻松排除bug!_第6张图片


版权声明:本文为知乎博主「玩转Linux内核」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接: https://zhuanlan.zhihu.com/p/639365490

你可能感兴趣的:(bug,Linux内核,内存管理,内存泄漏,嵌入式开发)