binutils工具集

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

1、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

2、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
其中, -- 创建一个档案文件; -- 将文件添加到所创建的库文件中; 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().

3、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 标准库中。链接过后再次查看,地址就会变成具体的地址了。

4、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

5、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

6、ranlib,库索引生成器

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

7、size,段大小观察器

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

8、strings,字符串窥视器

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

9、strip,程序文件瘦身器

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









  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值