对于任何系统来说,调试都是必不可少的内容,当然在Linux设备驱动程序中也不例外,下面就是介绍Linux调试的一些方法以及工具。
Linux调试器GDB
GDB调试器的介绍和基本用法
GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具,GDB主要可帮助工程师完成下面4个方面的功能:
- 启动程序,可以按照工程师自定义的要求运行程序。
- 让被调试的程序在工程师指定的断点处停住,断点可以是条件表达式。
- 当程序被停住时,可以检查此时程序中所发生的事,并追踪上文。
- 动态地改变程序的执行环境。
调试Linux内核空间的驱动还是调试用户空间的应用程序时使用的GDB命令是完全相同的。
GDB调试的用法:
启动程序准备调试,在Linux终端输入gdb yourpram
或者先输入gdb
,然后输入 file yourpram
然后使用run或者r命令开始程序的执行,也可以使用run parameter
将参数传递给该程序,可用的参数以及相关作用如下表:
命令 | 命令缩写 | 命令说明 |
---|---|---|
list | l | 显示多行源代码 |
break | b | 设置断点,程序运行到断点的位置会停下来 |
info | u | 描述程序的状态 |
run | r | 开始运行程序 |
display | disp | 跟踪查看某个变量,每次停下来都显示它的值 |
step | s | 执行下一条语句,如果该语句为函数调用,则进入函数执行其中的第一条语句 |
next | n | 执行下一条语句,如果该语句为函数调用,不会进入函数内部执行(即不会一步步地调试函数内部语句) |
prite | p | 打印内部变量值 |
continue | c | 继续程序的运行,直到遇到下一个断点 |
set var name=v | 设置变量的值 | |
start | st | 开始执行程序,在main函数的第一条语句前面停下来 |
file | 装入需要调试的程序 | |
kill | k | 终止正在调试的程序 |
watch | 监视变量值的变化 | |
backtrace | bt | 查看看函数调用信息(堆栈) |
frame | f | 查看栈帧 |
quit | q | 退出GDB环境 |
gdb使用的一些参数
下面介绍一些比较重要的参数用法:
-
list命令
在GDB中运行list命令(缩写l)可以列出代码,list的具体形式如下:
① list<linenum>,显示程序第linenum行周围的源程序
② list<function>,显示函数名为function的函数的源程序
③ list,显示当前行后面的源程序
④ list-,显示当前行前面的源程序 -
run命令
**在GDB中,运行程序使用run命令。**在程序运行前,我们可以设置以下四方面的工作环境:
① 程序运行参数
用set args
可指定运行时参数,如set args 10 20 30 40 50
;用show args
命令可以查看设置好的运行参数。
② 运行环境
用path\<dir\>
可设定程序的运行路径;用how paths
可查看程序的运行路径;用set environment varname[=value]
可设置环境变量,如set env USER=baohua
;用show environment[varname]
则可查看环境变量。
③ 工作目录
cd<dir>相当于shell的cd命令,pwd可显示当前所在的目录。
④ 程序的输入输出
info terminal
用于显示程序用到的终端的模式;在GDB中也可以使用重定向控制程序输出,如run>outfile
;用tty命令可以指定输入输出的终端设备,如tty/dev/ttyS1
-
Break命令
在GDB中用break命令来设置断点,设置断点的方法如下:
① break<function>
在进入指定函数时停住,在C++中可以使用class::function
或function(type,type)
格式来指定函数名。
② break<linenum>
在指定行号停住。
③ break+offset/break-offset
在当前行号的前面或后面的offset行停住,offiset为自然数。
④ break filename:linenum
在源文件filename的linenum行处停住。
⑤ break filename:function
在源文件filename的function函数的入口处停住。
⑥ break*address
在程序运行的内存地址处停住。
⑦ break
break命令没有参数时,表示在下一条指令处停住。
⑧ break…if<condition>
…可以是上述的break<linenum>
、break+offset/break–offset
中的参数,condition表示条件,在条件成立时停住。
拓展:查看断点时,可使用info命令,如info breakpoints[n]、info break[n](n表示断点号)。 -
单步命令
**在调试过程中,next命令用于单步执行,next的单步不会进入函数的内部,与next对应的step(缩写为s)命令则在单步执行一个函数时,进入其内部。**单步执行的复杂用法:
① step<count>
单步跟踪,如果有函数调用,则进入该函数(进入函数的前提是,此函数被编译有debug信息)。step后面不加count表示一条条地执行,加count表示执行后面的count条指令,然后再停住。
② next<count>
单步跟踪,如果有函数调用,它不会进入该函数。同理,next后面不加count表示一条条地执行,加count表示执行后面的count条指令,然后再停住。
③ set step-mode
set step-mode on
用于打开step-mode模式,这样,在进行单步跟踪(运行step指令)时,若跨越某没有调试信息的函数,程序的执行则会在该函数的第一条指令处停住,而不会跳过整个函数。这样我们可以查看该函数的机器指令。
④ finish
运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址、返回值及参数值等信息。
⑤ until(缩写为u)
一直在循环体内执行单步而退不出来是一件令人烦恼的事情,用until命令可以运行程序直到退出循环体。
⑥ stepi(缩写为si)和nexti(缩写为ni)
stepi和nexti用于单步跟踪一条机器指令。比如,一条C程序代码有可能由数条机器指令完成,stepi和nexti可以单步执行机器指令,相反,step和next是C语言级别的命令。 -
Info命令
info命令可以用来在调试时查看寄存器、断点、观察点和信号等信息。
① 要查看寄存器的值,可以使用如下命令:
info registers
(查看除了浮点寄存器以外的寄存器)
info all-registers
(查看所有寄存器,包括浮点寄存器)
info registers <regname ...>
(查看所指定的寄存器)
② 要查看断点信息,可以使用如下命令:
info break
③ 要列出当前所设置的所有观察点,可使用如下命令:
info watchpoints
④ 要查看有哪些信号正在被GDB检测,可使用如下命令:
info signals
info handle
可以使用info line命令来查看源代码在内存中的地址。info line后面可以跟行号、函数名、文件名:行号、文件名:函数名等多种形式,例如用下面的命令会打印出所指定的源码在运行时的内存地址:
info line tst.c:func
-
continue命令
当程序被停住后,可以使用continue命令(缩写为c,fg命令同continue命令)恢复程序的运行直到程序结束,或到达下一个断点,命令格式为:
continue [ignore-count]
c [ignore-count]
fg [ignore-count]
ignore-count表示忽略其后多少次断点。 -
call命令
call命令用于强制调用某函数:
call \<expr\>
表达式可以是函数,以此达到强制调用函数的目的,它会显示函数的返回值(如果函数返回值不是void) -
print命令
在调试程序时,当程序被停住时,可以使用print命令(缩写为p),或是同义命令inspect来查看当前程序的运行数据。print命令的格式如下:
print \<expr\>
print /\<f\> \<expr\>
<expr>是表达式,也是被调试的程序中的表达式,<f>是输出的格式,比如,如果要把表达式按十六进制的格式输出,那么就是/x。
在表达式中,有几种GDB所支持的操作符,它们可以用在任何一种语言中:@是一个和数组有关的操作符,::指定一个在文件或是函数中的变量,{<type>}<addr>表示一个指向内存地址<addr>的类型为type的对象。
DDD图形界面调试工具
GDB本身是一种命令行调试工具,但是通过DDD(Data Display Debugger)可以被图形界面化。DDD可以作为GDB、DBX、WDB、Ladebug、JDB、XDB、Perl Debugger或Python Debugger的可视化图形前端,其特有的图形数据显示功能(GraphicalData Display)可以把数据结构按照图形的方式显示出来。
DDD的功能非常强大,具体有:
① 可以调试用C/C++、Ada、Fortran、Pascal、Modula-2和Modula-3编写的程序;
② 能以超文本方式浏览源代码;能够进行断点设置、回溯调试和历史记录;
③ 具有程序在终端运行的仿真窗口,具备在远程主机上进行调试的能力;
④ 能够显示各种数据结构之间的关系,并将数据结构以图形形式显示;
⑤ 具有GDB/DBX/XDB的命令行界面,包括完整的文本编辑、历史纪录、搜寻引擎等。
注意:
- 在DDD中,GDB是作为独立的进程运行的,通过命令行接口与DDD进行交互。
- DDD和GDB之间的所有通信都是异步进行的。在DDD中发出的GDB命令都会与一个回调函数相连,放入命令队列中。这个回调函数在合适的时间会处理GDB的输出。
- DDD在事件循环时等待用户输入和GDB输出,同时等着GDB进入等待输入状态。当GDB可用时,下一条命令就会从命令队列中取出,送给GDB。GDB到达的输出由上次命令的回调函数过程来处理。这种异步机制避免了DDD在等待GDB输出时发生阻塞现象,到达的事件可以在任何时间得到处理。
Linux的内核调试方法
在嵌入式系统中,由于目标机资源有限,因此往往在主机上先编译好程序,再在目标机上运行。用户所有的开发工作都在主机开发环境下完成,包括编码、编译、连接、下载和调试等。目标机和主机通过串口、以太网、仿真器或其他通信手段通信,主机用这些接口控制目标机,调试目标机上的程序。
调试嵌入式Linux内核的方法主要有:
- 目标机“插桩”,如打上KGDB补丁,这样主机上的GDB可与目标机的KGDB通过串口或网口通信。
- 使用仿真器,仿真器可直接连接目标机的JTAG/BDM,这样主机的GDB就可以通过与仿真器的通信来控制目标机。
- 在目标板上通过printk()、Oops、strace等软件方法进行“观察”调试,这些方法不具备查看和修改数据结构、断点、单步等功能。
不管是目标机“插桩”还是使用仿真器连接目标机JTAG/SWD/BDM,在主机上,调试工具一般都采用GDB。
尽管采用“插桩”和仿真器结合GDB的方式可以查看和修改数据结构、断点、单步等,而printk()这种最原始的方法却应用得更广泛。printk()这种方法很原始,但是一般可以解决工程中95%以上的问题。
内核打印信息——prink()
在Linux中,内核打印语句printk()会将内核信息输出到内核信息缓冲区中,内核缓冲区是在kernel/printk.c中通过如下语句静态定义的:
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
内核信息缓冲区是一个环形缓冲区(Ring Buffer),因此,如果塞入的消息过多,则就会将之前的消息冲刷掉。
printk()定义了8个消息级别,分为级别0~7,级别越低(数值越大),消息越不重要,第0级是紧急事件级,第7级是调试级,下面是printk()的级别定义:
#define KERN_EMERG "<0>" /* 紧急事件,一般是系统崩溃之前提示的消息 */
#define KERN_ALERT "<1>" /* 必须立即采取行动 */
#define KERN_CRIT "<2>" /* 临界状态,通常涉及严重的硬件或软件操作失败 */
#define KERN_ERR "<3>" /* 用于报告错误状态,设备驱动程序会经常使用KERN_ERR来报告来自硬件的问题 */
#define KERN_WARNING "<4>" /* 对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重的问题 */
#define KERN_NOTICE "<5>" /* 有必要进行提示的正常情形,许多与安全相关的状况用这个级别进行汇报 */
#define KERN_INFO "<6>" /* 内核提示性信息,很多驱动程序在启动的时候,用这个级别打印出它们找到的硬件信息 */
#define KERN_DEBUG "<7>" /* 用于调试信息 */
通过/proc/sys/kernel/printk文件可以调节printk()的输出等级,该文件有4个数字值:
- 控制台(一般是串口)日志级别:当前的打印级别,优先级高于该值的消息将被打印至控制台。
- 默认的消息日志级别:将用该优先级来打印没有优先级前缀的消息,也就是在直接写printk(“xxx”)而不带打印级别的情况下,会使用该打印级别。
- 最低的控制台日志级别:控制台日志级别可被设置的最小值(一般都是1)。
- 默认的控制台日志级别:控制台日志级别的默认值。
在设备驱动中,经常需要输出调试或系统信息,尽管可以直接采用printk(“<7>debug info…\n”)方式的printk()语句输出,但是通常可以使用封装了printk()的更高级的宏,如pr_debug()、dev_debug()等。
使用“/proc”
在Linux系统中,“/proc”文件系统十分有用,它被内核用于向用户导出信息。
“/proc”文件系统是一个虚拟文件系统,通过它可以在Linux内核空间和用户空间之间进行通信。
在/proc文件系统中,我们可以将对虚拟文件的读写作为与内核中实体进行通信的一种手段,与普通文件不同的是,这些虚拟文件的内容都是动态创建的。
“/proc”下的绝大多数文件是只读的,以显示内核信息为主。但是“/proc”下的文件也并不是完全只读的,若节点可写,还可用于一定的控制或配置目的,例如前面介绍的写/proc/sys/kernel/printk可以改变printk()的打印级别。
Linux系统的许多命令本身都是通过分析“/proc”下的文件来完成的,如ps、top、uptime和free等。通过分析相关命令打印出来的信息来确定程序运行的情况以及定位错误。
Oops
当内核出现类似用户空间的Segmentation Fault时(例如内核访问一个并不存在的虚拟地址),Oops会被打印到控制台和写入内核log缓冲区。
通过打开对应的设备可以得到Oops信息,通过反汇编可以分析出相关的问题所在。
BUG_ON()和WARN_ON()
内核中有许多地方调用类似BUG()的语句,它非常像一个内核运行时的断言,意味着本来不该执行到BUG()这条语句,一旦执行即抛出Oops。BUG()的定义为:
#define BUG() do { \
printk("BUG: failure at %s:%d/%s()!\n"
, __FILE__, __LINE__, __func__); \
panic("BUG!"); \
} while (0)
其中的panic()定义在kernel/panic.c中,会导致内核崩溃,并打印Oops。
BUG()还有一个变体叫BUG_ON(),它的内部会引用BUG(),形式为:
#define BUG_ON(condition)
do
{
if (unlikely(condition))
BUG();
} while (0)
对于BUG_ON()而言,只有当括号内的条件成立的时候,才抛出Oops。
除了BUG_ON()外,内核有个稍微弱一些WARN_ON(),在括号中的条件成立的时候,内核会抛出栈回溯,但是不会panic(),这通常用于内核抛出一个警告,暗示某种不太合理的事情发生了。
利用仿真器调试内核
在ARM Linux领域,目前比较主流的是采用ARM DS-5Development Studio方案。ARM DS-5是一个针对基于Linux的系统和裸机嵌入式系统的专业软件开发解决方案,它涵盖了开发的所有阶段,从启动代码、内核移植直到应用程序调试、分析。
调试主机一般通过网线与DSTREAM仿真器连接,而仿真器则连接与电路板类似的JTAG接口,之后用DS-5调试器进行调试。DS-5图形化调试器提供了全面和直观的调试图,非常易于调试Linux和裸机程序,易于查看代码,进行栈回溯,查看内存、寄存器、表达式、变量,分析内核线程,设置断点。
应用程序调试
在嵌入式系统中,为调试Linux应用程序,可在目标板上先运行GDBServer,再让主机上的GDB与目标板上的GDBServer通过网口或串口通信。在目标板子上和主机上的操作分别为:
① 在开发板上:
需要运行如下命令启动GDBServer:
gdbserver \<host_ip\>:\<port\> \<app\>
<host_ip>:<port>为主机的IP地址和端口,app是可执行的应用程序名。
当然,也可以用系统中空闲的串口作为GDB调试器和GDBServer的底层通信手段,如:
gdbserver/dev/ttyS0./tdemo
② 在主机上:
需要先运行如下命令启动GDB:
arm-eabi-gdb <app>
app与GDBServer的app参数对应。
之后,运行如下命令就可以连接目标板:
target remote <target_ip>:<port>
<target_ip>:<port>为目标机的IP地址和端口。
如果目标板上的GDBServer使用串口,则在宿主机上GDB也应该使用串口,如:
(gdb)target remote/dev/ttyS1
之后,便可以使用GDB像调试本机上的程序一样调试目标机上的程序。
比如在ARM开发板上放置GDB server,便可以通过目标板与调试PC之间的以太网等调试。比如要调试的应用程序的源代码如下:
void increase_one(int *data)
{
*data = *data + 1;
}
int main(int argc, char *argv[])
{
int dat = 0;
int *p = 0;
increase_one(&dat);
/* program will crash here */
increase_one(p);
return 0;
}
通过debug方式编译它:arm-linux-gnueabi-gcc -g -o gdb_example gdb_example.c
将程序下载到目标板后,在目标板上运行:
\# gdbserver 192.168.1.20:1234 gdb_example
其中192.168.1.20为目标板的IP,1234为GDBserver的侦听端口。
如果目标机是Android系统,且没有以太网,可以尝试使用adb forward功能,比如adb forward tcp:1234tcp:1234 是把目标机1234端口与主机1234端口进行转发。
在主机上运行:
$ arm-eabi-gdb gdb_example…
主机的GDB中运行如下命令以连接目标板:
(gdb) target remote 192.168.1.20:1234
如果是Android的adb forward,则上述target remote 192.168.1.20:1234中的IP地址可以去掉,因为它变成直接连接本机了,可直接写成target remote:1234。
之后就可以使用gdb的命令来调试开发板上的应用程序了。
Linux性能监控和调优工具
除了保证程序的正确性以外,在项目开发中往往还关心性能和稳定性。这时候,我们往往要对内核、应用程序或整个系统进行性能优化。在性能优化中常用的方法有:
使用top、vmstat、iostat、sysctl等常用工具
- top命令用于显示处理器的活动状况。在缺省情况下,显示占用CPU最多的任务,并且每隔5s做一次刷新;
- vmstat命令用于报告关于内核线程、虚拟内存、磁盘、陷阱和CPU活动的统计信息;
- iostat命令用于分析各个磁盘的传输闲忙状况;
- netstat是用来检测网络信息的工具;
- sar用于收集、报告或者保存系统活动信息,其中,sar用于显示数据,sar1和sar2用于收集和保存数据。
- sysctl是一个可用于改变正在运行中的Linux系统的接口。用sysctl可以读取几百个以上的系统变量,例如用sysctl–a可读取所有变量。
sysctl的实现原理是:所有的内核参数在/proc/sys中形成一个树状结构,sysctl系统调用的内核函数是sys_sysctl,匹配项目后,最后的读写在do_sysctl_strategy中完成,如:
echo "1" > /proc/sys/net/ipv4/ip_forward
就等价于:
sysctl –w net.ipv4.ip_forward ="1"
使用高级分析手段,如OProfile、gprof
OProfile可以帮助用户识别诸如模块的占用时间、循环的展开、高速缓存的使用率低、低效的类型转换和冗余操作、错误预测转移等问题。它收集有关处理器事件的信息,其中包括TLB的故障、停机、存储器访问以及缓存命中和未命中的指令的攫取数量。
OProfile支持两种采样方式:基于事件的采样(Event Based)和基于时间的采样(Time Based)。
- 基于事件的采样是OProfile只记录特定事件(比如L2缓存未命中)的发生次数,当达到用户设定的定值时Oprofile就记录一下(采一个样)。这种方式需要CPU内部有性能计数器(Performace Counter)。
- 基于时间的采样是OProfile借助OS时钟中断的机制,在每个时钟中断,OProfile都会记录一次(采一次样)。引入它的目的在于,提供对没有性能计数器的CPU的支持,其精度相对于基于事件的采样要低,因为要借助OS时钟中断的支持,对于禁用中断的代码,OProfile不能对其进行分析。
OProfile在Linux上分两部分,一个是内核模块(oprofile.ko),另一个是用户空间的守护进程(oprofiled)。
- 内核模块负责访问性能计数器或者注册基于时间采样的函数,并将采样值置于内核的缓冲区内。
- 守护进程在后台运行,负责从内核空间收集数据,写入文件。其运行步骤如下:
① 初始化opcontrol–init
② 配置opcontrol–setup–event=…
③ 启动opcontrol–start
④ 运行待分析的程序xxx
⑤ 取出数据
opcontrol--dump
opcontrol--stop
⑥ 分析结果opreport-l./xxx
GNU gprof可以打印出程序运行中各个函数消耗的时间,以帮助程序员找出众多函数中耗时最多的函数;还可产生程序运行时的函数调用关系,包括调用次数,以帮助程序员分析程序的运行流程。
GNU gprof的实现原理:在编译和链接程序的时候(使用-pg编译和链接选项),gcc在应用程序的每个函数中都加入名为mcount(_mcount或__mcount,依赖于编译器或操作系统)的函数,也就是说应用程序里的每一个函数都会调用mcount,而mcount会在内存中保存一张函数调用图,并通过函数调用堆栈的形式查找子函数和父函数的地址。这张调用图也保存了所有与函数相关的调用时间、调用次数等的所有信息。
GNU gprof的基本用法如下:
① 使用-pg编译和链接应用程序。
② 执行应用程序并使它生成供gprof分析的数据。
③ 使用gprof程序分析应用程序生成的数据。
进行内核跟踪,如LTTng
LTTng(Linux Trace Toolkit-next generation)是一个用于跟踪系统详细运行状态和流程的工具,它可以跟踪记录系统中的特定事件。这些事件包括:
- 系统调用的进入和退出;
- 陷阱/中断(Trap/Irq)的进入和退出;
- 进程调度事件;
- 内核定时器;
- 进程管理相关事件——创建、唤醒、信号处理等;
- 文件系统相关事件——open/read/write/seek/ioctl等;
- 内存管理相关事件——内存分配/释放等;
- 其他IPC/套接字/网络等事件。
使用LTP进行压力测试
LTP(Linux Test Project)是一个由SGI发起并由IBM负责维护的合作计划。它的目的是为开源社区提供测试套件来验证Linux的可靠性、健壮性和稳定性。它通过压力测试来判断系统的稳定性和可靠性。
在工程中我们可使用LTP测试套件对Linux操作系统进行超长时间的测试,它可进行文件系统压力测试、硬盘I/O测试、内存管理压力测试、IPC压力测试、SCHED测试、命令功能的验证测试、系统调用功能的验证测试等。
(5)、使用Benchmark评估系统
使用Benchmark工具可以评估操作系统、网络、I/O子系统、CPU等的性能,在网址:http://lbs.sourceforge.net/ 列出了许多Benchmark工具。