logo资料库

熟悉binutils工具集.pdf

第1页 / 共20页
第2页 / 共20页
第3页 / 共20页
第4页 / 共20页
第5页 / 共20页
第6页 / 共20页
第7页 / 共20页
第8页 / 共20页
资料共20页,剩余部分请下载后查看
熟悉 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 █
分享到:
收藏