嵌入式开发调试方法

在嵌入式项目的开发中,受限于资源和工具,对程序的调试往往没有PC环境下的那么容易和轻松。而在嵌入式开发中,因为设备和环境的多样性、复杂性更易遇到各种奇怪的问题,这就产生了一个矛盾。为了定位和解决程序的问题,往往需要花费很大的精力,这就是最近一个礼拜我的经历。排查一个偶然才会出现的段错误,最后也才只是定位到线程栈被改写的位置,而没有定位到改写它的代码段,所以还是一筹莫展。这里总结一下用过和可用的调试方法与经验。

最简单:printf

在合理关键的路径位置添加打印信息是最简单,有时也是最有效的方法。如果程序的问题总是在固定的代码位置复现,那么通过在路径上添加打印信息就是最快速的定位问题出现在哪一代码行的方法。除了直接使用printf函数外,可以借鉴日志文件的记录格式来获取更多的信息,下面是我自己常用来替代日志记录的宏封装,可以指定打印信息的级别以便于分析和筛选:

#define LOG(level,format,args...) \
     do{printf("[%s][%s:%d]",#level,_func_,__LINE__);\
     printf(format,##args);}while(0)
宏中的level在使用时直接填写想要的信息级别就可以,比如常用的:DBG,ERR,WRN等,使用示例:
if((ret = pthread_create(&tid, NULL,module_proc, NULL))!=0)
{
     LOG(ERR,"create module_pro error:%s\n",strerror(ret));
     goto threadrun;
}

最实用:日志文件

printf是输出到标准输出或者终端的,需要人为地实时进行跟踪或者保存,否则就会失去这些信息。对于中大型的项目来说,大量地使用printf来进行跟踪和调试是不现实的,也不可能用眼睛去扫描那不断刷新的打印屏幕。程序运行过程的日志文件记录是最实用最规范的用来跟踪程序运行路径的方法。

实现一套日志文件记录接口没有什么难度,最简单的只需要将上面宏中的printf改成fprintf就可以,因此在嵌入式开发中,往往不同的项目常常会实现自己的日志记录接口。这只是一种轻量的重复劳动,倒问题不大,除了不利于程序的移植外。此外日志记录还是有不少细节需要考虑的,比如:循环覆盖写、不同线程或进程的同步写等问题,有时实现很容易会疏漏。这里推荐一个用C语言实现开源日志库:zlog,它提供了线程安全的日志记录接口和独立的日志接口行为配置文件,因此在不修改项目代码的情况下,你可以通过修改外部的zlog日志格式和行为配置文件来改变日志记录的格式和是否记录到日志文件中。

获取线程ID和名称

在多线程的程序中,获取线程的ID有现成的接口,但是获取线程的名称没有。在Linux2.6及更低版本的linux中,线程是使用clone系统调用创建进程来实现的,此时pthread的线程ID和线程本身的进程ID是不一致的。这里提供一个参考的实现来获取线程ID和名称:

/**
 * print thread/process id to analyse program
 *
 * @param[in] name the thread/process name.
 */
void printids(const char *name)
{
    pid_t pid, tid;
    pthread_t p_tid;
    char ids_buf[256] = {0};

    if(name == NULL) return;

    pid = getpid(); // get process group id
    tid = syscall(SYS_gettid); // get thread-process id ,man gettid/man syscall
    p_tid = pthread_self(); // get pthread tid

    snprintf(ids_buf,sizeof(ids_buf),"[IDS]%-25s: pid = %u,tid = %u,ptid = 0x%x\n",
        name,(unsigned int)pid,(unsigned int)tid,(unsigned int)p_tid);
    write_ids_info(ids_buf); // write ids info to log file
}

关于gettid参考linux中的man手册,注意其中的NOTES:

Glibc does not provide a wrapper for this system call; call it using syscall(2).

The  thread  ID  returned  by this call is not the same thing as a POSIX thread ID (i.e., the opaque value returned by pthread_self(3)).

最直接:gdb+gdbserver+core

在linux上最常用的调试工具就是gdb,它也绝对是最给力的调试利器。因此嵌入式开发中遇到棘手的问题是,首先想到的就是gdb工具。使用gdb进行嵌入式程序的调试有三种途径:

交叉编译gdb

如果开发板上的资源(内存和flash)充足的话,可以考虑下载gdb的源代码直接交叉编译得到可在开发板上运行的gdb工具,然后在编译嵌入式程序时加-g参数以保留调试符号信息,就可以直接在开发板上进行gdb的调试了。

gdbserver远程调试

交叉编译出的gdb一般会比较大,我这边使用arm-none-linux-gnueabi工具链编出的stripped后的都有3.3M。考虑到资源情况就需要使用gdbserver远程调试的方法,详细的步骤参考前文:使用gdbserver远程调试。这里使用时的说明几个注意点:

  1. gdb和gdbserver的版本必须是一致的,PC上运行的应该是编译工具链提供的gdb。
  2. PC上加载的程序和库应该是debug版本的,即not stripped的,以提供符号和代码信息。
  3. PC上使用的库的版本应该和开发板上使用的库版本一致,否则会出现库不匹配的warning。
  4. PC上使用gdb时首先应该设置好相应的环境变量及信号处理方式,以有效控制程序行为。

PC上可以通过写gdb的配置文件来实现环境和信号处理方式的设置,使用示例,下面是ak-gdbinit配置文件:

set solib-absolute-prefix /opt/arm-2009q3/arm-none-linux-gnueabi/libc
set solib-search-path /tftpboot/ak_ipc
handle SIGSEGV nostop print pass
使用时的命令如下,假设配置文件在/tftpboot目录下:
arm-none-linux-gnueabi-gdb -q -x /tftpboot/ak-gdbinit ipc_debug

分析core文件

像我在项目中遇到的问题:程序运行10几个小时或者几天后偶尔才出现段错误,这让我使用gdbserver时很郁闷,开着gdb+gdbserver等了两天都不出现段错误,而一不小心把gdb给关了就前功尽弃了。这种场景就是使用gdb+core文件调试的最佳选择了。

首先你得确定你的问题会不会生成core文件,SIGSEGV信号产生时,linux系统会生成core文件,所以我可以使用这种方法来调试。参考《APUE》P235可以知道还有以下信号的默认动作是终止+core:

  1. SIGABRT
  2. SIGBUS
  3. SIGEMT
  4. SIGFPE
  5. SIGILL
  6. SIGIOT
  7. SIGQUIT
  8. SIGSEGV
  9. SIGSYS
  10. SIGTRAP
  11. SIGXCPU
  12. SIGXFSZ

然后就是设置保存core文件的环境和使用,参考之前的:Linux系统中core文件调试方法

gdb加载core文件时如果出现下面的警告:

(gdb) core-file core-1385534307.6848 
warning: core file may not match specified executable file.
[New Thread 6879]
[New Thread 6848]
[New Thread 6849]
也许只是因为你使用的是带调试信息的程序,而开发板上生成core文件的是stripped后的程序,所以不用担心。

使用gdb调试时,对于多线程,以下的一些gdb命令是比较有用的:

info threads # 查看运行线程及当前线程
thread xxx # 切换到xxx编号的线程查看它的栈帧,编号有info threads命令给出
x/1024wx $sp # 以十六进制四字节单元方式查看当前栈顶开始的内存内容,help x查看x命令的用法

转载于:https://my.oschina.net/shelllife/blog/180901

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值