binutils工具集

如果使用gcc作为编译器,那么binutils就是必不可少的一个工具集。工具集中的部分工具除了被gcc在后台使用为我们创建程序文件(目标文件、库文件或可执行程序)外,其它的则有助于方便开发和调试。
在不少嵌入式开发环境中,根据目标平台的不同,编译器的名称往往不是gcc,而是像arm-rtems-gcc这样的名称。对于这种命名形式的编译器,其binutils工作集通常也采用arm-rtems-xxx命名。
在binutils工具集中,以下工具是我们在做嵌入式软件开发时需要掌握的。

一、addr2line,指令地址翻译器
用于得到程序指令地址所对应的函数,以及函数所在的源文件名和行号。假设现在有函数:
--main.c--
#include <stdio.h>

void foo()
{
	printf("The address of foo() is %p.\n", foo);  //打印foo函数所在地址
}

int main()
{
	foo();
	return 0;
}
使用addr2line时,可执行程序中必须含有调试信息(关于调试信息,可以参考objdump),这需要在编译时添加 -g 选项。
$ gcc -g main.c -o test
$ ./test
$$ The address of foo() is 0x804841d.
现在可以直接通过addr2line工具查看这个地址的内容(所在函数、函数所在文件名和行号):
$ addr2line 0x404841d -f -e test
$$ foo
$$ /home/binutils/main.c:5
[-f 表示查看函数名, -e 表示设置输入文件(默认a.out)]
这里的地址是通过程序打印而获得的,在现实工作往往是程序崩溃时通过某种方式获取的,在这种情况下就可以使用addr2line工具查看崩溃点,进而修正程序。
通过nm工具(参考nm),可以得到程序的符号信息:
$ nm -n test
$$ 0804841d T foo
$$ 08048439 T main
[省略其它内容]
事实上,0x0804841d~0x08048439这一段地址都属于foo的汇编内容,addr2line通过之间的任意地址都可以定位到这个函数。
针对C++程序,addr2line似乎有些不同。假设有main.cpp如下:
--main.cpp--
#include <iostream>

using namespace std;

void foo()
{
	cout << "The address of foo() is 0x" << hex << int(foo) <<endl;
}

int main()
{
	foo();
	return 0;
}
$ g++ -g main.cpp -o cpptest
$ ./cpptest
$$ The address of foo() is 0x804872d
$ addr2line 0x804872d -f -e cpptest
这时候打印出来的却是:
$$ _Z3foov
$$ /home/binutils/main.cpp:6
C++为了重载的需要,C++处理器会对每一个函数按照一定的编码方式进行重命名,这一过程就是名字分裂过程。而_Z3foov则正是名字分裂之后的形式。通过添加 --demangle 选项可以获得原名:
$ addr2line 0x804872d --demangle=gnu-v3 -f -e cpptest 
$$ foo()

$$ /home/binutils/main.cpp:6


二、ar,静态库生成器
Linux中存在两种类型的库,一种是静态库,后缀为.a;另一种是动态库,后缀为.so;关于这两者的区别大概就像内联函数和普通函数那样,可执行程序与静态库链接时,所使用的库中的函数和数据会被拷贝到最终的可执行程序中,而动态库中的程序并不会被拷贝,整个系统中只有它里面唯一的一份。无疑动态库更加节省内存,但在嵌入式系统中,大多数情况整个软件就是一个可执行程序且不支持动态加载,即以静态为主。
binutils中的ar被用来管理静态库。一个静态库实际上就是将*.o文件打包而生产的档案文件。
现假设有两个文件foo.c和bar.c,
--foo.c--
#include <stdio.h>

void foo()
{
	printf("This is foo().\n");
}

--bar.c--

#include <stdio.h>

void bar()
{
	printf("This is bar().\n");
}

先将它们各自编译成目标文件:

$ gcc -c foo.c
$ gcc -c bar.c
如此将会生成foo.o和bar.o。将它们合并到libmy.a中:
$ ar crs libmy.a foo.o bar.o
其中,c--创建一个档案文件;r--将文件添加到所创建的库文件中;s--生成库索引以提高链接效率。可以通过如下命令查看库中内容:
$ ar t libmy.a
$$ foo.o
$$ bar.o
参数d可以删除库中某个目标文件,比如:ar d libmy.a foo.o
参数x可以解压这个库文件,比如:ar x libmy.a 
现在假设main.c更改如下:
--main.c--
extern void foo();
extern void bar();

int main()
{
	foo();
	bar();
	return 0;
}
编译并链接libmy.a:
$ gcc main.c libmy.a -o test
$ ./test
$$ This is foo().
$$ This is bar().

三、nm,符号显示器
总体来说,nm用于列出程序文件中的符号。先大概感受一下nm:
$ nm -n test
$$ 0804841d T main
$$ 08048434 T foo
$$ 08048448 T bar
[省略其它内容...]
nm所列出的每一行由三部分组成,第一列是函数或者变量的开始地址,第二列是相应的符号位于哪个段,最后一列则是符号的名称。关于第二列,这里的T表示代码段,其它类似的还有:
A--符号对应的值是绝对的且在以后的链接过程中不会改变
B/b--符号位于未初始化的数据段(.bss段)中
C--没有被初始化的公共符号
D/d--符号位于初始化的数据段(.data段)中
N--符号是调试用的
P--符号位于一个栈回溯段内
R/r--符号位于只读数据段(.rdata段)中
T/t--符号位于代码段(.text段)中
U--符号没有被定义

为了更好地说明以上符号,现假设有main.c如下:

--main.c--

#include <time.h>

int global1;					//1、未初始化的全局变量分配在.bss段
								//   没有被链接时为C,链接后为B
int global2 = 3;  				//2、初始化的全局变量分配在.data段,
const int GLOBAL = 5;			//3、只读数据存放于.rdata段

static int static_global1;      //4、未初始化的静态变量分配在.bss段
static int static_global2 = 3;  //5、初始化的静态变量分配在.data段

void foo()						//6、函数被放在.text段,T--非静态
{
	static int internal1;		//同4
	static int internal2 = 3;	//同5
}

static void bar()				//8、函数被放在.text段,t--静态
{
}

int main()						//同6
{
	int local1;					//9、局部变量存在于栈上,nm无法查看
	int local2 = 3;

	foo();
	return 0;
}

把它编译成目标文件:

$ gcc -c -g main.c
使用nm查看目标文件:
$ nm -n main.o
$$ 00000000 T foo
$$ 00000000 D global2
$$ 00000000 R global3
$$ 00000000 b static_global1
$$ 00000004 C global1
$$ 00000004 b internal1.1555
$$ 00000004 d static_global2
$$ 00000005 t bar
$$ 00000008 d internal2.1556
$$ 0000000a T main
由于程序没有链接,此时列出的地址为相对偏移地址。另外,_time符号在文件中没有定义,是因为它的实现位于C标准库中。链接过后再次查看,地址就会变成具体的地址了。

四、objdump,信息查看器
在嵌入式软件开发中,有时需要知道所生成的程序文件中的段信息以分析问题,或者需要查看C语言所对应的汇编代码,此时objdump就派上用场了。
使用如下命令可以查看段信息:
$ objdump -h test
-d选项可以显示程序文件的汇编代码:
$ objdump -d test
-d查看汇编代码时结合-S可以显示汇编代码对应的C/C++源程序(此时需要编译程序的时候不要使用优化选项):
$ objdump -S -d test
-f选项可以显示程序文件的头信息:
$ objdump -f test
-s -j配合使用可以查看某个段的具体内容:
$ objdump -s -j .data test

五、objcopy,段剪辑器
objdump可以对段进行过滤剪辑。
可以通过-j选项指定需要剪辑的段(这也是嵌入式中最重要的功能,在有的嵌入式系统中,比如制作引导加载器时就需要用到objcopy,以便将代码段抽取出来,然后将其烧写到系统的启动运行地址处):
$ objcopy -j .text test onlytext
剪辑多个段:
$ objcopy -j .text -j .data -j .bss test onlytext
与-j选项相反,-R选项可以删除某些段:
$ objcopy -R .text test notext
--strip-debug选项可以删除程序中的调试信息:
$ objcopy --strip-debug notext

六、ranlib,库索引生成器
在档案文件中生成文件索引信息,如二中提到的ar s参数。比如:
$ ranlib libmy.a

七、size,段大小观察器
可以用它查看程序文件各个段的大小:
$ size test  (-A 可以查看详细信息)

八、strings,字符串窥视器
strings可以查看程序文件中的可显示字符,比如打印的字符串信息、函数名等。

九、strip,程序文件瘦身器
去除程序文件中的调试信息以便减少程序文件的大小。它的功能与objcopy --strip-debug一样。









猜你喜欢

转载自blog.csdn.net/qq_20023231/article/details/80008209