交叉编译工具的使用说明

写在前面的话,由于已经学习了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),通常包括:

  • o:目标文件(Object file,OBJ文件)
  • a:归档库文件(Archive file)

在编译过程中,除非使用了"-c","-S"或"-E"选项(或者编译错误阻止了完整的过程),否则最后的步骤总是连接。在连接阶段中,所有对应于源程序的.o文件,"-l"选项指定的库文件,无法识别的文件名(包括指定的".o"目标文件和".a"库文件)按命令行中的顺序传递给连接器。

以一个简单的"Hello, world!" C程序为例,它的代码如下,功能为打印"Hello World!"字符串。

/* File: hello.c */
#include <stdio.h>
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 <stdio.h>
#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

介绍完毕!

猜你喜欢

转载自blog.csdn.net/caihaitao2000/article/details/84169761