今天我们来看一看开发中的辅助工具,那么什么是开发环境呢?在我们的印象中,开发环境就指的是编写代码的环境。其实不然,开发环境包括三大部分:构建环境、调试环境以及测试环境。构建环境便指得是代码编写、程序编译以及版本控制等;调试环境则指的是用于定位问题的辅助工具集;测试环境指的是用于验证目标程序是否满足用户的显性需求和隐形需求。显性需求指的是客户的要求,而隐形需求则指的是一些用户没有要求到的但是必须具备的要求。比如一个应用程序在 win7 系统上可以运行起来,在 win10 系统上也要能运行起来。

        在嵌入式的开发中,我们在整个项目中的代码编写及目标构建上一般只花费 20% 的时间,剩下的 80% 的时间适用于测试、调试以及 bug 修复的。那么我们该如何提高开发效率呢?工欲善其事必先利其器,我们可以借助于一些工具,从而提高开发的效率。GNU 为 GCC 编译器提供了配套的辅助工具集(Binutils),网址是 http://www.gnu.org/software/binutils/ ;其中的一些工具介绍如下

工具名     
功能简介                                    
add2line 
将代码地址转换为对应的程序引导
strip        
提出可执行程序中的调试信息      
ar            
将目标文件打包成为静态库         
nm          
列出目标文件中的符号及对应地址
objdump
查看程序段信息及反汇编             
size         
查看目标文件中的段大小             
strings    
查看目标文件中的字符串             

        下来我们来一一的介绍下这几个工具。

        1、addr2line : 将制定复制转换为对应的文件名和行号,常用于分析和定位内存访问错误的问题。来个示例代码进行分析说明


func.c 源码

#include <stdio.h>

int* g_pointer;

int main()
{
    *g_pointer = (int)"D.T.Software";
    
    return 0;
}


test.c 源码

#include <stdio.h>

int g_global = 0;
int g_test = 1;

extern int* g_pointer;
extern void func();

int main(int argc, char *argv[])
{
    printf("&g_global = %p\n", &g_global);
    printf("&g_test = %p\n", &g_test);
    printf("&g_pointer = %p\n", &g_pointer);
    printf("g_pointer = %p\n", g_pointer);
    printf("&func = %p\n", &func);
    printf("&main = %p\n", &main);
    
    func();
    
    return 0;
}

        我们先来分析下这份代码。我们在前面直接定义 int 类型的指针,但是并没有将其指为 NULL。那么此时 g_pointer 已经指向了内存的 0 地址处,在 main 函数中操作 0 地址处,这肯定会引起段错误。我们来看看编译运行结果

开发中的辅助工具(六)_辅助工具集

        我们看到 g_pointer 是个空指针,后面就直接发生段错误,如果这是个很大的项目,几十万行的代码,相信我们定位问题就很困难了。那么此时我们该如何定位问题呢?addr2line 工具便出场了。使用的步骤如下:1、开启 core dump 选项:ulimit -c unlimited;2、运行程序,并生产崩溃时的 core 文件,执行导致程序崩溃的测试用例;3、读取 core 文件,获取 IP 寄存器的值:dmesg core;4、使用 add2line 定位代码行:addr2line 地址 -f -e test.out。如下

开发中的辅助工具(六)_开发_02

        我们看到此时已经生成了 core 文件,我们来 dmesg core 看看 IP 寄存器的值是什么

开发中的辅助工具(六)_嵌入式_03

        我们看到 IP 寄存器的值是 0x080484b8,然后利用 addr2line 工具就可以直接定位到问题的所在了。

        2、strip:用于剔除程序文件中的调试信息,减少目标程序的大小。一般在程序发布前都需要将调试信息剔除,过多的调试信息可能影响程序的执行效率。我们来看看它的用法,strip test.out

开发中的辅助工具(六)_开发_04

        我们看到在经过剔除之后,它的文件大小几乎减少了一半。使用它的注意事项:1、几乎所有的调试工具都依赖于目标文件中的调试信息,调试信息的运用能够快速定位问题;2、使用 gcc 编译程序时使用 -g 选项生成调试信息,发布程序时再考虑是否使用 strip 剔除调试信息。那么这时如果想要利用 core 文件定位问题就不可以了,因为 core 文件只能定位出调试版本的信息

开发中的辅助工具(六)_辅助工具集_05

        我们看到它是定位不出问题的所在的。

        3、ar : 打包目标文件,ar crs libname.a x.o y.o;解压目标文件,ar x libname.a。下来看看是如何使用的

开发中的辅助工具(六)_GNU _06

        我们利用打包和解压命令从而就可以直接给别人发第三方库文件。如果我们只有第三方的库文件而想使用其中的一个 .o 文件,那么就可以使用解压命令来进行使用其中的一个文件。

        4、nm:用于列出目标文件中的标识符(变量名、函数名),输出结果由三部分组成{地址、段以及标识符}。示例如下

开发中的辅助工具(六)_GNU _07

        段标识说明如下

开发中的辅助工具(六)_开发_08

        我们来看看 func.o 和 test.o 文件中的标识符

开发中的辅助工具(六)_GNU _09

        我们看到在 func.o 文件中 func 是位于代码段的,它的偏移量为0。在这没有经过链接,所以显示的都是相对地址。g_pointer 属于未定义的标识符,相对偏移量为 4。下来我们看看链接后的地址

开发中的辅助工具(六)_GNU _10

        我们看到经过链接后的地址是绝对地址。

        5、objdump : 反汇编目标问阿金,查看汇编到源码的映射。objdump -d func.o 或者 objdump -S func.o。查看目标文件中的详细段信息,objdump -h test.out。

        objdump -h 的输出说明如下

开发中的辅助工具(六)_开发_11

        使用输出信息如下

开发中的辅助工具(六)_辅助工具集_12

        我们再来看看链接后的 test.out 的详细信息。

开发中的辅助工具(六)_开发_13

        我们看到它的 VMA 和 LMA 是一样的,也就是说,虚存地址和加载地址是一样的。在进行运行程序的时候,首先是为可执行文件分配虚存,接着通过 file off 获取到相对位置,通过复制段信息过去,加载目标地址到虚存上。最后是执行程序。

        6、size 用来获取目标文件中的所有段大小,size test.outstrings 用来获取目标文件中的所有字符串常量。如下

开发中的辅助工具(六)_开发_14

        那么我们获取它们的大小和字符串有什么意义呢?在嵌入式的开发中,资源往往是非常受限的,因此我们就必须得严格控制目标文件的大小,以防止超出其界限造成不可预料的错误;我们如果想要获取某些特定的字符串时就不必去看代码了,直接用 strings 就可以看到全部的字符串了,以提高开发效率。