这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » MCU » 谈谈嵌入式中的GCC

共1条 1/1 1 跳转至

谈谈嵌入式中的GCC

助工
2008-03-28 18:23:27     打赏

所谓GCC,就是the GNU Compiler Collection的缩写,它是每一个嵌入式工程师,包括那些不用Linux的人员,都需要接触的工具。自从1987年被推出后,由于它的易用,易获取 以及非常强大的兼容性,这个无所不能的玩意其实一直都没有得到应有的尊重。在嵌入式环境中,GCC通常都是用于交叉编译。表面上看,使用它很简单,调用编 译器就一切OK了,实际上,这是一个拥有很多旋钮的复杂工具,这些旋钮,可以被用来调节编译和链接的整个过程。


在嵌入式环境中,如何理解Gcc是怎么工作的?你是否知道在工具链目录下的其他工具的用途?还有没有让你更好适应这些工具的诀窍和秘笈?OK,linuxjournal 一篇文章讲如何构建Gcc交叉编译器,探讨了Gcc编译的过程,并共享了一些有效的技巧。我觉得讲得非常棒,就和大家共享。请注意,我在一些地方加入一些 自己以前交叉编译的心得和看法。所以如果有错误,欢迎邮件或者留言指出。本文没有详细介绍一些基础概念,有兴趣的朋友请直接Google。

Table of Contents
  1. GCC简介
  2. 构建交叉编译器
  3. 工具链:不仅仅是编译器
    1. Binutils
    2. C Library
    3. 预处理器和编译器
  4. 协同工作的机制
  5. SPEC文件
  6. 其他一些技巧
    1. 迫使GCC使用别的C library
    2. 混合的 assembler/source 输出
    3. 列出预编译的宏
    4. 列出依赖
    5. 显示内部步骤
  7. 总结
GCC简介

所谓GCC,就是the GNU Compiler Collection的缩写,它是每一个嵌入式工程师,包括那些不用Linux的人员,都需要接触的工具。自从1987年被推出后,由于它的易用,易获取 以及非常强大的兼容性,这个无所不能的玩意其实一直都没有得到应有的尊重。在嵌入式环境中,Gcc通常都是用于交叉编译。表面上看,使用它很简单,调用编 译器就一切OK了,实际上,这是一个拥有很多旋钮的复杂工具,这些旋钮,可以被用来调节编译和链接的整个过程。

构建交叉编译器

开始之前约法三章,下文所有的$TARGET表示目标处理器,$INSTALLAT表示编译器被构建后所在的目录。

所有人,在开始一个嵌入式项目时,首先需要的工具就是交叉编译器。很多时候,我们可以获得一些prebuilt的交叉编译器,无论是通过商业途径还 是非商业资源,从而避免了从源码构建的麻烦。但是,一些项目可能需要所有的工具都从源码中重新构建,而且了解这个过程对编译的理解大有裨益,因此还是需要 学习一下的。

其实从源码构建的方法有很多种,可能是最简单的一种,就是利用crosstool项目。这是由Dan Kegel 创建的,位于www.kegel.com/crosstool。具体的使用方法很简单,下载源代码,在脚本中修改参数为你所需要的,然后构建。它所支持的平台和软件版本可以从www.kegel.com/crosstool/crosstool-0.43/buildlogs 看到。crosstool 要做的事情,就包括通过你定制的脚本下载正确的软件和相应补丁。不过,如果你需要的C库不是标准版本(比如更小的实现的uClibc),那么这个过程就没 那么简单了。幸好,这个工程包含另一个和crosstool相似的东西,叫做buildroot,位于buildroot.uclibc.net。它不仅 仅可以构建一个交叉编译器,还可以用于构建根文件系统。我不想对这种方式有太多介绍,因为这个东西有点鸡肋的感觉,懒人通常都喜欢直接下载 prebuilt好的工具链,勤快人通常还是喜欢自己编译。我虽然是懒人,但是不想完全受制于人,所以我还是多介绍一下如何从源码来手工构建吧—– -其实过程可能不像你想的那么恐怖的。

1. 下载并构建binutils:

$ tar xzf binutils-<version>.tar.gz
$./binutils-<version>/configure –target=$TARGET –prefix=$INSTALLAT
$ make ; make install

2. 把目标板所带内核中的include和asm复制到安装目录下:

$ mkdir $INSTALLAT/include
$ cp -rvL $KERNEL/include/linux $KERNEL/include/asm $INSTALLAT/include

3. 下载并构建bootstrap GCC。注意,最好是在它自己的目录下构建,而不是在解压缩的目录下直接build。看了命令就明白了。

$ tar xzf gcc-<version>.tar.gz
$ mkdir ~/$TARGET-gcc ; cd ~/$TARGET-gcc
$../gcc-<version>/configure –target=$TARGET –prefix=$INSTALLAT –with-headers=$INSTALLAT/include –enable-languages=”c” -Dinhibit_libc
$ make all ; make install

这里解释一下,所谓bootstrap,这个参数的目的不仅仅是编译 GCC ,而是重复编译它几次。它用第一次编译生成的程序来第二次编译自己,然后又用第二次编译生成的程序来第三次编译自己,最后比较第二次和第三次编译的结果, 以确保编译器能毫无差错的编译自身,这通常表明编译是正确的。

4. 下载并用刚才的bootstrap Gcc构建glibc (或者其他你希望用的libc) 。和Gcc一样注意路径问题,不要直接在源码树下编译。

$ tar xzf glibc-<version> –target=$TARGET –prefix=$INSTALLAT –enable-add-ons –disable-sanity-checks
$ CC=$INSTALLAT/bin/$TARGET-gcc make
$ CC=$INSTALLAT/bin/$TARGET-gcc make install

5. 构建最终的GCC。 bootstrap编译器是用来构建C库的,而现在,通过使用交叉编译过的C库,我们可以构建最终的GCC了。

$ cd ~/$TARGET-gcc
$../gcc-<version>/configure –target=$TARGET –prefix=$INSTALLAT
–with-headers=$INSTALLAT/include –enable-languages=”c”
$ make all ; make install

最后,新创建的交叉编译器位于 $INSTALLAT/bin。另外,关于编译时的一些配置选项,如果有兴趣深入研究一下,请参考文章Glibc Binutils GCC 配置选项简介

在我们完成这部分之前,先对一个经常被那些目标处理器是Pentium的嵌入式工程师问到的问题作出解释。目标板和我们的host电脑平台都是一样 的,我们还需要交叉编译器么?答案是,需要。这种情况下构建交叉编译器,目的是将构建环境以及库的依赖和开发机器隔离开。桌面系统一年可能换几次版本,而 且可能并不是所有的团队成员都使用相同的版本,因此,对于编译一个嵌入式系统来说,如果希望较少可能的与配置有关的错误,拥有一个consistent的 环境是非常必要的。

工具链:不仅仅是编译器

什么是工具链呢?编译和链接一个应用程序的工具的集合,就是工具链。Gcc只是这个工具链中的一部分而已。一个完整的工具链应该包括以下三个部分: binutils,语言相关的标准库,以及编译器。通常工具链还附带了调适器debugger,不过它不属于标准工具链的一部分。

Binutils

Binutils 是一组开发工具,包括连接器,汇编器和其他用于目标文件和档案的工具。像linker和assembler这种工具链的关键部分就包括在binutils 项目中,而不是GCC项目的一部分。binutils项目中还隐藏了BFD库,这玩意实际上是一个单独的工程,全称是Binary Descriptor Library,为二进制目标文件提供一个抽象连续的接口,处理一些具体的细节,比如地址重分配,符号翻译,字节排序等。举个例子,它可以用来载入elf 格式可执行文件的运行代码段至内存。得益于BFD提供的功能,大多数需要读取或者操作用于目标板的二进制文件的工具都位于binutils项目,好充分利 用BFD的优势。

另外,binutils还包括以下程序:

 

  • addr2line: 给它一个包含调试信息和地址的二进制文件,它可以给你返回对应的行号。
  • ar: 函数库打包程序,可创建静态库.a文档。
  • gprof: GNU profiler工具
  • nm: 列出目标文件的符号清单
  • objcopy: 把一种目标文件中的内容复制到另一种类型的目标文件中,主要用于嵌入式中从ELF生成S-Records的。
  • objdump/readelf: 读取和打印出二进制文件的信息。readelf只适用于ELF格式文件。
  • ranlib: 用于把一堆.o文件打包成一个静态库.a。
  • size: 打印出二进制文件中各个部分的大小。
  • strings: 打印出二进制文件中的可显示字符串。当你想看看交叉编译程序链接到哪些库,又无法用本机的ldd看时,就可以用strings来看了。用法 strings <binary> | grep lib.
  • strip: 从文件中去除符号等信息,通常是调试信息。
  • nlmconv: 把目标代码转换成Netware Loadable Module (NLM)。
C Library

C语言中只有32个关键字,具体数目允许有一些小误差,这取决于语言编译器的具体实现。像C一样,大部分语言都拥有所谓的标准库赖提供一些通用操 作,例如字符串操作等,或者是和文件系统或存储单元的接口。C中大部分的编程都牵涉到和C库的交互,因此,很多项目中的代码其实不是工程师写的,而是由标 准库提供的。如何选择一个标准库,让它尽可能小,会对最终项目的大小有很大的影响。

大多数嵌入式工程师倾向于使用不同于标准GNU C Library的C library(耗资源啊),glibc则不同,它的设计专门考虑到了可移植性和兼容性。 表格1是各种常见库的优缺点总结:

Table 1.

Library Pros Cons
glibc 最权威的C library; 包括对C语言特性最多的支持; 非常容易移植; 支持最多的架构. 体积; 可配置性; 可能比较难交叉构建. uClibc 小(但不是最小的); 很容易配置; 使用广泛; 活跃的开发团队及社区. 并不是在所有架构上都能得到很好支持; 只支持UTF-8 DietLibC 除了小,还是小;对ARM和MIPS的完美支持 功能性很弱; 没有动态链接; 文档少 NewLib 得到Red Hat很好的支持; 对数学函数最好的支持; 非常棒的文档帮助. 社区很小; 更新很少.
预处理器和编译器

在产生一个可执行文件的过程中,这两部分起的作用很小。预处理器在编译之前运行,负责在编译器把输入文件转换成机器代码之前执行文本上的转换。在编 译过程中,编译器执行用户指定的优化并生成语法树。语法树之后被解释成汇编代码,然后被汇编器用来生成目标文件。如果用户最终需要得到可执行的二进制文 件,那么目标文件就会被送给链接器,来产生可执行文件。

协同工作的机制

在我们大概了解了工具链中的各个工具后,下面的段落会介绍把一个C源文件编译成二进制文件的具体过程。首先是调用GCC:

armv5l-linux-gcc file1.c file2.c -o thebinary

当然,GCC 实质上是一个引导者,它负责调用潜在的编译器和二进制工具binutils来生成最终的可执行文件。通过对输入文件扩展名的观察,然后使用内建在编译器里 面的规则,GCC需要确定以什么样的顺序,使用什么程序来构建输出。如果希望看到编译文件时有哪些步骤发生,可以添加-### 参数:

armv5l-linux-gcc -### file1.c file2.c -o thebinary

这样会在终端处产生很多输出信息,大多是已经修整过,便于阅读。一开始显示的信息描述了编译器的版本以及它是如何被构建的—这是非常重要的信息,特别是当我们需要知道“GCC构建时是不是指定有xxxx特性的?”这类询问时:

Target: armv5l-linux
Configured with: <the contents of a autoconf command line>
Thread model: posix
gcc version 4.1.0 20060304 (TimeSys 4.1.0-3)

输出工具的信息后,编译过程开始。每一个源文件都被cc1编译器所编译,这个cc1编译器是用于目标架构(target architecture)的真实编译器,之前当GCC被编译时,它会被配置为传递特定的参数给cc1:

"/opt/timesys/toolchains/armv5l-linux/libexec/gcc/
↪armv5l-linux/4.1.0/cc1.exe" "-quiet" "file1.c"
↪"-quiet" "-dumpbase" "file1.c" "-mcpu=xscale"
↪"-mfloat-abi=soft" "-auxbase" "file1" "-o"
↪"/cygdrive/c/DOCUME~1/GENESA~1.TIM/LOCALS~1/Temp/ccC39DVR.s"

现在,汇编器接管过程,并将文件转换成目标代码:

"/opt/timesys/toolchains/armv5l-linux/lib/gcc/
↪armv5l-linux/4.1.0/../../../../armv5l-linux/bin/as.exe"
↪"-mcpu=xscale" "-mfloat-abi=soft" "-o"
↪"/cygdrive/c/DOCUME~1/GENESA~1.TIM/LOCALS~1/Temp/ccm4aB3B.o"
↪"/cygdrive/c/DOCUME~1/GENESA~1.TIM/LOCALS~1/Temp/ccC39DVR.s"

对于下一个源文件,同样的过程会被进行,命令行的输出和之前文件的过程一样,只不过输出输入文件的名称不一样而已。

编译完成后,collect2就开始执行link步骤并寻找在“main”函数之前就要被调用的初始化函数(或者叫构造函数,但是这里的构造函数和 面向对象编程中的概念不同)。collect2把这些函数集中起来,创建一个临时源文件,编译它,并将其link到剩下的程序中:

"/opt/timesys/toolchains/armv5l-linux/libexec/gcc/
↪armv5l-linux/4.1.0/collect2.exe" "--eh-frame-hdr"
↪"-dynamic-linker" "/lib/ld-linux.so.2" "-X" "-m"
↪"armelf_linux" "-p" "-o" "binary" "/opt/timesys/
↪toolchains/armv5l-linux/lib/gcc/armv5l-linux/
↪4.1.0/../../../../armv5l-linux/lib/crt1.o"
↪"/opt/timesys/toolchains/armv5l-linux/lib/gcc/
↪armv5l-linux/4.1.0/../../../../armv5l-linux/lib/crti.o"
↪"/opt/timesys/toolchains/armv5l-linux/lib/gcc/
↪armv5l-linux/4.1.0/crtbegin.o"
↪"-L/opt/timesys/toolchains/armv5l-linux/lib/
↪gcc/armv5l-linux/4.1.0" "-L/opt/timesys/
↪toolchains/armv5l-linux/lib/gcc/armv5l-linux/
↪4.1.0/../../../../armv5l-linux/lib"
↪"/cygdrive/c/DOCUME~1/GENESA~1.TIM/LOCALS~1/
↪Temp/ccm4aB3B.o" "/cygdrive/c/DOCUME~1/
↪GENESA~1.TIM/LOCALS~1/Temp/cc60Td3s.o"
↪"-lgcc" "--as-needed" "-lgcc_s" "--no-as-needed"
↪"-lc" "-lgcc" "--as-needed" "-lgcc_s" "--no-as-needed"
↪"/opt/timesys/toolchains/armv5l-linux/lib/
↪gcc/armv5l-linux/4.1.0/crtend.o" "/opt/timesys/
↪toolchains/armv5l-linux/lib/gcc/armv5l-linux/
↪4.1.0/../../../../armv5l-linux/lib/crtn.o"

当然,这里还有值得一提的要素:

1. 有一个选项是专门指定当程序在target platform上运行时要调用的动态链接文件的:

"-dynamic-linker" "/lib/ld-linux.so.2"

在Linux系统中,动态链接程序实际上是被运行中的动态加载器加载的,当时是将它们自己作为linker的参数。这个linker的作用呢,就是 把这些库加载到内存中,并解决其他一些问题的。如果在目标平台上,这些动态链接文件并不是在和编译平台相同的位置中,就会在运行程序时提示错误信息 “unable to execute program”。好吧,这个问题,我相信每一个嵌入式开发者都曾遇到过。

2. 下面这些文件呢,包括了在入口(通常是main,当然你也可以另行指定)之前的代码以及扮演大管家的角色,包括处理全局初始化,打开标准文件句柄等很多函数。

 

  • crtbegin.o
  • crt1.o
  • crti.o

 

3. 同样,下面这些文件包括了最后一次返回后的代码,例如关闭文件等。他们,包括上面提到的那些文件,都是在GCC被构建时就交叉编译过了。

 

  • crtend.o
  • crtn.o

 

OK,搞定!在过程的最后,我们就可以看到一个已经做好在target platform上执行的程序了!

SPEC文件

还记得刚才我们说过,GCC是一个引导者,或者说是幕后黑手么?它知道该调用那些程序来构建对应的输出。那么,它是怎么知道这些信息的呢?其实,这 些在GCC被构建时就一同构建的信息是被保持在“specs”中的。如果想看看这个“specs”,就要在运行GCC时增加一个-dumpspecs参 数:

armv5l-linux-gcc -dumpspecs

然后呢,你的终端就会被几百行的输出信息充斥。spec文件格式是在很多年基础上发展起来的,它更方便让计算机阅读和理解,而不是用户。它的每一行都包含了很多指令,比如给定一个工具,需要用到什么参数。看看前面的例子中,我们考虑汇编器的命令行显示:

"<path>/as.exe" "-mcpu=xscale" "-mfloat-abi=soft"
↪"-o" "<temppath>/ccm4aB3B.o" "<temppath>/ccC39DVR.s"

编译器为汇编器提供在specs中的下列信息:

*asm:
%{mbig-endian:-EB} %{mlittle-endian:-EL} %{mcpu=*:-mcpu=%*}
↪%{march=*:-march=%*} %{mapcs-*:-mapcs-%*}
↪%(subtarget_asm_float_spec)
↪%{mthumb-interwork:-mthumb-interwork}
↪%{msoft-float:-mfloat-abi=soft}
↪%{mhard-float:-mfloat-abi=hard} %{mfloat-abi=*}
↪%{mfpu=*} %(subtarget_extra_asm_spec)

下面解释了这行中使用的一些熟悉的构造。如果想对spec文件进行充分探讨,估计又是一个系列的文章了。

 

  • *asm: this line tells GCC the following line will override the internal specification for the asm tool.
  • %{mbig-endian:-EB}: the pattern %{symbol:parameter} means if a symbol was passed to GCC, replace it with parameter; otherwise, this expands to a null string. In our example, the parameter -mfloat-abi=soft was added this way.
  • %(subtarget_extra_asm_spec): evaluate the spec string %(specname). This may result in an empty string, as it did in our case.

 

大多数用户其实都不需要修改spec文件,有时候呢,一些处理遗留工程的工程人员是希望GCC能辨认一些非常规的扩展名文件的。比如,一些汇编代码 文件可能以.arm为文件扩展名,这种情况下,GCC是不知道执行什么程序来处理它,因为没有这种文件扩展名对应的规则。怎么办呢?创建一个包括了下列命 令的spec文件:

.arm:
@asm

 

然后使用 -specs=<file> 来将其传递给GCC,这样,GCC就能知道怎么处理扩展名是.arm 的文件了。当这个spec文件被处理完后,就会被添加到内部spec文件中。

其他一些技巧

下面是一些经常使用GCC的工程师需要注意的技巧:

迫使GCC使用别的C library
armv5l-linux-gcc -nostdlib -nostdinc -isystem
↪<path to header files> -L<path to c library>
↪-l <c library file>

这会告诉GCC,忽略一切已知的,从哪里寻找头文件和库的路径,相反,使用你告诉它的路径来寻找。大多数可替换的C libraries提供具有相同功能的脚本,不过,一些工程并不能使用这些脚本,而且一些时候直接指定这些信息对于做不同版本库的实验时非常有用。

混合的 assembler/source 输出
armv5l-linux-gcc -g program.c -o binary-program
armv5l-linux-objdump -S binary-program

如果你希望看看GCC到底将输入源程序变成啥了,这就是最好的方法。能看到汇编代码的好处就不用我多说了吧,比如能确保指定处理器相关的优化时,是否生成合适的指令,等等。

列出预编译的宏
armv5l-linux-gcc -E -dM - < /dev/null

对于移植过程来讲,这是一个无价之宝啊。它能让我们搞清楚,哪些GCC的宏会被自动设置,它们又是啥值。它不仅仅能显示标准宏,还包括所有的为 target architecture设置的宏。具体的作用嘛,就是当你手头有两个版本的GCC,却遇到用新版GCC时代码无法正常编译或者运行时,可以比较一下新版 和旧版在这方面处理的区别,这可能节约你大量的调试时间。

列出依赖
armv5l-linux-gcc -M program.c

顾名思义,就知道啥意思了。如果你希望跟踪与源文件使用哪些头文件有关的问题,或者与迫使GCC使用别的C库有关的问题时,这个输出就不可缺少了。 如果你的工程不小,那么深入理解头文件之间的关系是必不可少的。使用 -MM 来取代-M 的话,可以只显示系统无关的依赖,在问题只存在于项目文件中时,这样可以有效减少无用输出。

显示内部步骤
armv5l-linux-gcc -### program.c

本文其实已经使用这个参数来显示构建一个程序时内部发生的步骤。当程序没有被正常编译或者链接时,使用 -### 是确认GCC到底在干嘛的最快方式。每一个命令可以被单独运行,因此:

armv5l-linux-gcc -### program.c &> compile-commands

将产生包含了编译命令的文件,这时,我们可以赋予其运行权限,然后每次运行一行,来确定问题发生之处。

总结

GCC是一个功能强大而复杂的工具。开发商创造了一个仅靠用户提供的最少的信息就能做”正确的事” 的软件。因为它运行如此良好,使用者经常忽略了应该花时间去了解GCC的功能。这篇文章仅仅是停留在表面讨论的,读者有时间的话,最好读一读软件文档,并 每天花一些时间的来学习如何让这个工具发挥超乎我们预料的作用。




关键词: 谈谈     嵌入式     中的     需要     工具     这个     交叉     编译         

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]