博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
用Eclipse和GDB构建ARM交叉编译和在线调试环境
阅读量:2187 次
发布时间:2019-05-02

本文共 18698 字,大约阅读时间需要 62 分钟。

我们在 Linux 主机中搭建我们的开发环境,使用 Ubuntu 10.04 LTS 为例。

Eclipse 依赖于Java 环境,所以必须先安装 JRE 或 JDK。

去下载最新的Eclipse IDE。

我们一般选择 C/C++ 版本(Eclipse IDE for C/C++ Developers),这个版本自带了CDT,不用另行安装CDT插件。下载时选择 Linux 的版本,如:eclipse-cpp-juno-linux-gtk.tar.gz

直接把下载下来的压缩包(eclipse-cpp-juno-linux-gtk.tar.gz)解压到某一目录,运行可执行文件eclipse即可。

运行 eclipse 后,可先进行Eclipse使用环境Workspace的配置。

在 Eclipse 主界面点击菜单 File > New > C/C++ Project,在弹出的“C/C++ Project”对话框中,输入项目名称。选择一个项目类型(如Executable/Empty Project),在 Toolchains 中一定要选 Cross GCC,这是CDT对交叉环境的支持,提供了额外的功能,以方便嵌入式应用程序的开发。

点击下一步,来到“Select Configurations”页面,我们采用默认,直接下一步,来到“Cross GCC Command”的设置。这里我们可以填写交叉编译工具链的交缀和路径,如:

Cross compiler prefix: arm-arago-linux-gnueabi-Cross compiler path: /data/linux/ti-sdk5/linux-devkit/bin

上例的情况是在我们已经安装好了ARM芯片商提供的交叉编译包。一般来说,芯片商都提供了这样一个交叉编译环境,包括目标平台的编译工具链、Glibc库、二进制工具和其它常用工具。上例是TI提供的DM3730的开发包的安装路径和程序的前缀(真实程序为:arm-arago-linux-gnueabi-gcc、arm-arago-linux-gnueabi-ld等)

最后点击完成,一个交叉项目就创建好了。关于交叉编译前缀和路径的设置,在项目创建后,可以在项目的属性中改变它。

这里我们写一个简单的测试程序,如:

#include 
using namespace std;int main(int argc, char ** argv){ signal(0, 0); cout<<"This is a message from HelloDm3730!\n"; return 0;}

点击构建,对它进行编译。编译后,我们可以在左侧的“Project Explorer”中,项目目录下的Binaries中看到编译出来的程序。把它拷贝到目标ARM开发板上,运行,结果正确。

程序编译成目标平台的二进制码后,怎么拷贝到目标板上运行调试是个问题。

我们可以通过TFTP把编译好的程序下载到目标板上,即在开发主机安装TFTP服务器,在目标板上使用tftp命令向主机取文件。

另一个比较方便的做法是,在开发主机上部署NFS服务,导出一个共享目录,然后目标板上使用mount命令,把开发主机上的共享目录挂载到本地。这样,开发主机和目标板使用同一个目录,就不用不断下载程序了。NFS的部署可以参照。

终极的解决办法是搭建GUI的在线调试环境,即写完代码后,点击调试,Eclipse自动编译程序,然后把程序下载到目标板,然后运行打开GDB在线调试,这样就可以单步调试了,就像调试本地程序一样。

远程调试环境由宿主机GDB和目标机调试stub共同构成,两者通过串口或TCP连接。使用 GDB标准程串行协议协同工作,实现对目标机上的系统内核和上层应用的监控和调试功能。调试stub是嵌入式系统中的一段代码,作为宿主机GDB和目标机调试程序间的一个媒介而存在。

就目前而言,嵌入式Linux系统中,主要有三种远程调试方法,分别适用于不同场合的调试工作:

  • 普通列表项目用ROM Monitor调试目标机程序
  • 用KGDB调试系统内核
  • 用gdbserver调试用户空间程序

这三种调试方法的区别主要在于,目标机远程调试stub 的存在形式的不同,而其设计思路和实现方法则是大致相同的。

而我们最常用的是调试应用程序。就是采用gdb+gdbserver的方式进行调试。在很多情况下,用户需要对一个应用程序进行反复调试,特别是复杂的程序。采用GDB方法调试,由于嵌入式系统资源有限性,一般不能直接在目标系统上进行调试,通常采用gdb+gdbserver的方式进行调试。

嵌入式Linux的GDB调试环境由Host和Target两部分组成,Host端使用arm-linux-gdb,Target Board端使用gdbserver。这样,应用程序在嵌入式目标系统上运行,而gdb调试在Host端,所以要采用远程调试(remote)的方法。进行GDB调试,目标系统必须包括gdbserver程序(在主机上正对硬件平台编译成功后下载到目标机上),宿主机也必须安装GDB 程序。一般Linux发行版中都有一个可以运行的GDB,但开发人员不能直接使用该发行版中的GDB来做远程调试,而要获取GDB的源代码包,针对arm 平台作一个简单配置,重新编译得到相应GDB。GDB的源代码包可以从中下载。

再次强调一次,GDB远程调试套件包括Host端的gdb和Target端的gdbserver,对于gdb,宿主机上发行版本自带的PC版gdb是不能用的,它没有目标架构(ARM)相关的调试支持。所以我们应该使用gdb的源码,针对ARM平台编译一个(toolchain还是Host上的)特别的版本。当然,如果芯片商提供的交叉编译套件中已经包含了arm-linux-gdb,我们就不用重新编译gdb了,直接用它即可。不管怎么说,gdbserver还是需要用目标板的toolchain重新编译一遍,因为芯片商提供的交叉开发套件通常不包括gdbserver。

下载完后,解压:

#cd /opt#tar xzvf /tmp/gdb-6.8.tar.gz

建立配置文件,编译:

#cd /opt#mkdir -p arm-gdb/build#cd arm-gdb/build#/opt/gdb-6.8/configure --target=arm-linux --prefix=/opt/arm-gdb#make#make install

上面命令中,–target配置gdb的目标平台,–prefixp指定了编译结果的存放位置,也就是安装目录。编译完后可以在/opt/arm-gdb/bin目录下找到可执行的arm-linux -gdb, arm-linux -gdbtui, arm-linux-run。 拷贝arm-linux-gdb 到/usr/bin目录:

#cd /opt/arm-gdb/bin/#cp arm-linux-gdb /usr/bin/

下面把 gdbserver 移植到ARM平台。要点是指定目标平台的交叉编译链(gcc和ar)。我们创建一个临时的编译目录,以避免弄脏原代码。

#cd /home/kim#mkdir gdb-build#CC="/data/Linux/ti-sdk5/linux-devkit/bin/arm-arago-linux-gnueabi-gcc" \  AR=/data/Linux/ti-sdk5/linux-devkit/bin/arm-arago-linux-gnueabi-ar \  /opt/gdb-6.8/gdb/gdbserver/configure  --target=arm-linux --host=arm-linux#make

上面命令中,–target=arm-linux表示目标平台,–host表示主机端运行的是arm-linux-gdb,不需要配置—prefix,因为gdbserver不在主机端安装运行。临时环境变量CC和AR用于指定交叉编译和汇编选项,在同一行命令的configure执行时将应用这些选项。

没有错误的话就在/home/kim/gdb-build目录下生成gdbserver可执行文件,注意此时要更改其属性,否则可能会出现无法访问的情况,chmod 777 gdbserver将其更改为任何人都可以读写执行;使用arm-linux-strip命令处理一下gdbserver,将多余的符号信息删除,可让elf文件更精简,通常在应用程序的最后发布时使用;然后把它烧写到flash的根文件系统分区的/usr/bin(在此目录下,系统可以自动找到应用程序,否则必须到gdbserver所在目录下运行之),或通过nfs mount的方式都可以。只要保证gdbserver能在开发板上运行就行。

编译好gdbserver后,把它拷贝到目标板上的 /usr/bin 目录下,运行,如果能显示帮助信息,则交叉编译成功,如:

#gdbserverUsage:  gdbserver [OPTIONS] COMM PROG [ARGS ...]        gdbserver [OPTIONS] --attach COMM PID        gdbserver [OPTIONS] --multi COMMCOMM may either be a tty device (for serial debugging), or HOST:PORT to listen for a TCP connection.Options:  --debug               Enable general debugging output.  --remote-debug        Enable remote protocol debugging output.  --version             Display version information and exit.  --wrapper WRAPPER --  Run WRAPPER to start new programs.  --once                Exit after the first connection has closed.

如果提示其它错误信息,如二进制文件无法执行,则表示编译不成功。注意我们交叉编出来的gdbserver是无法在开发主机上运行的。

在目标板上,运行 gdbserver 命令启动测试程序的调试,并指定目标板的IP和监听端口号,如:

#gdbserver 192.168.188.120:12345 HelloDm3730 (注,HelloDm3730 为要调试的程序)Process HelloDm3730 created; pid = 625Listening on port 12345

我们看到gdbserver已经正常启动了,正在等待客户端程序gdb的连接。

此时我们在开发主机上运行 arm-linux-gdb,指定gdbserver的IP和端口,连接上去,如:

# arm-arago-linux-gnueabi-gdb (gdb) target remote 192.168.188.120:12345Remote debugging using 192.168.188.120:123450x400b57f0 in ?? ()

显示“0x400b57f0 in ?? ()”表示已经连接到远端的gdbserver并且开始调试了,此时目标板终端会显示“Remote debugging from host 192.168.188.201”,再次确认远程调试连接成功。接下来的调试方法与普通本机的gdb使用相同。

我们可以直接使用 arm-linux-gdb 对应用程序进行远程调试,但命令行界面实在是不够友好。这里我们想办法在Eclipse上,通过CDT使用GDB进行远程在线调试。

在Eclipse中选择一个项目,点击菜单 Run > Debug Configurations,在左侧的Dubug类型中找到“C/C++ Remote Application”,右击点“New”,创建一个远程调试配置,如下图:

如果安装的CDT插件不全,可能没有“C/C++ Remote Application”这个类型,意味着CDT没有远程调试功能。这时需要安装一个叫“Remote System Explorer End-User Runtime ”的插件,如何安装请参考:。

Eclipse的C/C++插件CDT已经很好的支持gdb在远程调试了。调试一个应用程序时,CDT有三种运行方式:

  1. Automatic Remote Launcher :远程自动运行,这是最方便好用的一种方式
  2. Manual Remote Launcher : 远程手动运行。用户自己在目标板上运行gdbserver,然后在开发主机上指定远程连接的方式(如IP地址和端口),连接到gdbserver
  3. Remote Attach Launcher :远程依附运行。类似于上一种,但它不是重新运行程序开启一个debug会话,而是直接Attach到一个已经运行的程序,然后调试

在Debug Configurations 对话框中,创建一个远程调试配置,这个配置在创建时会根据项目情况提供一个默认的配置,默认将使用第一种Automatic Remote Launcher方式,这在Main标签中下方“GDB (DSF) Automatic Remote Debugging Launcher”可以看出,点击右边的“Select other…”可以切换其它方式。

我们希望Eclipse每次生成一个项目之后,自动把生成出来的二进制程序拷贝到目标板上,这可以通过NFS挂载共享目录来实现,我们只需要配置项目属性(依次展开:C/C++Build > Settings > Build Steps > Post-build steps,在Command中输入“cp ProgramBin /mnt/share”)即可。

接下来配置CDT的Debug选项,步骤如下:

  1. 选中项目→菜单栏 ”Run“→Debug Configurations…
  2. 双击 C/C++ Remote Application 新建一个配置,Eclipse会根据当前选择的项目初始化大部分配置,这里只需修改Debugger配置页
  3. 在右下方点击“Select other”,选择“GDB(DSF) Manual Remote Debugging Launcher”,确认
  4. 选择进入Debugger配置页,在Main标签中,GDB debugger 填写 arm-linux-gdb,如果未加入PATH环境变量则应该填入绝对路径
  5. 在Debugger配置页的Shared Libraries标签中,可以添加库路径,比如调试过程中要步入外部函数,就必须在这里给出带调试信息的库文件路径,否则会找不到该函数的定义
  6. 在Debugger配置页的Connection标签中,Type选“TCP”,并填写目标板上gdbserver监听的IP和端口号(这个在下面文档会提及)
  7. 所有配置完成后,点“Apply”保存配置,并关掉配置窗口

接下来在目标板上运行 gdbserver,假如测试程序HelloDm3730已经下载到当前目录,如:

#gdbserver 192.168.188.120:12345 HelloDm3730Process HelloDm3730 created; pid = 625Listening on port 12345

目标板的 GDB 服务开启后,我们就可以在开发主机中,点击Eclipse的调试按钮(指定调试配置为刚才配置好的),开始应用程序的远程调试。我们在连接目标板的终端中,可以看到程序的标准输出;也可以在这里标准输入。

当我们结束调试后,目标板上的gdbserver进程会自动退出。如果我们在修改代码后想再次调试,仍然需要在目标板上运行gdbserver,这样会很麻烦,但毕竟可以进行远程的图形化调试了。

我们希望在开发主机上查看目标板(远端主机)的系统状况,如查看远程的文件,查看进程列表等,甚至打开终端运行远程机器上的程序。我们还希望这样一个工具软件能集成在Eclipse上,并且提供了API接口让Eclipse的插件能调用远程机器上的程序。

TM/RSE 就是这样一个工具,全称是 Target Management / Remote Systems Explorer,是Eclipse上的一个插件。Eclipse的远程管理插件有很多,但这个最常用,所以我们安装了 Eclipse 的C++版,就自带了这个插件。ARM的软件开发环境DS-5 也使用了这个插件,其实整个DS-5的基础都是Eclipse社区的开源软件搭建起来的。关于TM/RSE的更多介绍,可参考其官方资料:

我们可以在安装了TM/RSE的Eclipse上,点击右上角的“Open Perspective”按钮,打开“Remote System Explorer”,在 Remote System Explorer 上,你可以很方便的管理多个远程连接,查看远程文件和进程,运行远程程序等。

RSE 支持很多网络连接类型,常用的有:

  • FTP/sFTP
  • SSH
  • Telnet

支持连接到Unix/Linux/Windows主机和Local(本地)。要在开发主机上连接远程机器,必须先在远程机器上安装并启动某一远程控制服务,如 Telnet 服务、FTP服务、SSH服务。如果远程主机安装了文件服务(如FTP、SFTP)和Shell服务(如SSH、Telnet),那么可以在本地开发主机上,CDT可以调用RSE的API接口拷贝生成的目标程序到远端,并在远端目标板上运行gdbserver,从而做到自动下载和调试程序的功能。

CDT调用RSE的API接口实现远程调试的过程可分解为:

  1. 通过文件服务接口下载编译的程序到目标板(可用的文件服务有:FTP、SFTP)
  2. 通过Shell运行目标板上的gdbserver加载下载的程序(可用的Shell服务有:SSH、Telnet)

上面的第一步不是必需的,我们可以通过其它手段来实现,如在目标板上mount一个NFS文件系统,让开发主机(也挂载了同一个NFT文件系统)在编译后执行一条拷贝命令即可,这可以在项目的编译选项中配置。需要注意的是,如果想忽略这一步骤,还要在项目的Debug Configuration 中,勾选“Skip download to target path”,这样CDT才不会每次启动调试时都尝试打开文件服务接口。

为了方便,我们还是希望让目标板同时提供文件服务和Shell服务。我们有两种选择:

  1. 在目标板上部署 OpenSSH。OpenSSH 本身带了SSH服务和SFTP服务,所以一个软件包可以搞定。缺点是编译出来的文件很大
  2. 在目标板上部署 Dropbear 和 vsFTP(或者其它FTP服务器)

下面将分别介绍这两种方案。

请参考:

因为目标板的资源有限,在目标板上部署Telnet/FTP/SSH服务有点麻烦。好在Linux社区永远是开放的,我们有太多选择,如SSH服务可使用DropBear或OpenSSH,FTP可使用vsftpd等。如果我们的目标板已经在这些当中的某一服务了,那好办,直接连接即可;如果目标板上没有这些服务,我们还得自己编译和部署——用交叉编译器编译出目标平台的程序,然后部署和配置它。这里我们使用Dropbear来部署SSH服务。Dropbear 依赖 zlib 库,所以如果目标板上没有 libz.so,还得自己编译。

先在网上下载最新的 zlib 和 dropbear。我下载的版本是:dropbear-2012.55.tar.gz 和 zlib-1.2.7.tar.gz 。

编译的基本流程是:解压,配置,编译,安装。我们可以在./configure时就指定交叉编译工具。

对于 zlib,我们指定安装路径为~/soft/install,这样make install后,make就会把编译后的程序拷贝安装到这个目录,方便我们取程序。如:

#tar -xzf zlib-1.2.7.tar.gz#cd zlib-1.2.7#CC="/data/Linux/ti-sdk5/linux-devkit/bin/arm-arago-linux-gnueabi-gcc"   \ AR=/data/Linux/ti-sdk5/linux-devkit/bin/arm-arago-linux-gnueabi-ar  \  ./configure --shared --prefix=~/soft/install/#make#make install

“安装”后,可在我们指定的目录~/soft/install/的lib下找到共享库文件(so),我们要用到的是:libz.so libz.so.1 libz.so.1.2.7

对于 Dropbear ,我们需要多指定ranlib和strip,并且要指定目标平台为arm-linux。安装路径要使用绝对路径,虽然我们不安装。如:

#tar -xzf dropbear-2012.55.tar.gz#cd dropbear-2012.55#CC="/data/Linux/ti-sdk5/linux-devkit/bin/arm-arago-linux-gnueabi-gcc"   \ AR=/data/Linux/ti-sdk5/linux-devkit/bin/arm-arago-linux-gnueabi-ar  \ RANLIB="/data/Linux/ti-sdk5/linux-devkit/bin/arm-arago-linux-gnueabi-ranlib" \ STRIP="/data/Linux/ti-sdk5/linux-devkit/bin/arm-arago-linux-gnueabi-strip" \ ./configure --prefix=/home/kim/soft/dropbear-bin/ --host arm-linux#make

编译后,在当前目录可以看到我们需要的程序:

dropbear: ssh2 server dropbearkey: 密钥生成器 dropbearconvert: 可以转换openssh的密钥dbclient: ssh2 client

把zlib的三个so(libz.so libz.so.1 libz.so.1.2.7,前两个是连接文件)拷贝到目标板上的 /usr/lib;把dropbear生成的四个程序拷贝到目标板的 /usr/sbin 目录下。

然后(在目标板上)建立配置文件,生成SSH需要的密钥,如:

#mkdir /etc/dropbear #cd dropbear #dropbearkey -t rsa -f dropbear_rsa_host_key#dropbearkey -t dss -f dropbear_dss_host_key

最后启动服务:

#dropbear -E[746] Jan 01 03:30:41 Running in background (提示在后台运行了)

最好为目标板的用户建立密码。然后在开发机上,先测试一下我们的SSH服务:

#ssh root@192.168.188.55root@192.168.188.55's password: (这里输入密码)

如果输入正确的密码,但总是登录不成功(在目标板上的终端提示:user 'root' has invalid shell, rejected),可能是由于 root 帐号没有设置正确的登录shell。各大多数程序一样,Dropbear 只允许 /etc/passwd 中列举的用户登录,并且在/etc/passwd中该用户需要设置正确的shell(/bin/sh 或 /bin/csh)。有些busybox用的是/bin/bash,所以被Dropbear拒绝了。这里我们使用 /bin/sh 以后,就可以正常登录了。

vsFTP 暂时没有编译成功,它没有提供 configure 工具。

现在回过头来看CDT的远程调试。假设我们已经完成了以下工作:

  • 在远端部署了SSH或Tenlet服务(Telnet 在当前版本的RSE属于试验性质,远程调试没调通,不知是什么原因)
  • 在远端部署了某一个文件服务器(如SFTP或vsFTP)(这项没有也行,但调试会稍微麻烦点)
  • 本地RSE能访问远端功能(如Shell)
  • 在远端部署了 gdbserver,并存放在/usr/bin 中
  • 远端运行 gdbserver 时,开发主机能用 gdb (命令行)连接过去

我们应该先在Remote System Explorer上创建连接,让RSE能连接到目标机器和管理目标机器。然后在项目的Debug Configurations中,创建“C/C++ Remote Application”的一个配置,在Main标签中,Connection选择RSE上建立的连接,然后输入远端的程序存放的路径,这个路径是包含最终的程序文件名的,不是目录。注意:如果在远端没有部署文件服务(SFTP或FTP),那么还需要勾选“Skip download to target path”,这样CDT才不会每次启动调试时都尝试打开文件服务接口,造成调试进行不下去。但勾选“Skip download to target path”了后,必须有途径把最新的程序拷贝到刚才指定的远端程序路径,如每次手动拷贝,或者在项目配置中,Post-build steps 中输入自动拷贝命令,让项目每次编译后自动拷贝到目标机器。

运行 gdb

在 shell 中,可以使用 'gdb' 命令并指定程序名作为参数来运行 gdb,例如 'gdb eg';或者在 gdb 中,可以使用 file 命令来装入要调试的程序,例如 'file eg'。这两种方式都假设您是在包含程序的目录中执行命令。装入程序之后,可以用 gdb 命令 'run' 来启动程序。


调试会话示例

如果一切正常,程序将执行到结束,此时 gdb 将重新获得控制。但如果有错误将会怎么样?这种情况下,gdb 会获得控制并中断程序,从而可以让您检查所有事物的状态,如果运气好的话,可以找出原因。为了引发这种情况,我们将使用一个 程序:

代码示例 eg1.c
#include int wib(int no1, int no2){  int result, diff;  diff = no1 - no2;  result = no1 / diff;  return result;}int main(int argc, char *argv[]){  int value, div, result, i, total;  value = 10;  div = 6;  total = 0;  for(i = 0; i < 10; i++)  {    result = wib(value, div);    total += result;    div++;    value--;  }  printf("%d wibed by %d equals %d\n", value, div, total);  return 0;}

这个程序将运行 10 次 for 循环,使用 'wib()" 函数计算出累积值,最后打印出结果。

在您喜欢的文本编辑器中输入这个程序(要保持相同的行距),保存为 'eg1.c',使用 'gcc -g eg1.c -o eg1' 进行编译,并用 'gdb eg1' 启动 gdb。使用 'run' 运行程序可能会产生以下消息:

Program received signal SIGFPE, Arithmetic exception.0x80483ea in wib (no1=8, no2=8) at eg1.c:77         result = no1 / diff;(gdb)

gdb 指出在程序第 7 行发生一个算术异常,通常它会打印这一行以及 wib() 函数的自变量值。要查看第 7 行前后的源代码,请使用 'list' 命令,它通常会打印 10 行。再次输入 'list'(或者按回车重复上一条命令)将列出程序的下 10 行。从 gdb 消息中可以看出,第 7 行中的除法运算出了错,程序在这一行中将变量 "no1" 除以 "diff"。

要查看变量的值,使用 gdb 'print' 命令并指定变量名。输入 'print no1' 和 'print diff',可以相应看到 "no1" 和 "diff" 的值,结果如下:

(gdb) print no1$5 = 8(gdb) print diff$2 = 0

gdb 指出 "no1" 等于 8,"diff" 等于 0。根据这些值和第 7 行中的语句,我们可以推断出算术异常是由除数为 0 的除法运算造成的。清单显示了第 6 行计算的变量 "diff",我们可以打印 "diff" 表达式(使用 'print no1 - no2' 命令),来重新估计这个变量。gdb 告诉我们 wib 函数的这两个自变量都等于 8,于是我们要检查调用 wib() 函数的 main() 函数,以查看这是在什么时候发生的。在允许程序自然终止的同时,我们使用 'continue' 命令告诉 gdb 继续执行。

(gdb) continueContinuing.Program terminated with signal SIGFPE, Arithmetic exception.The program no longer exists.

使用断点

为了查看在 main() 中发生了什么情况,可以在程序代码中的某一特定行或函数中设置断点,这样 gdb 会在遇到断点时中断执行。可以使用命令 'break main' 在进入 main() 函数时设置断点,或者可以指定其它任何感兴趣的函数名来设置断点。然而,我们只希望在调用 wib() 函数之前中断执行。输入 'list main' 将打印从 main() 函数开始的源码清单,再次按回车将显示第 21 行上的 wib() 函数调用。要在那一行上设置断点,只需输入 'break 21'。gdb 将发出以下响应:

(gdb) break 21Breakpoint 1 at 0x8048428: file eg1.c, line 21.

以显示它已在我们请求的行上设置了 1 号断点。'run' 命令将从头重新运行程序,直到 gdb 中断为止。发生这种情况时,gdb 会生成一条消息,指出它在哪个断点上中断,以及程序运行到何处:

Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:2121          result = wib(value, div);

发出 'print value' 和 'print div' 将会显示在第一次调用 wib() 时,变量分别等于 10 和 6,而 'print i' 将会显示 0。幸好,gdb 将显示所有局部变量的值,并使用 'info locals' 命令保存大量输入信息。

从以上的调查中可以看出,当 "value" 和 "div" 相等时就会出现问题,因此输入 'continue' 继续执行,直到下一次遇到 1 号断点。对于这次迭代,'info locals' 显示了 value=9 和 div=7。

与其再次继续,还不如使用 'next' 命令单步调试程序,以查看 "value" 和 "div" 是如何改变的。gdb 将响应:

(gdb) next22          total += result;

再按两次回车将显示加法和减法表达式:

(gdb)23          div++;(gdb)24          value--;

再按两次回车将显示第 21 行,wib() 调用。'info locals' 将显示目前 "div" 等于 "value",这就意味着将发生问题。如果有兴趣,可以使用 'step' 命令(与 'next' 形成对比,'next' 将跳过函数调用)来继续执行 wib() 函数,以再次查看除法错误,然后使用 'next' 来计算 "result"。

现在已完成了调试,可以使用 'quit' 命令退出 gdb。由于程序仍在运行,这个操作会终止它,gdb 将提示您确认。


更多断点和观察点

由于我们想要知道在调用 wib() 函数之前 "value" 什么时候等于 "div",因此在上一示例中我们在第 21 行中设置断点。我们必须继续执行两次程序才会发生这种情况,但是只要在断点上设置一个条件就可以使 gdb 只在 "value" 与 "div" 真正相等时暂停。要设置条件,可以在定义断点时指定 "break <line number> if <conditional expression>"。将 eg1 再次装入 gdb,并输入:

(gdb) break 21 if value==divBreakpoint 1 at 0x8048428: file eg1.c, line 21.

如果已经在第 21 行中设置了断点,如 1 号断点,则可以使用 'condition' 命令来代替在断点上设置条件:

(gdb) condition 1 value==div

使用 'run' 运行 eg1.c 时,如果 "value" 等于 "div",gdb 将中断,从而避免了在它们相等之前必须手工执行 'continue'。调试 C 程序时,断点条件可以是任何有效的 C 表达式,一定要是程序所使用语言的任意有效表达式。条件中指定的变量必须在设置了断点的行中,否则表达式就没有什么意义!

使用 'condition' 命令时,如果指定断点编号但又不指定表达式,可以将断点设置成无条件断点,例如,'condition 1' 就将 1 号断点设置成无条件断点。

要查看当前定义了什么断点及其条件,请发出命令 'info break':

(gdb) info breakNum Type           Disp Enb Address    What1   breakpoint     keep y   0x08048428 in main at eg1.c:21        stop only if value == div        breakpoint already hit 1 time

除了所有条件和已经遇到断点多少次之外,断点信息还在 'Enb' 列中指定了是否启用该断点。可以使用命令 'disable <breakpoint number>'、'enable <breakpoint number>' 或 'delete <breakpoint number>' 来禁用、启用和彻底删除断点,例如 'disable 1' 将阻止在 1 号断点处中断。

如果我们对 "value" 什么时候变得与 "div" 相等更感兴趣,那么可以使用另一种断点,称作监视。当指定表达式的值改变时,监视点将中断程序执行,但必须在表达式中所使用的变量在作用域中时设置监视点。要获取作用域中的 "value" 和 "div",可以在 main 函数上设置断点,然后运行程序,当遇到 main() 断点时设置监视点。重新启动 gdb,并装入 eg1,然后输入:

(gdb) break mainBreakpoint 1 at 0x8048402: file eg1.c, line 15.(gdb) run...Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:1515        value = 10;

要了解 "div" 何时更改,可以使用 'watch div',但由于要在 "div" 等于 "value" 时中断,那么应输入:

(gdb) watch div==valueHardware watchpoint 2: div == value

如果继续执行,那么当表达式 "div==value" 的值从 0(假)变成 1(真)时,gdb 将中断:

(gdb) continueContinuing.Hardware watchpoint 2: div == valueOld value = 0New value = 1main (argc=1, argv=0xbffff954) at eg1.c:1919        for(i = 0; i < 10; i++)

'info locals' 命令将验证 "value" 是否确实等于 "div"(再次声明,是 8)。

'info watch' 命令将列出已定义的监视点和断点(此命令等价于 'info break'),而且可以使用与断点相同的语法来启用、禁用和删除监视点。


core 文件

在 gdb 下运行程序可以使俘获错误变得更容易,但在调试器外运行的程序通常会中止而只留下一个 core 文件。gdb 可以装入 core 文件,并让您检查程序中止之前的状态。

在 gdb 外运行示例程序 eg1 将会导致核心信息转储:

$ ./eg1Floating point exception (core dumped)

要使用 core 文件启动 gdb,在 shell 中发出命令 'gdb eg1 core' 或 'gdb eg1 -c core'。gdb 将装入 core 文件,eg1 的程序清单,显示程序是如何终止的,并显示非常类似于我们刚才在 gdb 下运行程序时看到的消息:

...Core was generated by `./eg1'.Program terminated with signal 8, Floating point exception....#0  0x80483ea in wib (no1=8, no2=8) at eg1.c:77         result = no1 / diff;

此时,可以发出 'info locals'、'print'、'info args' 和 'list' 命令来查看引起除数为零的值。'info variables' 命令将打印出所有程序变量的值,但这要进行很长时间,因为 gdb 将打印 C 库和程序代码中的变量。为了更容易地查明在调用 wib() 的函数中发生了什么情况,可以使用 gdb 的堆栈命令。


堆栈跟踪

程序“调用堆栈”是当前函数之前的所有已调用函数的列表(包括当前函数)。每个函数及其变量都被分配了一个“帧”,最近调用的函数在 0 号帧中(“底部”帧)。要打印堆栈,发出命令 'bt'('backtrace' [回溯] 的缩写):

(gdb) bt#0  0x80483ea in wib (no1=8, no2=8) at eg1.c:7#1  0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21

此结果显示了在 main() 的第 21 行中调用了函数 wib()(只要使用 'list 21' 就能证实这一点),而且 wib() 在 0 号帧中,main() 在 1 号帧中。由于 wib() 在 0 号帧中,那么它就是执行程序时发生算术错误的函数。

实际上,发出 'info locals' 命令时,gdb 会打印出当前帧中的局部变量,缺省情况下,这个帧中的函数就是被中断的函数(0 号帧)。可以使用命令 'frame' 打印当前帧。要查看 main 函数(在 1 号帧中)中的变量,可以发出 'frame 1' 切换到 1 号帧,然后发出 'info locals' 命令:

(gdb) frame 1#1  0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:2121          result = wib(value, div);(gdb) info localsvalue = 8div = 8result = 4i = 2total = 6

此信息显示了在第三次执行 "for" 循环时(i 等于 2)发生了错误,此时 "value" 等于 "div"。

可以通过如上所示在 'frame' 命令中明确指定号码,或者使用 'up' 命令在堆栈中上移以及 'down' 命令在堆栈中下移来切换帧。要获取有关帧的进一步信息,如它的地址和程序语言,可以使用命令 'info frame'。

gdb 堆栈命令可以在程序执行期间使用,也可以在 core 文件中使用,因此对于复杂的程序,可以在程序运行时跟踪它是如何转到函数的。


连接到其它进程

除了调试 core 文件或程序之外,gdb 还可以连接到已经运行的进程(它的程序已经过编译,并加入了调试信息),并中断该进程。只需用希望 gdb 连接的进程标识替换 core 文件名就可以执行此操作。以下是一个执行循环并睡眠的 :

eg2 示例代码
#include int main(int argc, char *argv[]){  int i;  for(i = 0; i < 60; i++)  {    sleep(1);  }  return 0;}

使用 'gcc -g eg2.c -o eg2' 编译该程序并使用 './eg2 &' 运行该程序。请留意在启动该程序时在背景上打印的进程标识,在本例中是 1283:

./eg2 &[3] 1283

启动 gdb 并指定进程标识,在我举的这个例子中是 'gdb eg2 1283'。gdb 会查找一个叫作 "1283" 的 core 文件。如果没有找到,那么只要进程 1283 正在运行(在本例中可能在 sleep() 中),gdb 就会连接并中断该进程:

.../home/seager/gdb/1283: No such file or directory.Attaching to program: /home/seager/gdb/eg2, Pid 1283...0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6(gdb)

此时,可以发出所有常用 gdb 命令。可以使用 'backtrace' 来查看当前位置与 main() 的相对关系,以及 mian() 的帧号是什么,然后切换到 main() 所在的帧,查看已经在 "for" 循环中运行了多少次:

(gdb) backtrace#0  0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6#1  0x400a877d in __sleep (seconds=1) at ../sysdeps/unix/sysv/linux/sleep.c:78#2  0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7(gdb) frame 2#2  0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:77           sleep(1);(gdb) print i$1 = 50

如果已经完成了对程序的修改,可以 'detach' 命令继续执行程序,或者 'kill' 命令杀死进程。还可以首先使用 'file eg2' 装入文件,然后发出 'attach 1283' 命令连接到进程标识 1283 下的 eg2。


其它小技巧

gdb 可以让您通过使用 shell 命令在不退出调试环境的情况下运行 shell 命令,调用形式是 'shell [commandline]',这有助于在调试时更改源代码。

最后,在程序运行时,可以使用 'set ' 命令修改变量的值。在 gdb 下再次运行 eg1,使用命令 'break 7 if diff==0' 在第 7 行(将在此处计算结果)设置条件断点,然后运行程序。当 gdb 中断执行时,可以将 "diff" 设置成非零值,使程序继续运行直至结束:

Breakpoint 1, wib (no1=8, no2=8) at eg1.c:77         result = no1 / diff;(gdb) print diff$1 = 0(gdb) set diff=1(gdb) continueContinuing.0 wibed by 16 equals 10Program exited normally.

转载地址:http://tzwub.baihongyu.com/

你可能感兴趣的文章
利用栈实现DFS
查看>>
逆序对的数量(递归+归并思想)
查看>>
数的范围(二分查找上下界)
查看>>
算法导论阅读顺序
查看>>
Windows程序设计:直线绘制
查看>>
linux之CentOS下文件解压方式
查看>>
Django字段的创建并连接MYSQL
查看>>
div标签布局的使用
查看>>
HTML中表格的使用
查看>>
(模板 重要)Tarjan算法解决LCA问题(PAT 1151 LCA in a Binary Tree)
查看>>
(PAT 1154) Vertex Coloring (图的广度优先遍历)
查看>>
(PAT 1115) Counting Nodes in a BST (二叉查找树-统计指定层元素个数)
查看>>
(PAT 1143) Lowest Common Ancestor (二叉查找树的LCA)
查看>>
(PAT 1061) Dating (字符串处理)
查看>>
(PAT 1118) Birds in Forest (并查集)
查看>>
数据结构 拓扑排序
查看>>
(PAT 1040) Longest Symmetric String (DP-最长回文子串)
查看>>
(PAT 1145) Hashing - Average Search Time (哈希表冲突处理)
查看>>
(1129) Recommendation System 排序
查看>>
PAT1090 Highest Price in Supply Chain 树DFS
查看>>