嵌入式Linux系统 GDB、GDBServer 调试环境搭建

嵌入式系统开发过程中调试方法主要有:

1.内核、驱动代码调试工具,valgrind、lttng、trace、kdump、kprobe、jprobe等,
2.linux系统上的App用户程序调试方法,GDB、app-log,还有些第三方捕获内核crash工具

当用户App程序涉及的应用复杂时,软件长时间运行才能够出现的bug、或内核crash情况,可以采用applog+第三方内核捕获工具的方法、来查找系统的故障现场;此部分内容待时间方便时、另行分享。此篇主要介绍GDBServer和GDB常用命令使用方法。

一、 GDB应用场景

嵌入式linux的目标主机性能相对较弱、直接在目标机上运行GDB调试有时会力不从心;这种情况一般选用目标机作为 GDBServer、Linux桌面版发布系统、如ubuntu、centos环境作为 GDBclient 方式,组合方式来进行调试。
一般桌面系统如Ubuntu、Centos等可以直接运行gdb + 目标可执行程序,直接gdb调试就可以,一般在pc机系统中调试应用程序的话、可以使用的ide环境更方便(如:QT、eclipse + GDB),也不需要使用gdb 命令行这种古老调试方法,因此这种调试命令先用现查就可以。
本篇文章开发环境:

目标主机:LS1046主板、LEDE-17 的路由操作系统;
pc 机 VMware-15: ubuntu18 桌面版系统

GDB 调试工具是主线Linux 内核具备功能,因此在 最小linux嵌入式平台、openWrt/LEDE 路由平台、ubuntu嵌入式平台都是通用方法,换句话说,只要是linux发布版的操作系统基本上都可以使用。

2.1 嵌入式主机的 GDBServer 环境搭建

  1. 在内核裁剪时需要把 GDBServer 模块编译进内核中,菜单路径 Development -> gdbserver,选择 ‘y’ ;保存退出;

  2. 选定要调试的app,修改源码目录下的Makefile 文件,修改 CXXFLAGS += -g 增加 -g 标示 打开 gdb调试;

  3. 编译内核与app软件,烧写只嵌入式主机中,即具备使用GDBserver调试环境。

本篇手工编写一个测试用例如下:

#include <stdio.h>

void debug(char *str)
{
    printf("debug info :%s\n",str );
}

main(int argc,char *argv[])
{
    int i,j;
    j=0;
    for(i=0;i<10;i++){
        j+=5;
        printf("now a=%d\n", j);
    }
}

手工编译、并打开 gdb 调试功能

arm-none-linux-gnueabi-gcc -g test.c -o test

生成a.out可执行文件, 拷贝到嵌入式主机。

2.2 配置虚拟主机的 网络环境

本实验环境采用的是 ubuntu18 桌面版,安装在 VMware-15的虚拟机中。
配置网络连接为桥接模式:

1.VMware菜单选择“编辑”–>“虚拟网络编辑器”;
2.启用管理权限,使用桥接模式、VMnet0桥接模式–>桥接到PC机的物理网卡上;

ubuntu 开机后,配置为dhcp自动获取ip的模式后,重新运行一下网络服务即可。获取IP地址信息如下:

robot@ubuntu:/etc$ ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.15.160  netmask 255.255.255.0  broadcast 192.168.15.255
        inet6 fe80::c8dd:6c50:25ef:4ea7  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:05:c2:d4  txqueuelen 1000  (Ethernet)
        RX packets 2194  bytes 2478926 (2.4 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1042  bytes 96661 (96.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

本机 ip地址是inet 192.168.15.160,目标主机ip 192.168.15.1 路由器,GDB server就运行在此路由器上。

2.3 运行 GDB server

由于gdb server 已经编译到内核中,直接运行:

// 通过3000端口都可以连接 gdb server 进行调试
$ gdbserver:3000 /home/a.out
// 指定 client ip 地址为 192.168.15.160 客户端
$ gdbserver 192.168.15.160:3000 /home/a.out

2.4 运行 GDB client

在 ubuntu 主机中运行交叉编译后 gdb 调试工具,OpenWRT 路由系统的编译后gdb路径
staging_dir/toolchain-aarch64_generic_gcc-5.5.0_musl/bin/ ,此路径是生成目标主机的交叉编译工具链。

a. 输入 target remote 192.168.15.1:3000

在这里插入代码片

b.输入file a.out

在这里插入代码片

c.启动程序执行

在这里插入代码片

单步调试

robot@ubuntu:~/test/gdb/simple$ arm-none-linux-gnueabi-gdb
GNU gdb (Sourcery CodeBench Lite 2012.03-57) 7.2.50.20100908-cvs
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
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 "--host=i686-pc-linux-gnu --target=arm-none-linux-gnueabi".
For bug reporting instructions, please see:
<https://support.codesourcery.com/GNUToolchain/>.
//连接 gdb server
(gdb) target remote 172.16.30.20:777
Remote debugging using 172.16.30.20:777
0xb6ed7ed0 in ?? ()

//file 装载调试文件
(gdb) file a.out 
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
Reading symbols from /home/fuzk/test/gdb/simple/a.out...done.
Cannot access memory at address 0x0
//运行程序
(gdb) b main
Cannot access memory at address 0x0
Note: breakpoints 1 and 2 also set at pc 0x846c.
Breakpoint 3 at 0x846c: file test.c, line 11.
//启动调试
(gdb) c
Continuing.

Breakpoint 1, main (argc=1, argv=0xbe9bbe54) at test.c:11
11          j=0;
(gdb) s  //单步运行
12          for(i=0;i<10;i++){
(gdb) s
13              j+=5;
(gdb) s
14              printf("now a=%d\n", j);
(gdb) n
12          for(i=0;i<10;i++){
(gdb) s
13              j+=5;
(gdb) s
14              printf("now a=%d\n", j);
(gdb) n
12          for(i=0;i<10;i++){
(gdb) n
13              j+=5;
(gdb) n
14              printf("now a=%d\n", j);
(gdb) n
12          for(i=0;i<10;i++){
(gdb) n
13              j+=5;
(gdb) n
14              printf("now a=%d\n", j);
(gdb)
(gdb) s12          for(i=0;i<10;i++){(gdb) s13              j+=5;(gdb) s  printf由于没有-g编译 调用step会导致异常, 所以建议使用next, 但如果用系统自带的会有上面红色提示 所以用step也不怕!14              printf("now a=%d\n", j);(gdb) s12          for(i=0;i<10;i++){(gdb) s13              j+=5;(gdb) s14              printf("now a=%d\n", j);

二、 GDB 调试常用命令

在这里插入图片描述
在调试应用程序时,比较好的方式是输出应用程序的log日志,可以更加log全面分析程序中各种异常和程序逻辑问题。

2.1 list 命令

在gdb中运行list命令(缩写l)可以列出代码,list的具体形式包括:
list ,显示程序第linenum行周围的源程序,显示函数,如:

//显示函数
(gdb) list main
4	{
5	    printf("debug info :%s\n",str );
6	}
7	
8	void main(int argc,char *argv[])
9	{
10	    int i,j;
11	    j=0;
12	    for(i=0;i<10;i++){
13	        j+=5;
//显示行
(gdb) list 10
5	    printf("debug info :%s\n",str );
6	}
7	
8	void main(int argc,char *argv[])
9	{
10	    int i,j;
11	    j=0;
12	    for(i=0;i<10;i++){
13	        j+=5;
14	        printf("now a=%d\n", j);
(gdb) 

2.2 run命令

在gdb中,运行程序使用run命令。在程序运行前,我们可以设置如下4方面的工作环境:

程序运行参数
set args 可指定运行时参数,如:set args 10 20 30 40 50;show args 命令可以查看设置好的运行参数。

运行环境
path

可设定程序的运行路径;how paths可查看程序的运行路径;set environment varname [=value]用于设置环境变量,如set env USER=baohua;
show environment [varname]则用于查看环境变量。

工作目录
cd

相当于shell的cd命令;pwd 显示当前所在的目录。

程序的输入输出
info terminal 用于显示程序用到的终端的模式;gdb中也可以使用重定向控制程序输出,如run > outfile;
tty命令可以指定输入输出的终端设备,如:tty /dev/ttyS1。

2.3 break命令

在gdb中用break命令来设置断点,设置断点的方法包括:

break
在进入指定函数时停住,C++中可以使用class::function或function(type, type)格式来指定函数名。

break
在指定行号停住。

break +offset / break -offset
在当前行号的前面或后面的offset行停住,offiset为自然数。

break filename:linenum
在源文件filename的linenum行处停住。

break filename:function
在源文件filename的function函数的入口处停住。

break *address
在程序运行的内存地址处停住。

break
break命令没有参数时,表示在下一条指令处停住。

break … if
“…”可以是上述的break 、break +offset / break –offset中的参数,condition表示条件,在条件成立时停住。比如在循环体中,可以设置break if i=100,表示当i为100时停住程序。

查看断点时,可使用info命令,如info breakpoints [n]、info break [n](n表示断点号)。

2.4 单步命令

在调试过程中,next命令用于单步执行,类似VC++中的step over。next的单步不会进入函数的内部,与next对应的step(缩写s)命令则在单步执行一个函数时,会进入其内部,类似VC++中的step into。下面演示了step命令的执行情况,在23行的add()函数调用处执行step会进入其内部的“return a+b;”语句:

(gdb) break 25
Breakpoint 1 at 0x8048362: file gdb_example.c, line 25.
(gdb) run
Starting program: /driver_study/gdb_example 
 
Breakpoint 1, main () at gdb_example.c:25
25          sum[i] = add(array1[i], array2[i]);
(gdb) step
add (a=48, b=85) at gdb_example.c:3
3         return a + b;

2.5 print命令

在调试程序时,当程序被停住时,可以使用print命令(缩写为p),或是同义命令inspect来查看当前程序的运行数据。print命令的格式是:

 print <expr>
 print /<f> <expr>

是表达式,是被调试的程序中的表达式,是输出的格式,比如,如果要把表达式按16进制的格式输出,那么就是/x。在表达式中,有几种GDB所支持的操作符,它们可以用在任何一种语言中,“@”是一个和数组有关的操作符,“::”指定一个在文件或是函数中的变量,“{} ”表示一个指向内存地址的类型为type的一个对象。

下面演示了查看sum[]数组的值的过程:

(gdb) print sum
$2 = {133, 155, 0, 0, 0, 0, 0, 0, 0, 0}
(gdb) next
 
Breakpoint 1, main () at gdb_example.c:25
25          sum[i] = add(array1[i], array2[i]);
(gdb) next
23        for (i = 0; i < 10; i++)
(gdb) print sum
$3 = {133, 155, 143, 0, 0, 0, 0, 0, 0, 0}

当需要查看一段连续内存空间的值的时间,可以使用GDB的“@”操作符,“@”的左边是第一个内存地址,“@”的右边则是想查看内存的长度。例如如下动态申请的内存:

int *array = (int *) malloc (len * sizeof (int));
*array = (int *) malloc (len * sizeof (int));

在GDB调试过程中这样显示出这个动态数组的值:

p *array@len
*array@len

print的输出格式包括:

x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。

我们可用display命令设置一些自动显示的变量,当程序停住时,或是单步跟踪时,这些变量会自动显示。 如果要修改变量,如x的值,可使用如下命令:

print x=4
x=4

2.6 watch命令 (查看变量的值)

watch一般来观察某个表达式(变量也是一种表达式)的值是否有变化了,如果有变化,马上停住程序。我们有下面的几种方法来设置观察点: watch :为表达式(变量)expr设置一个观察点。一量表达式值有变化时,马上停住程序。rwatch :当表达式(变量)expr被读时,停住程序。awatch :当表达式(变量)的值被读或被写时,停住程序。info watchpoints:列出当前所设置了的所有观察点。 下面演示了观察i并在连续运行next时一旦发现i变化,i值就会显示出来的过程:

(gdb) watch i
Hardware watchpoint 3: i
(gdb) next
23        for (i = 0; i < 10; i++)
(gdb) next
Hardware watchpoint 3: i
 
Old value = 0
New value = 1
0x0804838d in main () at gdb_example.c:23
23        for (i = 0; i < 10; i++)
(gdb) next
 
Breakpoint 1, main () at gdb_example.c:25
25          sum[i] = add(array1[i], array2[i]);
(gdb) next
23        for (i = 0; i < 10; i++)
(gdb) next
Hardware watchpoint 3: i
 
Old value = 1
New value = 2
0x0804838d in main () at gdb_example.c:23
23        for (i = 0; i < 10; i++)

2.7 examine命令 ( 查看 内存地址中的值)

我们可以使用examine命令(缩写为x)来查看内存地址中的值。examine命令的语法如下所示:

x/<n/f/u> <addr> 
/<n/f/u> <addr> 

表示一个内存地址。“x/”后的n、f、u都是可选的参数,n 是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容;f 表示显示的格式,如果地址所指的是字符串,那么格式可以是s,如果地址是指令地址,那么格式可以是i;u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4字节。u参数可以被一些字符代替:b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指定的内存地址开始,读写指定字节,并把其当作一个值取出来。n、f、u这3个参数可以一起使用,例如命令“x/3uh 0x54320”表示从内存地址0x54320开始以双字节为1个单位(h)、16进制方式(u)显示3个单位(3)的内存。
例子:

main()
{
        char *c = "hello world";
        printf("%s\n", c);
}

我们在 char *c = “hello world”; 下一行设置断点:

// 装载调试文件debug
robot@ubuntu:~/ixeos-dev/gdb-server$ gdb debug
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
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-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from debug...done.
(gdb) l
1	#include <stdio.h>
2	
3	void main(void)
4	{
5	        char *c = "hello world";
6	        printf("%s\n", c);
7	}
// 设置断点
(gdb) b 6
Breakpoint 1 at 0x400536: file debug.c, line 6.
//运行
(gdb) r
Starting program: /home/robot/ixeos-dev/gdb-server/debug 

Breakpoint 1, main () at debug.c:6
6	        printf("%s\n", c);
//显示变量内容
(gdb) p c
$1 = 0x4005d4 "hello world"

// 显示 内存地址内容
(gdb) x/s 0x4005d4
0x4005d4:	"hello world"
//修改变量值
(gdb) p *(char*)0x4005d4='H'
$2 = 72 'H'
(gdb) p c
$3 = 0x4005d4 "Hello world"

2.8 jump 命令

一般来说,被调试程序会按照程序代码的运行顺序依次执行,但是GDB也提供了乱序执行的功能,也就是说,GDB可以修改程序的执行顺序,从而让程序随意跳跃。这个功能可以由GDB的jump命令:jump 来指定下一条语句的运行点。可以是文件的行号,可以是file:line格式,也可以是+num这种偏移量格式,表示下一条运行语句从哪里开始。jump

这里的
是代码行的内存地址。 注意,jump命令不会改变当前的程序栈中的内容,所以,如果使用jump从一个函数跳转到另一个函数,当跳转到的函数运行完返回,进行出栈操作时必然会发生错误,这可能导致意想不到的结果,所以最好只用jump在同一个函数中进行跳转。

2.9 signal命令

使用singal命令,可以产生一个信号量给被调试的程序,如中断信号“Ctrl+C”。这非常方便于程序的调试,可以在程序运行的任意位置设置断点,并在该断点用GDB产生一个信号量,这种精确地在某处产生信号的方法非常有利于程序的调试。 signal命令的语法是:signal ,UNIX的系统信号量通常从1到15,所以取值也在这个范围。

2.10 help 命令

help 命令可以帮助我们使用不同的gdb功能模块,例如:

// call 使用说明
(gdb) help call
Call a function in the program.
The argument is the function name and arguments, in the notation of the
current working language.  The result is printed and saved in the value
history, if it is not void.
// print 使用说明
(gdb) help print
Print value of expression EXP.
Variables accessible are those of the lexical environment of the selected
stack frame, plus all those whose scope is global or an entire file.

$NUM gets previous value number NUM.  $ and $$ are the last two values.
$$NUM refers to NUM'th value back from the last one.
Names starting with $ refer to registers (with the values they would have
if the program were to return to the stack frame now selected, restoring
all registers saved by frames farther in) or else to debugger
"convenience" variables (any such name not a known register).
Use assignment expressions to give values to convenience variables.

{TYPE}ADREXP refers to a datum of data type TYPE, located at address ADREXP.
@ is a binary operator for treating consecutive data objects
anywhere in memory as an array.  FOO@NUM gives an array whose first
element is FOO, whose second element is stored in the space following
where FOO is stored, etc.  FOO must be an expression whose value
resides in memory.

EXP may be preceded with /FMT, where FMT is a format letter
but no count or size letter (see "x" command).

三、GDB调试core 文件

3.1 core文件

当程序运行过程中出现段错误(Segmentation Fault),程序将停止运行,由操作系统把程序当前的内存状况存储在一个 core 文件中,即核心转储文件(Coredump File),core 文件是程序运行状态的内存映象。

之所以将程序运行状态存为名为 core 的文件,因为 core 意指 core memory,用线圈做的内存。如今,半导体工业澎勃发展,已不再使用 core memory 了,不过,在许多情况下,人们还是把记忆体叫作 core 。

使用 gdb 调试 core 文件,可以帮助我们快速定位程序出现段错误的位置。当然,可执行程序编译时应加上 -g 编译选项,生成调试信息。

当程序访问非法内存会产生段错误,产生段错误的常见情况有:
(1)访问不存在的内存地址;
(2)访问系统保护的内存地址;
(3)数组访问越界等。

3.2 控制 core 文件是否生成

(1)使用 ulimit -c 命令可查看生成 core 文件的最大大小,若结果为 0,则表示不会生成 core文件。

( 2) 使用 ulimit -c FILESIZE 命令,可以限制 core 文件的大小(FILESIZE 单位为 KB)。如果生成的信息超过此大小,将会被截断,最终生成一个不完整的 core 文件。在调试此 core 文件的时候,gdb 会提示错误。比如:ulimit -c 1024。

(3)使用 ulimit -c unlimited,则表示 core 文件的大小不受限制。

在终端通过命令ulimit -c unlimited只是临时修改,重启后无效 ,要想永久修改有三种方式:
(1)在/etc/rc.local 中增加一行 ulimit -c unlimited;
(2)在/etc/profile 中增加一行 ulimit -c unlimited;
(3)在/etc/security/limits.conf 最后增加如下两行记录:

3.3 控制 core 文件的名称和生成路径

core 默认的文件名称是 core.pid,pid 指的是产生段错误的程序的进程号。默认路径是产生段错误的程序的当前目录。

如果想修改 core 文件的名称和生成路径,相关的配置文件为:

/proc/sys/kernel/core_uses_pid
	用于控制产生的 core 文件的文件名中是否添加 pid 作为扩展,如果添加则文件内容为 1,否则为 0

/proc/sys/kernel/core_pattern
	可以设置 core 文件保存的位置和名称。比如 echo "/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern,
所产生的 core 文件会存放到 /corefile 目录下,产生的文件名为:core-命令名-pid-时间戳

4.使用 gdb 调试 core 文件的步骤

使用 gdb 调试 core 文件来查找程序中出现段错误的位置时,要注意的是可执行程序在编译的时候需要加上 -g 编译命令选项。

gdb 调试 core 文件的步骤也比较简单,步骤如下:
(1)启动 gdb,同时指定程序与 core 文件。

gdb EXEFILE COREFILE
#或
gdb -c | --core COREFILE EXEFILE

#或
gdb COREFILE
file EXEFILE

(2)在进入 gdb 后,查找段错误位置,使用 where 或者bt。
在这里插入图片描述
可以定位到源程序中具体文件的具体位置,出现了段错误。

参考连接
https://openwrt.org/docs/guide-developer/gdb
https://zhuanlan.zhihu.com/p/95924162

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值