第21章 Linux设备驱动的调试之GDB调试器的用法

本章知识点

        为了方便进行Linux设备驱动的开发和调试,建立良好的开发环境很重要,还要使用必要的工具软件以及掌握常用的调试技巧等。

1、Linux下调试器GDB的基本用法和技巧。

2、Linux内核的调试方法。

3、Linux内核调试用的printk()、BUG_ON()、WARN_ON()、/proc、Oops、strace、KGDB,以及使用仿真器进行调试的方法。

4、Linux应用程序的调试方法,驱动工程师需要编写用户空间的应用程序以对自身编写的驱动进行验证和测试,因此,掌握应用程序调试方法对驱动工程师来说也是必需的。

5、Linux常用的一些稳定性、性能分析和调优工具

21.1 GDB调试器的用法

21.1.1 GDB的基本用法

    GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具,GDB主要完成下面4个方面的功能。

启动程序,可以按照工程师自定义的要求运行程序。

让被调试的程序在工程师指定的断点处停住,断点可以是条件表达式。

当程序被停住时,可以检查此时程序中所发生的事,并追踪上文。

动态地改变程序的执行环境。

不管是调试Linux内核空间的驱动还是调试用户空间的应用程序,都必须掌握GDB的用法。而且,在调试内核和调试应用程序时使用的GDB命令是完全相同的,以代码清单21.1的应用程序为例演示GDB调试器的用法。

代码清单21.1 GDB调试器用法的演示程序(gdb_example.c)

#include <stdio.h>

int add(int a, int b)
{
return (a + b);
}

int main(void)
{
int sum[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int i;

int array1[10] = {48, 56, 77, 33, 33, 11, 226, 544, 78, 90 };
int array2[10] = {85, 99, 66, 199, 393, 11, 1, 2, 3, 4};

for (i = 0; i < 10; i++)
{
sum[i] = add(array1[i], array2[i]);
printf("sum[%d] = %d\n", i, sum[i]);
}
 
return 0;
}

使用命令gcc -g gdb_example.c -o gdb_example编译上述程序,得到包含调试信息的二进制文件gdb_example,执行gdb gdb_example命令进入调试状态,如下所示:


1.list命令

在GDB中运行list命令(缩写l)可以列出代码,list的具体形式如下。

list<linenum>,显示程序第linenum行周围的源程序,如下所示:


list <function>,显示函数名为function的函数的源程序,如下所示:


list,显示当前行后面的源程序。


list -,显示当前行前面的源程序。


下面演示了使用GDB中的run(缩写为r)、break(缩写为b)、next(缩写为n)命令控制程序的运行,并使用print(缩写为p)命令打印程序中的变量sum的过程:


2.run命令

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

(1)程序运行参数

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


(2)运行环境

用path <dir>可设定程序的运行路径;用how paths可查看程序的运行路径;


用set environment varname[=value]可设置环境变量,如set env USER=ubuntu;

用show environment[varname]则可查看环境变量。


(3)工作目录

cd <dir>相当于shell的cd命令,pwd可显示当前所在的目录。


(4)程序的输入输出

info terminal用于显示程序用到的终端的模式;

在GDB中也可以使用重定向控制程序输出,如run > outfile;

用tty命令可以指定输入输出的终端设备,如tty  /dev/ttyS1。

3.break命令

在GDB中用break命令来设置断点,设置断点的方法如下。

(1)break <function>

在进入指定函数时停住。

(2)break <linenum>

在指定行号停住。

(3)break+offset/break-offset。

在当前行号的前面或后面的offset行停住,offset为自然数。

(4)break filename:linenum

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

(5)break filename:function

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

(6)break *address

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

(7)break

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

(8)break…if<condition>

…可以是上述的break <linenum>、break+offset/break–offset中的参数,condition表示条件,在条件成立

时停住。比如在循环体中,可以设置break if i=6,表示当i为6时停住程序。


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


4.单步命令

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


单步执行的更复杂用法如下:

(1)step <count>

        单步跟踪,如果有函数调用,则进入该函数(进入函数的前提是,此函数被编译有debug信息)。step后面不加count表示一条条地执行,加count表示执行后面的count条指令,然后再停住。


(2)next <count>

        单步跟踪,如果有函数调用,它不会进入该函数。同理,next后面不加count表示一条条地执行,加count表示执行后面的count条指令,然后再停住。

        

(3)set step-mode

    set step-mode on用于打开step-mode模式,这样,在进行单步跟踪(运行step指令)时,若跨越某没有调试信息的函数,程序的执行则会在该函数的第一条指令处停住,而不会跳过整个函数。这样可以查看该函数的机器指令。


(4)finish

运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址、返回值及参数值等信息。

(5)until(缩写为u)

一直在循环体内执行单步而退不出来,用until命令可以运行程序直到退出循环体。

(6)stepi(缩写为si)和nexti(缩写为ni)

stepi和nexti用于单步跟踪一条机器指令。

备注:

        运行display/i$pc命令后,单步跟踪会在打出程序代码的同时打出机器指令,即汇编代码。

5.continue命令

    当程序被停住后,可以使用continue命令恢复程序的运行直到程序结束,或到达下一个断点。


6.print命令

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

print <expr>

print /<f> <expr>

       <expr>是表达式,也是被调试的程序中的表达式,<f>是输出的格式,比如,如果要把表达式按十六进制的格式输出,那么就是/x。

    在表达式中,有几种GDB所支持的操作符,它们可以用在任何一种语言中,@是一个和数组有关的操作符,::指定一个在文件或是函数中的变量,{<type>}<addr>表示一个指向内存地址<addr>的类型为type的对象。


        当需要查看一段连续内存空间的值时,可以使用GDB的@操作符,@的左边是第一个内存地址,@的

右边则是想查看内存的长度。例如如下动态申请的内存:

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

在GDB调试过程中这样显示这个动态数组的值:p *array@len

print的输出格式如下。

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

    可用display命令设置一些自动显示的变量,当程序停住时,或是单步跟踪时,这些变量会自动显示。

如果要修改变量,如x的值,可使用如下命令:print x=4

    当用GDB的print查看程序运行时数据时,每一个print都会被GDB记录下来。GDB会以$1,$2,$3…这样的方式为每一个print命令编号。可以使用这个编号访问以前的表达式,如$1。

7.watch命令

    watch一般用来观察某个表达式(变量也是一种表达式)的值是否有了变化,如果有变化,马上停止程序运行。有如下几种方法来设置观察点。

watch <expr>:为表达式(变量)expr设置一个观察点。一旦表达式值有变化时,马上停止程序运行。

rwatch <expr>:当表达式(变量)expr被读时,停止程序运行。

awatch <expr>:当表达式(变量)的值被读或被写时,停止程序运行。

info watchpoints:列出当前所设置的所有观察点。


8.examine命令

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

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

    <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)的内存。

9.set命令

examine命令用于查看内存,而set命令用于修改内存。命令格式是“set*有类型的指针=value”。

比如,下列程序,在用gdb运行起来后,通过Ctrl+C停住。

#include <stdlib.h>

void main(void)
{
        void *p = malloc(16);
        while(1);
}

可以在运行中用如下命令来修改p指向的内存。

(gdb) set *(unsigned char *)p='h'
(gdb) set *(unsigned char *)(p+1)='e'
(gdb) set *(unsigned char *)(p+2)='l'
(gdb) set *(unsigned char *)(p+3)='l'

(gdb) set *(unsigned char *)(p+4)='o'

看看结果:

(gdb) x/s p

0x804b008:      "hello"

10.jump命令

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

jump <address>

这里的<address>是代码行的内存地址。

备注:

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

11.signal命令

    使用singal命令,可以产生一个信号量给被调试的程序,如中断信号Ctrl+C。可以在程序运行的任意位置处设置断点,并在该断点处用GDB产生一个信号量,这种精确地在某处产生信号的方法非常有利于程序的调试。

    signal命令的语法是signal <signal>,UNIX的系统信号量通常为1~15,<signal>的取值也在这个范围内。

12.return命令

    如果在函数中设置了调试断点,在断点后还有语句没有执行完,可以使用return命令强制函数忽略还没有执行的语句并返回。

    return
    return <expression>

    上述return命令用于取消当前函数的执行,并立即返回,如果指定了<expression>,那么该表达式的值会被作为函数的返回值。

13.call命令

call命令用于强制调用某函数:

call <expr>

表达式可以是函数,以此达到强制调用函数的目的,它会显示函数的返回值(如果函数返回值不是void)。

比如在下列程序执行while(1)的时候:

#include <stdlib.h>

void main(void)
{
        void *p = malloc(16);
        while(1);
}

强制要求其执行strcpy()和printf():

(gdb) call strcpy(p, "hello world")
$3 = 134524936
(gdb) call printf("%s\n", p)
hello world
$4 = 12

14.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 gdb_example.c:add


15.disassemble

    disassemble命令用于反汇编,用来查看当前执行时的源代码的机器码,只是把目前内存中的指令冲刷出来。

用于查看函数add的汇编代码:




展开阅读全文

没有更多推荐了,返回首页