写在前面的话,由于已经学习了JZ2440V3开发板的裸机程序。想检验下学习成果,所以从今天开始把以前学的知识点在tiny4412开发板上面做个检验。裸机部分学习到把uboot移植完成就结束;然后,学习内核的驱动和其他子系统框架。言归正传,现在开始学习交叉编译工具链的使用。
源文件需要经过编译才能生成可执行文件。在Windows下进行开发时,只需要点几个按钮即可编译,集成开发环境(比如 Visual studio)已经将各种编译工具的使用封装好了。
Linux下也有很优秀的集成开发工具,但是更多的时候是直接以命令方式运行编译工具;即使使用集成开发工具,也需要掌握一些编译选项。
PC机上的编译工具链为gcc、 ld、 objcopy、 objdump等,它们编译出来的程序在x86平台上运行。要编译出能在ARM平台上运行的程序,必须使用交叉编译工具arm-linux-gcc、arm-linux-ld等,下面分别介绍。
1.arm-linux-gcc工具介绍
一个 C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和连接(linking)等4步才能变成可执行文件,如表1.1所示。在日常交流中通常使用"编译"统称这4个步骤,如果不是特指这4个步骤中的某一个,本书也依惯例使用"编译"这个统称。
1.1.预处理
C/C++源文件中,以"#"开头的命令被称为预处理命令,如包含命令"#include"、宏定义命令"#define"、条件编译命令"#if"、"#ifdef"等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个".i"文件中等待进一步处理。预处理将用到arm-linux-cpp工具。
1.2.编译
编译就是把 C/C++代码(比如上述的".i"文件)"翻译"成汇编代码,所用到的工具为cc1(它的名字就是 cc1,不是 arm-linux-cc1)。
1.3.汇编
汇编就是将第二步输出的汇编代码翻译成符合一定格式的机器代码,在Linux系统上一般表现为ELF目标文件(OBJ文件),用到的工具为arm-linux-as。"反汇编"是指将机器代码转换为汇编代码,这在调试程序时常常用到。
1.4.连接
连接就是将上步生成的OBJ文件和系统库的OBJ文件、库文件连接起来,最终生成了可以在特定平台运行的可执行文件,用到的工具为arm-linux-ld。
编译器利用这4个步骤中的一个或多个来处理输入文件,源文件的后缀名表示源文件所用的语言,后缀名控制着编译器的缺省动作,如表1.1。
后缀名 | 语言种类 | 后期操作 |
.c | C源程序 | 预处理、编译、汇编 |
.cpp | C++源程序 | 预处理、编译、汇编 |
.m | Objective-C源程序 | 预处理、编译、汇编 |
.i | 预处理后的C文件 | 编译、汇编 |
.ii | 预处理后的C++文件 | 编译、汇编 |
.s | 汇编语言源程序性 | 汇编 |
.S | 汇编语言源程序 | 预处理、汇编 |
.h | 预处理文件 | 通常不出现在命令行上 |
其他后缀名的文件被传递给连接器(linker),通常包括:
在编译过程中,除非使用了"-c","-S"或"-E"选项(或者编译错误阻止了完整的过程),否则最后的步骤总是连接。在连接阶段中,所有对应于源程序的.o文件,"-l"选项指定的库文件,无法识别的文件名(包括指定的".o"目标文件和".a"库文件)按命令行中的顺序传递给连接器。
以一个简单的"Hello, world!" C程序为例,它的代码如下,功能为打印"Hello World!"字符串。
/* File: hello.c */
#include
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
使用arm-linux-gcc,只需要一个命令就可以生成可执行文件hello,它包含了上述4个步骤:
arm-linux-gcc -o hello hello.c
加上"-v"选项, 即使用"arm-linux-gcc -v -o hello hello.c"命令可以观看编译的细节,下面摘取关键部分:
cc1 hello.c -o /tmp/cctETob7.s
as -o /tmp/ccvv2KbL.o /tmp/cctETob7.s
collect2 -o hello crt1.o crti.o crtbegin.o /tmp/ccvv2KbL.o crtend.o crtn.o
以上3个命令分别对应于编译步骤中的预处理、编译、汇编和连接, ld被collect2调用来连接程序。预处理和编译被放在了一个命令(cc1)中进行的,可以把它再次拆分为以下两步:
cpp -o hello.i hello.c
cc1 hello.i -o /tmp/cctETob7.s
可以通过各种选项来控制arm-linux-gcc的动作,下面介绍一些常用的选项:
总体选项:
-c选项:预处理、编译和汇编源文件,但是不作连接,编译器根据源文件生成OBJ文件。缺省情况下,GCC通过用".o"替换源文件名的后缀".c",".i",".s"等,产生OBJ文件名。可以使用-o选项选择其他名字。GCC忽略-c选项后面任何无法识别的输入文件。
-S选项:编译后即停止,不进行汇编。对于每个输入的非汇编语言文件,输出结果是汇编语言文件。缺省情况下,GCC通过用".s"替换源文件名后缀".c",".i"等等,产生汇编文件名。可以使用-o选项选择其他名字。GCC忽略任何不需要汇编的输入文件。
-E选项:预处理后即停止,不进行编译。预处理后的代码送往标准输出。GCC忽略任何不需要预处理的输入文件。
-o选项:指定输出文件为file。无论是预处理、编译、汇编还是连接,这个选项都可以使用。如果没有使用'-o'选项,默认的输出结果是:可执行文件为'a.out';修改输入文件的名称是'source.suffix',则它的OBJ文件是'source.o',汇编文件是'source.s',而预处理后的C源代码送往标准输出。
-v选项:显示制作GCC工具自身时的配置命令;同时显示编译器驱动程序、预处理器、编译器的版本号。
以一个程序为例,它包含三个文件,下面列出源码:
//File: main.c
#include
#include "sub.h"
int main(int argc, char *argv[])
{
int i;
printf("Main fun!\n");
sub_fun();
return 0;
}
//File: sub.h
void sub_fun(void);
//File: sub.c
void sub_fun(void)
{
printf("Sub fun!\n");
}
arm-linux-gcc、arm-linux-ld等工具与gcc、ld等工具的使用方法相似,很多选项是一样的。本节使用gcc、ld等工具进行编译、连接,这样可以在PC上直接看到运行结果。使用上面介绍的选项进行编译,命令如下:
$ gcc -c -o main.o main.c
$ gcc -c -o sub.o sub.c
$ gcc -o test main.o sub.o
其中,main.o、sub.o是经过了预处理、编译、汇编后生成的OBJ文件,它们还没有被连接成可执行文件;最后一步将它们连接成可执行文件test,可以直接运行以下命令:
$ ./test
Main fun!
Sub fun!
现在试试其他选项,以下命令生成的main.s是main.c的汇编语言文件:
gcc -S -o main.s main.c
以下命令对main.c进行预处理,并将得到的结果打印出来。里面扩展了所有包含的文件、所有定义的宏。在编写程序时,有时候查找某个宏定义是非常繁琐的事,可以使用'-dM –E'选项来查看。命令如下:
$ gcc -E main.c
警告选项:
-Wall选项:这个选项基本打开了所有需要注意的警告信息,比如没有指定类型的声明、在声明之前就使用的函数、局部变量除了声明就没再使用等。
上面的main.c文件中,第6行定义的变量i没有被使用,但是使用"gcc –c –o main.o main.c"进行编译时并没有出现提示。
可以加上-Wall选项,例子如下:
$ gcc -Wall -c main.c
执行上述命令后,得到如下警告信息:
main.c: In function `main':
main.c:6: warning: unused variable `i
这个警告虽然对程序没有坏的影响,但是有些警告需要加以关注,比如类型匹配的警告等。
调试选项:
-g选项:以操作系统的本地格式(stabs,COFF,XCOFF,或DWARF)产生调试信息,GDB能够使用这些调试信息。在大多数使用stabs格式的系统上,'-g'选项加入只有GDB才使用的额外调试信息。可以使用下面的选项来生成额外的信息:'-gstabs+','-gstabs','-gxcoff+','-gxcoff','-gdwarf+'或'-gdwarf',具体用法请读者参考GCC手册。
优化选项:
-O或-O1选项:优化:对于大函数,优化编译的过程将占用稍微多的时间和相当大的内存。不使用"-O"或"-O1"选项的目的是减少编译的开销,使编译结果能够调试、语句是独立的;如果在两条语句之间用断点中止程序,可以对任何变量重新赋值,或者在函数体内把程序计数器指到其他语句,以及从源程序中精确地获取你所期待的结果。
-O2选项:多优化一些。除了涉及空间和速度交换的优化选项,执行几乎所有的优化工作。例如不进行循环展开(loop unrolling)和函数内嵌(inlining)。和'-O'或'-O1'选项比较,这个选项既增加了编译时间,也提高了生成代码的运行效果。
-O3选项:优化的更多。除了打开-O2所做的一切,它还打开了-finline-functions选项。
-O0选项:不优化。
如果指定了多个-O选项,不管带不带数字,生效的是最后一个选项。在一般应用中,经常使用-O2选项,比如对于options程序:
$ gcc -O2 -c -o main.o main.c
$ gcc -O2 -c -o sub.o sub.c
$ gcc -o test main.o sub.o
连接器选项:
下面的选项用于连接OBJ文件,输出可执行文件或库文件。
object-file-name选项:如果某些文件没有特别明确的后缀(a special recognized suffix),GCC就认为他们是OBJ文件或库文件(根据文件内容,连接器能够区分 OBJ 文件和库文件)。如果GCC执行连接操作,这些OBJ文件将成为连接器的输入文件。
比如上面的"gcc -o test main.o sub.o"中,main.o、sub.o就是输入的文件。
-llibrary选项:连接名为library的库文件。连接器在标准搜索目录中寻找这个库文件,库文件的真正名字是'liblibrary.a'。搜索目录除了一些系统标准目录外,还包括用户以'-L'选项指定的路径。
目录选项:
下列选项指定搜索路径,用于查找头文件,库文件,或编译器的某些成员。
-Idir选项:在头文件的搜索路径列表中添加dir目录。
头文件的搜索方法为:如果以"#include<>"包含文件,则只在标准库目录开始搜索(包括使用-Idir选项定义的目录);如以"#include “” "包含文件,则先从用户的工作目录开始搜索,再搜索标准库目录。
1.2.arm-linux-ld工具介绍
arm-linux-ld用于将多个目标文件、库文件连接成可执行文件,它的大多数选项已经在上面介绍过了。
本小节介绍'-T'选项,可以直接使用它来指定代码段、数据段、bss段的起始地址,也可以用来指定一个连接脚本,在连接脚本中进行更复杂的地址设置。
'-T'选项只在连接Bootloader、内核等“没有底层软件支持”的软件;连接运行于操作系统之上的应用程序时,无需指定`-T’ 选项,它们使用默认的方式进行连接。
直接指定代码段、数据段、bss段的起始地址:
格式如下:
-Ttext startaddr
-Tdata startaddr
-Tbss startaddr
其中的'startaddr'分别表示代码段、数据段和bss段的起始地址,它是一个16进制数。示例:
arm-linux-ld -Ttext 0x0000000 -g led_on.o -o led_on_elf
它表示代码段的运行地址为0x0000000,由于没有定义数据段、bss段的起始地址,它们被依次放在代码段的后面。
使用连接脚本设置地址:
示例,它的Makefile中有这一句:
arm-linux-ld -Ttimer.lds -o timer_elf $^
其中的'$^'表示"head.o init.o interrupt.o main.o"(为何如此暂时不用管),所以这句代码就变为:
arm-linux-ld -Ttimer.lds -o timer_elf head.o init.o interrupt.o main.o
它使用连接脚本timer.lds来设置可执行文件timer_elf的地址信息,timer_elf文件内容如下:
SECTIONS {
. = 0x30000000;
.text : { *(.text) }
.rodata ALIGN(4) : {*(.rodata)}
.data ALIGN(4) : { *(.data) }
.bss ALIGN(4) : { *(.bss) *(COMMON) }
}
解析timer_elf文件之前,先讲解连接脚本的格式。连接脚本的基本命令是SECTIONS命令,它描述了输出文件的"映射图":输出文件中各段、各文件怎么放置。一个SECTIONS命令内部包含一个或多个段,段(Section)是连接脚本的基本单元,它表示输入文件中的某部分怎么放置。
完整的连接脚本格式如下,它的核心部分是段(Section):
SECTIONS {
...
secname start ALIGN(align) (NOLOAD) : AT(ldadr)
{ contents } >region :phdr =fill
...
}
secname和contents是必须的,前者用来命名这个段,后者用来确定代码中的什么部分放在这个段中。
start是这个段重定位地址,也称为运行地址。如果代码中有位置相关的指令,程序在运行时,这个段必须放在这个地址上。
ALIGN(align):虽然start指定了运行地址,但是仍可以使用 BLOCK(align)来指定对齐的要求──这个对齐的地址才是真正的运行地址。
(NOLOAD):用来告诉加载器,在运行时不用加载这个段。显然,这个选项只有在有操作系统的情况下才有意义。
AT(ldadr):指定这个段在编译出来的映像文件中的地址──加载地址(load address)。如果不使用这个选项,则加载地址等于运行地址。通过这个选项,可以控制各段分别保存输出文件中不同的位置,便于把文件保存到到单板上:A段放在A处,B段放在B处,运行前再把A、B段分别读出来组装成一个完整的执行程序。
现在,可以明白前面的连接脚本timer.lds的含义了:
第2行表示设置"当前运行地址"为0x30000000。
第3行定义了一个名为".text"的段,它的内容为"*(.text)",表示所有输入文件的代码段。这些代码段被集合在一起,起始运行地址为0x30000000。
第 4 行定义了一个名为".rodata"的段,在输出文件timer_elf中,它紧挨着".text"段存放。其中的"ALIGN(4)"表示起始运行地址为4字节对齐。假设前面".text"段的地址范围是0x30000000~0x300003f1,则".rodata"段的地址是4字节对齐后的0x300003f4。
第5、6行的含义与第4行类似。
1.3.arm-linux-objcopy工具介绍
arm-linux-objcopy被用来拷贝一个目标文件的内容到另一个文件中,可以使用不同于源文件的格式来输出目的文件,即可以进行格式转换。
在本书中,常用arm-linux-objcopy来将ELF格式的可执行文件转换为二进制文件。arm-linux-objcopy的使用格式如下:
arm-linux-objcopy [ -F bfdname | --target=bfdname ]
[ -I bfdname | --input-target=bfdname ]
[ -O bfdname | --output-target= bfdname ]
[ -S | --strip-all ] [ -g | --strip-debug ]
[ -K symbolname | --keep-symbol= symbolname ]
[ -N symbolname | --strip-symbol= symbolname ]
[ -L symbolname | --localize-symbol= symbolname ]
[ -W symbolname | --weaken-symbol= symbolname ]
[ -x | --discard-all ] [ -X | --discard-locals ]
[ -b byte | --byte= byte ]
[ -i interleave | --interleave= interleave ]
[ -R sectionname | --remove-section= sectionname ]
[ -p | --preserve-dates ] [ --debugging ]
[ --gap-fill= val ] [ --pad-to= address ]
[ --set-start= val ] [ --adjust-start= incr ]
[ --change-address= incr ]
[ --change-section-address= section{=,+,-} val ]
[ --change-warnings ] [ --no-change-warnings ]
[ --set-section-flags= section= flags ]
[ --add-section= sectionname= filename ]
[ --change-leading char ] [--remove-leading-char ]
[ --weaken ]
[ -v | --verbose ] [ -V | --version ] [ --help ]
input-file [ outfile ]
下面讲解常用的选项:
input-file、outfile选项:参数input-file和outfile分别表示输入目标文件(源目标文件)和输出目标文件(目的目标文件)。如果在命令行中没有明确地指定outfile,那么arm-linux-objcopy将创建一个临时文件来存放目标结果,然后使用input-file的名字来重命名这个临时文件(这时候,原来的input-file将被覆盖)。
-I bfdname或--input-target=bfdname选项:用来指明源文件的格式,bfdname是BFD库中描述的标准格式名。如果不指明源文件格式,arm-linux-objcopy会自己去分析源文件的格式,然后去和BFD中描述的各种格式比较,从而得知源文件的目标格式名。
-O bfdname或--output-target= bfdname选项:使用指定的格式来输出文件,bfdname是BFD库中描述的标准格式名。
-F bfdname或--target= bfdname选项:同时指明源文件、目的文件的格式。将源目标文件中的内容拷贝到目的目标文件的过程中,只进行拷贝不做格式转换,源目标文件是什么格式,目的目标文件就是什么格式。
-R sectionname或--remove-section= sectionname选项:从输出文件中删掉所有名为sectionname的段。这个选项可以多次使用。
-S 或--strip-all选项:不从源文件中拷贝重定位信息和符号信息到目标文件中去。
-g 或--strip-debug选项:不从源文件中拷贝调试符号到目标文件中去。
在编译bootloader、内核时,常用arm-linux-objcopy命令将ELF格式的生成结果转换为二进制文件,比如:
$ arm-linux-objcopy -O binary -S elf_file bin_file
1.4.arm-linux-objdump工具介绍:
arm-linux-objdump用于显示二进制文件信息,本书中常用来查看反汇编代码。使用格式如下:
arm-linux-objdump [-a] [-b bfdname | --target=bfdname]
[-C] [--debugging]
[-d] [-D]
[--disassemble-zeroes]
[-EB|-EL|--endian={big|little}] [-f]
[-h] [-i|--info]
[-j section | --section=section]
[-l] [-m machine ] [--prefix-addresses]
[-r] [-R]
[-s|--full-contents] [-S|--source]
[--[no-]show-raw-insn] [--stabs] [-t]
[-T] [-x]
[--start-address=address] [--stop-address=address]
[--adjust-vma=offset] [--version] [--help]
objfile...
下面讲解常用的选项:
-b bfdname或--target=bfdname选项:指定目标码格式。这不是必须的,arm-linux-objdump能自动识别许多格式。可以使用
"arm-linux-objdump –i"命令查看支持的目标码格式列表。
--disassemble或-d选项:反汇编可执行段(executable sections)。
--disassemble-all或-D选择:与-d 类似,反汇编所有段。
-EB或-EL或--endian={big|little}选项:指定字节序。
--file-headers或-f选项:显示文件的整体头部摘要信息。
--section-headers、--headers或-h选项:显示目标文件各个段的头部摘要信息。
--info或-i选项:显示支持的目标文件格式和CPU架构,它们在"-b"、"-m"选项中用到。
--section=name或-j name选项:仅仅显示指定section的信息。
在调试程序时,常用arm-linux-objdump命令来得到汇编代码。使用这两个命令:
将ELF格式的文件转换为反汇编文件:
$ arm-linux-objdump -D elf_file > dis_file
将二进制文件转换为反汇编文件:
$ arm-linux-objdump -D -b binary -m arm bin_file > dis_file
介绍完毕!