在一种计算机环境(称为host machine)中运行的编译程序,能编译出在另外一种环境(称为target machine)下运行的代码,叫做交叉编译。实现这个交叉编译的一系列工具,包括C函数库,内核文件,编译器,链接器,调试器,二进制工具……称为交叉编译工具链。
实际上在进行嵌入式开发时,我们通常都会在主机上(host machine)使用开发板厂商提供的编译器,调试器。比如在windows上装环境调试51,61单片机,在Linux上用arm-gcc写arm开发板的程序……
搭建交叉编译环境是一个非常繁琐并细致的过程。笔者就已嵌入式linux交叉编译工具链的搭建为例,介绍下交叉编译工具的创建过程,和原理。
主机(hostmachine),Fedora 9 Linux,
目标机(target machine),arm,mips,sh,ppc……
一,首先介绍下交叉编译工具链的组成部分。
1,编译器,汇编器。
编译器是交叉编译工具链中最显眼的部分,因为平常我们与它打交道,写程序,编译成binary,下到开发板上。但是请注意,真正的编译器并不会把程序直接变成二进制文件,而是链接器在做这件事情。
在Linux中,最常用的编译器就是传说中的gcc了,目前gcc已经到了4.4版本。编译器做的事情是把程序,翻译成目标机的汇编文件(.s),然后调用二进制工具的汇编器(as),变成目标文件(.o)。
严格的说,汇编器as并不属于编译器gcc,它和链接器ld属于二进制工具(binutils -- binary utilies)。
上一张图供大家理解(gcc.jpg)。
什么是目标文件(.o)?目标文件本身是一种可链接的二进制文件,目前在System V系列系统中(Linux,Unix)常用的是ELF格式。简单的把.o画一下吧。
2,链接器。
链接器是把多个目标文件变成二进制文件。这个文件,可以是可执行文件,也可以是一个动态链接库,也可以是一个可重定位的目标文件(更大的.o)。链接成的文件也是ELF格式。
链接的过程非常复杂,但原理却又简单,后面可以讨论。链接器在Linux中用的是ld,和as一样,在binutils程序包中。
ELF定义比较复杂,主要是对于链接来说分了很多的section,对于运行来说逻辑上有一些segment。如果大家想了解一下ELF格式,其实是有利于对整个系统的理解和高级调试的。需要时进行讨论。
3,内核文件。
内核文件是指内核头文件。用途?实际上在编译程序(包括我们编译内核,甚至交叉编译工具链)的时候,通常要使用一些系统调用。内核头文件用于声明这些函数,以便链接器能够找到相应的程序入口。
Linux中的内核头文件当然是在Linux Kernal 源码包里了,下载Linux Kernel后,即可安装头文件到某目录,供编译器及C库使用。请注意的是,内核头文件和二进制工具没有依赖关系。
为什么要专门提出这一点?首先,有助于我们对交叉编译工具链各个环境的理解。其次,很多教材都是错的。原因:二进制工具,实际上是根据target machine的ABI接口对程序进行规范和抽象,并不关心到底程序在做什么。ABI=application binary interface,是system V系列系统汇编和硬件关系的接口标准。每个系列的处理器都有自己的ABI接口规范,ABI中定义了一些汇编使用规范和接口,如寄存器的使用,栈的组织,函数调用入口,数据对齐、格式,异常处理接口、信号规范等等等等。
4,C函数库。
无论是编译内核,还是日常的程序,甚至编译一个C++的编译器,都需要C语言函数库的支持。比如,我malloc一块内存,我要知道怎么malloc,Linux Kernel中不会提供一个内存管理的工具。其次C库是可以更换的,无论是静态链接库还是动态链接库。由于C库的接口标准同意,就可以对库而不是整个系统进行升级。
在嵌入式系统中,使用的c语言库比较多。最强大,也最huge,最完善的莫过于GNU的glibc了,glibc也是标准的C语言库,目前已经到了version 2.9。另外,由于嵌入式系统的灵活性,不可能将如此大的一个c库移植到一个系统中,就衍生了很多轻量级的c库,如uClibc,newlib。
请注意,由于编译器是根据C语言库创建出来的,所以编译器是依赖于C库的。比如ARM中的gcc,我们一般看到两种,一种是arm-linux-*,一种是arm-linux-*。区别就在于,arm-linux-*一般是根据glibc创建出来的,只能够和glibc的库使用(你使用glibc 2.8 2.9的动、静态连接库没有关系,而不能使用别的c库),但不能使用,而arm-elf-*一般使用 uClibc或者使用redhat专门为嵌入式系统的开发的C库newlib。
请注意C库有个C库的头文件需要安装,依赖于内核头文件。
5,调试器。
调试器可以看作是补充的工具,当然,前提是你介于牛A和牛C之间。在编程中经常遇到这样那样的问题,有个调试器自然是最方便不过的了。目前支持各种环境的调试器最强的莫过于GDB了,GNU的gcc或许还对于某个平台优化的不好(例如x86环境下gcc编译出来的程序效率就不如intel自家的c++ complier),GDB在嵌入式上强大到一统江湖……
但调试器并不只包括GDB,还有二进制工具中的objdump,address2line,readelf等。GDB依赖于内核头文件和C库。
请注意在程序编译时必须加入一些调试信息,才能够使用调试器。常用的调试信息格式有stabs/stabs+, coff/xcoff/xcoff+, dwarf/dwarf+,怎么把这些信息加到程序中可以在gcc里面通过-g选项打开,一般貌似也用不到太深入的……需要用到时候在讨论吧。
二,创建交叉编译工具链。
创建交叉编译工具链有一些基本步骤。无论是Linux还是其他系统,原理是相通的。可以参考《构建嵌入式操作系统》
可以参考youbest的CLFS2.0原理分析,CLFS也是一个著名的crosstoolhttp://www.linuxsir.org/bbs/showthread.php?t=267672
主要步骤是
- 安装交叉编译内核头文件/安装交叉编译的binutils (不分先后)
- 安装target machine c库头文件。
- 通过内核、C头文件和binutils安装gcc的c交叉编译器(bootstrap gcc)
- 编译交叉编译的c库
- 通过c库,头文件,编译出gcc的c++编译器。
- 安装gdb
对于板子的不同,需要更改的地方是:C库中内存map,kernel header中的各中断,内存映射地址……这些难度较大,都能写一本书了。一般来说抄公版设计,应用标准的C库及kernel header(各平台会以补丁的形式发布)。用到时再讨论吧。
建议阅读youbest的CLFS2.0原理分析,原理基本差不多。具体编译参数,LFS中有几个参数用不到。主要是target host prefix withheader build等参数设对。