熟悉 binutils 工具集
熟悉 binutils 工具集
李 云
Blog: yunli.blog.51cto.com
摘要
对于嵌入式系统开发,掌握相应的工具至关重要,它能使我们解决问题的效率大大提高。目前,
可以说嵌入式系统的开发工具是 GNU 的天下,因为来自 GNU 的 GCC 编译器支持大量的目标处理
器。除了 GCC,还有一个非常重要的、同样来自于 GNU 的工具集(toolchain) —— binutils toolchain。
这一工具集中存在的一些工具,可以说是我们开发和调试不可缺少的利器。
本文通过介绍 binutils 以及提供一定的使用实例来帮助读者熟悉这一工具集,以达到提高效率的
目的。当你掌握了 binutils 后,你会发现你得到的是“渔”而不只是“鱼”。
关键词
binutils
工具集
参考资料
《什么是 boot loader》
《堆和栈》
《程序中的段》
《C 语言中一个字节对齐问题的分析》
1 引言
对于嵌入式系统开发,掌握相应的工具至关重要,它能使我们解决问题的效率大大提高。目前,
可以说嵌入式系统开发工具是 GNU(www.gnu.org)的天下,因为来自 GNU 的 GCC 编译器支持
大量的目标处理器。除了 GCC,还有一个非常重要的、同样来自于 GNU 的工具集(toolchain) —
— binutils toolchain。Binutils 中的工具不少和 GCC 相类似,也是针对特定的处理器的。
你可能要问:哪些嵌入式操作系统的开发是采用 GNU 工具集(包括 GCC 编译器、binutils 工
具集等)的?Linux 相关的实时(Monta Vista Linux、WindRiver Linux、RTLinux 等)或非实时嵌
入式系统开发就不用说了,全是采用 GNU 工具集的;最为有名的来自 WindRiver(现已被 Intel 收
购)的 VxWorks 操作系统也是采用 GNU 工具集的,为了使用 GNU 工具集,VxWork 的开发 IDE
采用 Cygwin 作为其在 Windows 操作系统的支撑平台;还有就是 RTEMS(www.rtems.org)操作系
统,以前是美国军方的一个实时操作系统,后来开源了,也是采用 GNU 工具集的;此外,另一个
很有名的实时操作系统 —— eCos,也是采用 GNU 工具集的,如果你熟悉 Altera 的 Nios,那么对
eCos 也应当不陌生;等等。我想可以举出很多很多的例子。例子越多,说明我们学习 binutils 就越
是有用!还有对于 bintuils 工具集的学习,不光是对于嵌入式系统开发有用,对于 Linux 主机或是
Solaris 服务器上的程序开发也是很有帮助的。
对于采用 C/C++从事 Windows 应用程序开发的人来说,很有可能会问:我在 Windows 上的一
个目标文件其后缀是.obj,在 GNU 的工具集中仍是采用.obj 后缀吗?在 Windows 中的动态库是以.dll
结尾的,那在 GNU 的工具集中也一样吗?。这些都是很好的问题,通过类比,我们可以根据我们
的经验去掌握另一类似的新东西。在 GNU 工具集中,一个源程序(.c 或是.cpp)是先被编译成.o
目标文件(对应于 Windows 中的.obj 文件)的,如果目标文件直接连接成可执行文件,则生成的是
ELF(Excutable and Linkable Format)文件。这种可执行文件对应于 Windows 中的.exe 文件,与
Windows 系统所不同的是,在 GNU 工具集中一个可执行文件并没有一个统一的后缀,甚至没有后
█ 1
熟悉 binutils 工具集
缀。如果要将多个.o 文件生成一个库文件,那么存在两种类型的库:一种是静态库,其后缀是.a;
另一种是动态库,其后缀是.so。在 Windows 系统中,其全部都是.dll。静态库与动态库的区别是什
么呢?静态库是每一个与这一库进行连接的都将有一份代码(和数据)拷贝。比如,如果 libx.a 中
存在一个 foo ()函数,而程序 A 和程序 B 都需要采用 libx.a 进行连接以使用其中的 foo ()函数,那么
在连接以后,程序 A 和 B 的可执行程序中都会存在一个 foo ()函数,即程序 A 和程序 B 的可执行代
码中都存在 foo ()函数的一个拷贝。与静态库所不同的是,采用动态库则不会生成多个代码拷贝。采
用动态库时,如前面的程序 A 和 B,所有的程序共享这个库的代码,即在内存中只存在这个库中代
码的一个拷贝,但这个库中的(可读写)数据仍然是每一个程序拥有一个独立的拷贝。
在 binutils 中以下的工具是我们在做嵌入式系统开发时需要掌握的:
as 是汇编器,在此我不打算对其进行讲解,因为其涉及到了处理器的指令集,我们在合适
的时候再来讲。
addr2line 用得到程序地址所对应源代码的文件名和行号以及所对应的函数。
ar 用于创建、修改档案文件(比如.a 静态库文件)以及从档案文件中抽取文件(比如从.a
静态库中抽取.o 文件)。
ld 是连接器,对其的讲解我打算采用独立的一篇文章来进行,因为连接器在嵌入式系统开
发中非常重要。比如,我们需要通过写或是修改连接脚本,来定制我们的嵌入式程序中的
各个段(section)。
nm 用于列出目标文件、库或是可执行文件(后面统称这三种文件为程序文件)中的代码符
号及代码符号所对应的程序开始地址。
objcopy 是用来拷贝或是翻译目标文件的。
objdump 帮助我们显示程序文件的相关信息。
ranlib 用于生成一个档案文件的内容索引。这样做的目的是为了加快档案文件的访问速度,
比如,我们常对静态库文件(.a 文件)进行 ranlib 以提高连接速度。
readelf 用于显示 ELF 文件的信息。
size 用于显示程序文件的段信息。
strings 用于显示一个程序文件当中的可显示字符串。
strip 用于剥去程序文件中的符号信息,以减小程序文件的大小。这对于存储空间有限的嵌
入式系统尤为有用。
在接下来的章节,我们将看一看各个工具的使用方法和使用例子。需要注意的是,本文并不是
binutils 工具集的完整参考手册,对于每一个工具的讲解都是基于其常用功能来进行的,当你需要得
到更为详细的帮助信息时,完全可以参照相应工具的 man(或 info)信息。比如,你要获得 objdump
工具的 man 信息,你可以在 Linux 或是 Cygwin 中运行“man objdump”。另一种更为简单的方法是
采用--help 参数运行相应工具得到简单的帮助信息,比如“objdump --help”。
需要注意的是,这不是一篇教你如何进行 Linux 程序开发的文章,相反,这里假设了你了解一
些基本的 Linux 命令。同样地,这篇文章不会告诉你什么时候要用 GCC 进行编译,而什么时候又得
用 G++进行编译,更不会告诉你这些编译器的具体参数的意思是什么以及如何使用。对于这些信息,
你需要参考其它的文章或是书籍。
2 准备环境
在讲解 binutils 中的工具之前,我们需要有一定的环境用于练习。你可以找一台安装有 GCC 的
Linux 计算机(可以是 Wmware 上虚拟的),如果没有,你可以在你的 Windows 上安装一个 Cygwin。
如果你需要安装 Cygwin,通常分为以下几个步骤:
从 www.cygwin.com 上下载 setup.exe,并运行它。然后选择“从 Internet 下载安装包到本
地”。在下载之前,请确保你选择了下载 GCC 和 binutils 安装包。
安装包下载完了以后,你需要再次运行 setup.exe,且这次选择“从本地安装”。在安装时,
2 █
同样不要忘了选择安装 GCC 和 binutils。
熟悉 binutils 工具集
当你安装好了 Cygwin 后,运行 Cygwin 并在其上运行如下的命令(注:美元符‘$’不是命令
的一部分,它是命令提示符,这如同 Windows 命令窗口中的‘C:\>’提示符)来验证 binutils 是否
已准备好。不管你使用的是 Linux 操作系统或是 Cygwin,如果你能看到命令运行后出现了对于这一
命令的使用说明,那么说明 binutils 在你的环境中被正确地安装了。
yunli.blog.51cto.com ~
$nm -h
如果采用 Cygwin,由于 Cygwin 只是在 Windows 上模拟 Linux 的环境,因此,其有些行为仍
然是像 Windows 的。比如,如果我们采用以下的命令来编译一个程序,在 Linux 上其生成的可执行
文件名就是 test,而在 Gygwin 上则为 test.exe。在后面的使用实例中,你需要注意这一区别。了就
是说,在 Linux 中可能输入的是 test,而在 Cygwin 中你必须换成输入 test.exe;反之亦然。
yunli.blog.51cto.com ~
$gcc main.c -o test
3 addr2line
addr2line 是用来将程序地址转换成其所对应的程序源文件及所对应的代码行,当然,也可以得
到所对应的函数。为了说明 addr2line 是如何使用的,我们需要有一个练习用的程序。先采用编辑工
具编辑一个 main.c 源文件,其内容如图 1 所示。
main.c
#include
void foo ()
printf (“The address of foo () is %p.\n”, foo);
{
}
int main ()
{
}
foo ();
return 0;
运行如下的命令将 main.c 编译成可执行文件,并运行之。在运行 test.exe 程序后,我们可以在
其终端上看到它打印出的 foo ()函数的地址 —— 0x401100。
图 1
yunli.blog.51cto.com ~
$gcc -g main.c -o test
yunli.blog.51cto.com ~
$./test.exe
The address of foo () is 0x401100.
现在,我们可以用这一地址来看一看 addr2line 是如何使用的。在终端中运行如下的命令,从命
令的运行结果来看,addr2line 工具正确的指出了地址 0x401100 所对于应的程序的具体位置是在哪
以及所对应的函数名是什么。
yunli.blog.51cto.com ~
$addr2line 0x401100 -f -e test.exe
█ 3
熟悉 binutils 工具集
foo
/home/Administrator/main.c:4
可能有人会问了:这个 0x401100 地址是我们打印出来,即然有打印,我们一般情况下也会打
印出其具体的函数位置,而不是只打印地址,我为何要这么绕一下通过 addr2line 去找到地址所对应
的函数呢?其实,这里打印出地址只是为了得到一个地址以便用于练习。在现实中,地址往往是在
调试过程中或是当程序崩溃时通过某种方式获得的。此外,采用 nm 工具(后面会讲到)可以得到
如下的函数地址信息。
yunli.blog.51cto.com ~
$nm -n test.exe
…显示结果有删减…
00401100 T _foo
0040111C T _main
nm 命令会打印出所有的符号(包括函数和全局变量名)所对应的开始地址,需要注意的是,在
C 中源代码中的函数其所对应的 nm 输出符号前会多加一个下划线‘_’,比如 C 程序中的 main ()
函数对应的是 nm 输出符号中的_main,同样地,C 程序中的 foo ()函数对应的是 nm 输出符号中的
_foo。从 nm 输出的信息你可以看出,foo ()函数所对应的地址为 0x00401100,而 foo ()函数是有大
小的(因为其有实现代码,且代码越是复杂或是长,则函数的大小越大),其大小是_main 的地址减
_foo 的地址(_main 紧跟在_foo 的后面说明在 C 程序中 main ()函数是跟在 foo ()函数的后面的),
那么是不是说我们给 add2line 的地址可以是从 0x0040100 到 0x0040111C 的任一地址呢?是的,
请看下面的操作结果。
yunli.blog.51cto.com ~
$addr2line 0x401110 -f -e test.exe
foo
/home/Administrator/main.c:4
yunli.blog.51cto.com ~
$addr2line 0x40111B -f -e test.exe
foo
/home/Administrator/main.c:4
我们已经讲了对于 C 程序 addr2line 是如何使用的,那么对于 C++程序呢?现在假设我们有如
图 2 所示的 C++代码。
main.cpp
#include
using namespace std;
void foo ()
cout << “The address of foo () is “ << hex << int (foo) << endl;
{
}
int main ()
foo ();
return 0;
{
}
4 █
图 2
熟悉 binutils 工具集
采用 g++编译这一代码并运行之,我们得到如下的结果。
yunli.blog.51cto.com ~
$g++ -g main.cpp -o cpptest
yunli.blog.51cto.com ~
$./cpptest.exe
The address of foo () is 401150
使用 addr2line 的结果可以从下面得到。奇怪!怎么出现了乱码?应当是_foo,怎么变成了
_Z3foov 了呢?
yunli.blog.51cto.com ~
$addr2line 0x401150 -f -e cpptest.exe
_Z3foov
/home/Administrator/main.cpp:6
其实,这是 C++语言的一个特点。在 GNU 工具集中存在 mangling 的这么一个称呼,而在
Windows 中称之为 decorating,也就是对 C++源程序中的函数名进行名字分裂的过程。为什么要进
行名字分裂呢?还记得 C++语言中的重载(overload)吗?在 C++中,允许多个函数是重名的,但各
个函数的输入参数必需是不一样的。那就有一个问题,当我们在实际的程序中调用这些重名的函数
时,如何区分哪一个函数应当被调用呢?显然,应当根据不同的参数调用其相应的函数。 C++语言
是在 C 语言的基础上实现的,因此,我们需要从 C 语言的角度来看这个问题。从 C 语言的角度来看,
那么每个函数名必须是不一样的,即不存在重载的概念。为此,C++编译器的处理方法是,对于每
一个函数根据其输入参数采用一定的编码方式,形成不同的 C 函数名,这一过程就是名字分裂过程。
正如上面你所看到的,_Z3foov 其实就是 C++程序中 foo ()函数的名字分裂后的形式。如果要我们看
这种“加了密”的函数名,那是很容易让人抓狂的。Addr2line 是否提供一定的选项来解决这一问题
呢?有的,那就是--demangle 选项,但很遗憾的是,我发现它在我的 Cygwin 中不能正常工作,而
在一台真正的 Linux 机器上,它却能正常工作。从下面在 Cygwin 中运行的结果可以看出,增加了
--demangle 选项后,名字变成了 Z3foov。后面我们讲 nm 工具时,我们再看看如何用 nm 得知名字
分裂后的形式所对应的真正的 C++程序中的函数。
yunli.blog.51cto.com ~
$addr2line 0x401150 --demangle=gnu-v3 -f -e cpptest.exe
Z3foov
/home/Administrator/main.cpp:6
使用 addr2line 的前提是程序文件中存在符号表,这通过给 GCC 增加一个-g 参数来达到这一目
的,在讲解 objdump 时我们会更加的清楚为什么。如果没有加入-g 选项,那么 addr2line 并不能起
作用。可能你会问,当我使用 GCC 或 g++进行代码优化时(比如使用-O2 选项)能否也使用-g 选
项呢?在 GCC 和 g++中,代码优化与生成符号表是正交的,也就是说前面所问的问题的答案是“可
以”。
除了这里所介绍的外,addr2line 还有其它的一些选项,你可以通过如下两条命令来得到其帮助
信息,以便在具体使用时参照。
yunli.blog.51cto.com ~
$addr2line -h
yunli.blog.51cto.com ~
$man addr2line
█ 5
熟悉 binutils 工具集
4 ar
ar 是用来管理档案文件的,在嵌入式系统开发中, ar 主要是用来对静态库进行管理的。现在
让我们先看一看一个静态库中到底有些什么。我们采用系统库文件/lib/libc.a 为例,对其采用 ar 的 x
参数进行解压操作,如下所示。
yunli.blog.51cto.com ~
$cp /lib/libc.a libc.a
yunli.blog.51cto.com ~
$ls
libc.a
yunli.blog.51cto.com ~
$ar x libc.a
yunli.blog.51cto.com ~
$ls
…显示结果有删减…
acltotext.o
fsetpos.o initgroups.o
regcomp.o tmpfile.o
chown.o fstat.o lchown.o
regerror.o truncate.o
cygwin_attach_dll.o
ftello.o libc.a
regexec.o
cygwin_crt0.o
ftruncate.o libcmain.o
regfree.o
dll_entry.o
getegid.o
lseek.o
seekdir.o
如果你本来就是一名 C 程序开发者,相信你对于上面解压出来的.o 文件名一点也不陌生。每一
个.o 文件差不多都能找到其文件名所对应的 C 库函数。采用 GNU 工具集进行开发时,一个静态库
其实就是将所有的.o 文件打成一个档案包就好了。当然,为了使得连接的速度更快,我们往往还得
生成内容索引,这可以通过给 ar 工具增加 s 参数来实现。
现在,为了示例我们是如何使用 ar 来生成静态库的,我们需要一些源程序文件。现在假设我们
有 foo.c 和 bar.c 两个 C 程序文件,其分别实现了 foo ()和 bar ()两个函数,代码如图 3 所示。
foo.c
#include
void foo ()
{
}
printf (“This is foo ().\n”);
bar.c
#include
void bar ()
{
}
printf (“This is bar ().\n”);
图 3
我们希望将 foo ()和 bar ()函数做成一个库,为此先要将它们分别编译成.o 目标文件。有了目标
文件之后,我们采用 ar 命令来生成 libmy.a 库,如下所示。其中,ar 的 c 参数表示创建一个档案文
件,而 r 参数表示增加文件到所创建的库文件中,s 参数是为了生成库索引以提高连接速度。
yunli.blog.51cto.com ~
6 █
熟悉 binutils 工具集
$gcc -c foo.c
yunli.blog.51cto.com ~
$gcc -c bar.c
yunli.blog.51cto.com ~
$ar crs libmy.a foo.o bar.o
现在你应当能在你的目录下看到一个 libmy.a 文件,这就是我们的静态库。下面我们需要验证这
一库确实是可用的,我们采用图 4 所示的源程序来验证它。
main.c
extern void foo ();
extern void bar ();
int main ()
{
}
foo ();
bar ();
return 0;
编译我们的验证程序,并与 libmy.a 进行连接,之后运行最终的可执行程序。从程序的运行结果
你确实可以看出,我们的 libmy.a 是起作用的。
图 4
yunli.blog.51cto.com ~
$gcc main.c libmy.a -o mylib
yunli.blog.51cto.com ~
$./mylib.exe
This is foo ().
This is bar ().
采用 ar 的 t 参数可以查看一个档案文件中有些什么内容,下面的操作示例了这一参数的作用。
yunli.blog.51cto.com ~
$ar t libmy.a
foo.o
bar.o
如果想删除档案文件中的文件,我们可以用 d 参数。下面示例了用 d 参数删除 libmy.a 中的 foo.o
文件。从操作的最后结果来看,当执行完了 d 操作后,libmy.a 中只存在一个 bar.o 文件了。
yunli.blog.51cto.com ~
$ar d libmy.a foo.o
yunli.blog.51cto.com ~
$ar t libmy.a
bar.o
上面我们讲解了 ar 的几个参数,这里我们再总结一下。采用 c 参数用于创建一个档案文件,r
参数表示向档案文件中增加文件,t 参数用于显示档案文件中存在哪些文件,s 参数用于指示生成索
引以加块查找速度,而 d 参数用于从档案文件中删除文件,最后 x 参数用于从档案文件中解压文件。
应当说 ar 还有其它的一些命令参数,但这里所讲到的几个都是最为常用的。
█ 7
熟悉 binutils 工具集
5 nm
前面我们提到了 nm,现在我们就来看一看 nm 的功能。总的来说,nm 用于列出程序文件中的
符号,符号是指函数或是变量名什么的。下面,我们来看一看图 2 所编译出来的程序当中有些什么
符号。
yunli.blog.51cto.com ~
$nm -n test.exe
…显示结果有删减…
00401100 T _foo
0040111c T _main
004011b8 T _printf
00401378 T _calloc
00401388 T _realloc
00401398 T _free
004013a8 T _malloc
00402000 D __data_start__
00402000 D _register_frame_info_ptr
00402004 D _deregister_frame_info_ptr
0040200c D __data_end__
nm 所列出的每一行有三部分组成:第一列是指程序运行时的符号所对应的地址,对于函数则地
址表示的是函数的开始地址,对于变量则表示的是变量的存储地址;第二列是指相应符号是放在哪
一个段的;而最后面的一列则是指符号的名称。在前面我们讲解 addr2line 时,我们提到 addr2line
是将程序地址转换成这一地址所对应的具体函数是什么,而 nm 则是全面的列出这些信息。但是,
nm 不具备列出符号所在的源文件及其行号这一功能,因此,我们说每一个工具有其特定的功能,在
嵌入式系统的开发过程中我们需要灵活的运用它们。
对于 nm 列出的第二列信息,非常的有用,其意义在于可以了解我们在程序中所定义的一个符
号(比如变量等等)是被放在程序的哪一个段的,对于程序段的概念和解释请参照《程序中的段》
一文。下表列出了第二列将会出现的部分字母的含义,要参看所有字母的意思,请在你的开发环境
中运行“man nm”。
字母
说 明
表示符号所对应的值是绝对的且在以后的连接过程中也不会改变
A
B 或 b 表示符号位于未初始化的数据段(.bss 段)中
表示没有被初始化的共公符号
C
D 或 d 表示符号位于初始化的数据段(.data 段)中
N
表示符号是调试用的
表示符号位于一个栈回朔段中
p
R 或 r 表示符号位于只读数据段(.rdata 段)中
T 或 t 表示符号位于代码段(.text 段)中
U
表示符号没有被定义
为了更清楚的理解 nm 中的符号与我们所编写的程序的关系。我们需要看一看图 5 所示的显示
源程序在采用只编译而不连接与采用连接的情况下,其所对应的 nm 输出结果有什么不同。
main.c
#include
int global1;
8 █