交叉编译的常识
制作交叉工具链,会涉及到三个架构的机器:
- build,哪个机器制作的这个编译器
- host,哪个机器将运行编译器
- target,编译器生成代码将会跑在哪个机器上
如果build == host == target,就叫native
编译器。这是最正常的情况。
如果build == host != target,就叫cross
编译器。这就是常规意义下的交叉编译器。
如果build != host != target,就叫canadian
编译器。据说在社区讨论这种情况的编译器时候,加拿大国会有三个党,于是就这样开玩笑了。。。这种情况一般用在Compile farm里面。编译农场或许就是一堆树莓派,它用来编译不同架构的软件包,制作这个编译器的或许就是某台高性能的x86机器。
其他情况也有不同的叫法,参考这个回答
对于Cortex M,有哪些工具链可选?
- optimize-uclinux这篇报告第34页简述了一些可用的编译器。
- Bootlin(以前叫Free electrons)公司收录了一堆常见CPU架构的不同版本的gcc工具链。不过armv7-m的工具链貌似不行。
- 社区推荐的arm2010-q1工具链有很强的可用性。
- arm官方维护的亲儿子gcc-arm-none-eabi可用用来开发裸机程序,基本没bug,适应的CPU又多。
- 包管理可用安装gcc-arm-none-eabi工具链,不过版本略旧与arm官网的gcc。
- 自己动手做arm-none-eabi或者arm-uclinuxeabi工具链。
个人意见:
- 如果要弄旧的uclinux或者弄busybox的话首选arm2010-q1的工具链,经过社区的验证很好;不过gcc 4.4稍微老了点。
- 开发裸机,首选arm官网的工具链。
- 如果碰到arm2010q1的工具链里面缺乏某些Linux头文件(比方说v4l2)的情况,或者要弄新版本的Linux内核,或者单纯想换新版本的gcc、新版本内核,那就自己动手丰衣足食吧。
首先,稍微研究一下arm-xxxxxxxx-gcc吧
编译一个应用程序需要经过编译、链接两步骤。编译这一步生成obj文件,这个文件里面已经是可执行的代码了,不过并没有启动、结束的代码,也没有库文件的代码,只是在需要调用库文件的函数的地方做了记号;链接这一步,将库文件里的函数复制到最终的elf文件里面,各个记号换成对应函数的地址,是谓“链接”。比方说这样一个简单的程序hello.c:
#include <stdio.h>
int main(void)
{
double d = 0.12345;
printf ("%lf\n", d*2.0);
return 0;
}
用下面的命令编译:
$ arm-uclinuxeabi-gcc -g -mthumb -march=armv7-m -c hello.c -o hello.o
这条命令其实是,arm-uclinuxeabi-gcc hello.c
,加上下列参数:
-g
,加入调试信息,这样的话addr2line就有用,反汇编时候可以看到C源码,而且gdb调试时候就可以知道现在程序跑到哪一行了。-c
,仅仅编译这个文件,而不链接最终的elf。制作库文件时候用处大。-o hello.o
,指定生成的文件名字叫hello.o。-mthumb -march=armv7-m
,生成armv7-m的thumb2模式的代码;不加这两个参数的话会默认生成arm模式的代码。
如果编译时候还加上-mfloat-abi=hard
这个参数,那么将会生成使用fpu的代码。
我们反汇编一下hello.o看看:
$ arm-uclinuxeabi-objdump -S hello.o
然后输出这些东西:
hello.o: 文件格式 elf32-littlearm
Disassembly of section .text:
00000000 <main>:
#include <stdio.h>
int main(void)
{
0: b590 push {r4, r7, lr}
2: b083 sub sp, #12
4: af00 add r7, sp, #0
double d = 0.12345;
6: a40c add r4, pc, #48 ; (adr r4, 38 <main+0x38>)
8: e9d4 3400 ldrd r3, r4, [r4]
c: e9c7 3400 strd r3, r4, [r7]
printf ("%lf\n", d*2.0);
10: e9d7 0100 ldrd r0, r1, [r7]
14: 4602 mov r2, r0
16: 460b mov r3, r1
18: f7ff fffe bl 0 <__aeabi_dadd>
1c: 4603 mov r3, r0
1e: 460c mov r4, r1
20: 461a mov r2, r3
22: 4623 mov r3, r4
24: 4806 ldr r0, [pc, #24] ; (40 <main+0x40>)
26: f7ff fffe bl 0 <printf>
return 0;
2a: 2300 movs r3, #0
}
2c: 4618 mov r0, r3
2e: 370c adds r7, #12
30: 46bd mov sp, r7
32: bd90 pop {r4, r7, pc}
34: f3af 8000 nop.w
38: 50b0f27c .word 0x50b0f27c
3c: 3fbf9a6b .word 0x3fbf9a6b
40: 00000000 .word 0x00000000
44: f3af 8000 nop.w
这是thumb2的汇编代码。可以看到,printf ("%lf\n", d*2.0)
引出了两个函数调用:__aeabi_dadd
和printf
;前者是为实现d*2.0
这个浮点数乘法的,后者就是格式化输出的。编译器还是挺聪明的,知道乘以2可以用加法代替。
使用fpu的代码反汇编时候长这样:
...
printf ("%lf\n", d*2.0);
10: ed97 7b00 vldr d7, [r7]
14: ee37 7b07 vadd.f64 d7, d7, d7
18: ec53 2b17 vmov r2, r3, d7
1c: 4804 ldr r0, [pc, #16] ; (14 <printf+0x14>)
1e: f7ff fffe bl 0 <printf>
...
可见就不需要调用__aeabi_dadd
了。
如果我们一步编译链接到头,对于arm-uclinuxeabi-gcc来说将会生成两个文件:
$ arm-uclinuxeabi-gcc -g -mthumb -march=armv7-m hello.c -o hello
$ ls
hello hello.c hello.gdb
# 看看他们是啥类型的文件
$ file ./*
./hello: BFLT executable - version 4 ram
./hello.c: C source, ASCII text
./hello.gdb: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
其中,hello
是最终下载到板子上运行的flt文件;hello.gdb
是一个正常的elf文件,有它就可以在上位机上运行gdb来调试了。对它反汇编可以看到上述的待链接的标号都已经填上了。
printf ("%lf\n", d*2.0);
c8: e9d7 0100 ldrd r0, r1, [r7]
cc: 4602 mov r2, r0
ce: 460b mov r3, r1
d0: f000 f81c bl 10c <__adddf3>
d4: 4603 mov r3, r0
d6: 460c mov r4, r1
d8: 461a mov r2, r3
da: 4623 mov r3, r4
dc: 4806 ldr r0, [pc, #24] ; (f8 <main+0x40>)
de: f000 f9c7 bl 470 <__GI_printf>
并且,最终的elf文件会多很多函数,这些都是库函数,比方说elf入口是_start
,在crt1.o里面定义;__adddf3
是浮点数的加法,在libgcc.a里面定义;等等。arm-none-eabi
工具链里面缺乏启动函数,所以不能编译这样的Linux下面的应用程序,只能编译裸机程序,并且需要一个链接文件来表明哪里是入口。STM32CubeMX生成的gcc工程是很好的例子。arm-uclinuxeabi
工具链里面预设了Linux应用程序的链接文件,上述hello.c可以编译过关并在uclinux平台下运行。当然它也可以弄裸机程序,此时也需要链接文件。
其实,arm的工具链可以生成不同架构的代码,关键就是-mxxxx
这些参数;可以用-print-multi-lib
参数来查看工具链支持哪些架构,比方说arm-none-eabi的工具链:
$ arm-none-eabi-gcc -print-multi-lib
.;
thumb;@mthumb
hard;@mfloat-abi=hard
thumb/v6-m;@mthumb@march=armv6s-m
thumb/v7-m;@mthumb@march=armv7-m
thumb/v7e-m;@mthumb@march=armv7e-m
thumb/v7-ar;@mthumb@march=armv7
thumb/v8-m.base;@mthumb@march=armv8-m.base
thumb/v8-m.main;@mthumb@march=armv8-m.main
thumb/v7e-m/fpv4-sp/softfp;@mthumb@march=armv7e-m@mfpu=fpv4-sp-d16@mfloat-abi=softfp
thumb/v7e-m/fpv4-sp/hard;@mthumb@march=armv7e-m@mfpu=fpv4-sp-d16@mfloat-abi=hard
thumb/v7e-m/fpv5-sp/softfp;@mthumb@march=armv7e-m@mfpu=fpv5-sp-d16@mfloat-abi=softfp
thumb/v7e-m/fpv5-sp/hard;@mthumb@march=armv7e-m@mfpu=fpv5-sp-d16@mfloat-abi=hard
thumb/v7e-m/fpv5/softfp;@mthumb@march=armv7e-m@mfpu=fpv5-d16@mfloat-abi=softfp
thumb/v7e-m/fpv5/hard;@mthumb@march=armv7e-m@mfpu=fpv5-d16@mfloat-abi=hard
thumb/v7-ar/fpv3/softfp;@mthumb@march=armv7@mfpu=vfpv3-d16@mfloat-abi=softfp
thumb/v7-ar/fpv3/hard;@mthumb@march=armv7@mfpu=vfpv3-d16@mfloat-abi=hard
thumb/v8-m.main/fpv5-sp/softfp;@mthumb@march=armv8-m.main@mfpu=fpv5-sp-d16@mfloat-abi=softfp
thumb/v8-m.main/fpv5-sp/hard;@mthumb@march=armv8-m.main@mfpu=fpv5-sp-d16@mfloat-abi=hard
thumb/v8-m.main/fpv5/softfp;@mthumb@march=armv8-m.main@mfpu=fpv5-d16@mfloat-abi=softfp
thumb/v8-m.main/fpv5/hard;@mthumb@march=armv8-m.main@mfpu=fpv5-d16@mfloat-abi=hard
解读几行:
- 不加参数,生成的是arm模式的代码;
- 加
-mthumb
,生成thumb代码; - 加
-mfloat-abi=hard
,生成使用硬件浮点数的代码; - 加
-mthumb -march=armv6s-m
,生成armv6-m的代码(对应Cortex-M0架构); - 加
-mthumb -march=armv7-m
,生成armv7-m的代码(对应Cortex-M3、Cortex-M4、Cortex-M7架构);
之所以能支持不同的架构,不仅是因为汇编器能生成对应的代码,还因为工具链的安装目录下面为不同架构预设了库文件。这些库文件放置在特定的几个地方,可以通过-print-search-dirs
来查看。不同的编译器略有不同。
对于arm-none-eabi-gcc:
$ arm-none-eabi-gcc -print-search-dirs
install: 某某某/lib/gcc/arm-none-eabi/6.3.1/
programs: =某某某/lib/gcc/arm-none-eabi/6.3.1/:某某某/lib/gcc/:某某某/lib/gcc/arm-none-eabi/6.3.1/../../../../arm-none-eabi/bin/arm-none-eabi/6.3.1/:某某某/lib/gcc/arm-none-eabi/6.3.1/../../../../arm-none-eabi/bin/
libraries: =某某某/lib/gcc/arm-none-eabi/6.3.1/:某某某/lib/gcc/:某某某/lib/gcc/arm-none-eabi/6.3.1/../../../../arm-none-eabi/lib/arm-none-eabi/6.3.1/:某某某/lib/gcc/arm-none-eabi/6.3.1/../../../../arm-none-eabi/lib/:某某某/arm-none-eabi/lib/arm-none-eabi/6.3.1/:某某某/arm-none-eabi/lib/:某某某/arm-none-eabi/usr/lib/arm-none-eabi/6.3.1/:某某某/arm-none-eabi/usr/lib/
对于社区推荐的arm2010q1工具链:
$ arm-uclinuxeabi-gcc -print-search-dirs
install: 某某某/lib/gcc/arm-uclinuxeabi/4.4.1/
programs: =某某某/libexec/gcc/arm-uclinuxeabi/4.4.1/:某某某/libexec/gcc/:某某某/lib/gcc/arm-uclinuxeabi/4.4.1/../../../../arm-uclinuxeabi/bin/arm-uclinuxeabi/4.4.1/:某某某/lib/gcc/arm-uclinuxeabi/4.4.1/../../../../arm-uclinuxeabi/bin/
libraries: =某某某/lib/gcc/arm-uclinuxeabi/4.4.1/:某某某/lib/gcc/:某某某/lib/gcc/arm-uclinuxeabi/4.4.1/../../../../arm-uclinuxeabi/lib/arm-uclinuxeabi/4.4.1/:某某某/lib/gcc/arm-uclinuxeabi/4.4.1/../../../../arm-uclinuxeabi/lib/:某某某/arm-uclinuxeabi/libc/lib/arm-uclinuxeabi/4.4.1/:某某某/arm-uclinuxeabi/libc/lib/:某某某/arm-uclinuxeabi/libc/usr/lib/arm-uclinuxeabi/4.4.1/:某某某/arm-uclinuxeabi/libc/usr/lib/
自己做的工具链:
$ arm-uclinuxeabi-gcc -print-search-dirs
install: 某某某/lib/gcc/arm-uclinuxeabi/6.4.0/
programs: =某某某/libexec/gcc/arm-uclinuxeabi/6.4.0/:某某某/libexec/gcc/:某某某/lib/gcc/arm-uclinuxeabi/6.4.0/../../../../arm-uclinuxeabi/bin/arm-uclinuxeabi/6.4.0/:某某某/lib/gcc/arm-uclinuxeabi/6.4.0/../../../../arm-uclinuxeabi/bin/
libraries: =某某某/lib/gcc/arm-uclinuxeabi/6.4.0/:某某某/lib/gcc/:某某某/lib/gcc/arm-uclinuxeabi/6.4.0/../../../../arm-uclinuxeabi/lib/arm-uclinuxeabi/6.4.0/:某某某/lib/gcc/arm-uclinuxeabi/6.4.0/../../../../arm-uclinuxeabi/lib/
库文件来源一般有两个:gcc自带库,以及libc。制作工具链时候二者都要编译,并安装到这些目录的其中之一里面。
不同架构的这些参数,取决于gcc编译时候的配置(以及你的实现,不过一般都已经实现了的)。arm-uclinuxeabi这里有坑,稍后叙述。其他通用参数可以参考gcc官网上ARM工具链的参数。
制作arm-uclinuxeabi工具链
不同交叉编译工具链,其实是配置出来的,毕竟编译器前端的词法语法语意优化什么的都与架构无关,而后端架构相关的部分都是不常变化的。源码里面早已有不同的配置,比方说arm-none-eabi、arm-elf、arm-uclinuxeabi等等。
想要制作一个可以编译Linux应用程序的交叉编译工具,需要这些源码:
- binutils,它包括有
as(汇编器)
、ld(链接器)
、ar(打包)
、objdump(反汇编器)
、objcopy(一般用来生成bin文件)
、addr2line(查询一个地址对应源码哪一行)
,还有一些别的工具; - gcc,它调用别的工具。代码里还有gcc自己的底层库,比方说进行浮点数运算等的库。
- Linux内核头文件,有了它你可以
#include <linux/xxxx.h>
。如果是裸机程序的话就不用。 - libc,c库,有了它你才可以
printf()
什么的。可以选择glibc(体积很大,功能齐全)、newlib(稍小,“嵌入式友好”)、uclibc(小,功能齐全,buildroot工程的默认c库)、其他的库。这些库性能各有不同。
如果要做的是arm-uclinuxeabi的工具链,因为它要生成flat可执行文件而不是传统的elf,还需要这个:
- elf2flt,uclinuxeabi配置的工具链在编译你的程序的最后一步,将elf转化为flt。编译器将会为应用程序生成一个flat文件和一个elf文件,后者专门用来gdb调试。
值得一提的是,网上资料说,制作交叉编译工具链,gcc需要编译两次:第一次生成基本gcc,用这个gcc去编译libc,最后第二次编译gcc;很多资料说两次编译都要配置,有的就说不用;但是如果两次都重新配置的话,gcc -v
会输出两次配置的参数,而现成的工具链都没有这样的输出,说明人家没有多次配置。而我经过多次编译失败后发现,gcc一次配置,分别编译gcc和libgcc两个模块,就可过关。
下载地址
- binutils-2.25;注意,binutils版本不要太高,亲测最新版本加入了armv8-m的支持,导致armv7-m生成的代码有问题,会触发usage fault。
- gcc-6.3.0;gcc还需要下载cloog,gmp,isl,mpc,mpfr。后面的几位如果不手动下载,gcc编译过程中会下载,国外的ftp下载的特别慢。。。
- uclibc-ng;ng代表new generation,老uclibc已经不再更新了。不同的libc编译方式不同,uclibc是
make menuconfig
、make install
这种风格的;uclinux推荐用这个。如果是newlib,则是./configure --target=xxxxxx ...
、make
、make install
这种风格的,而newlib并没有arm-uclinuxeabi的target,直接不能用。别的libc暂时没试过。。。 - 内核源码,不妨从清华软件源里挑选一个喜欢的内核版本;kernel.org那里下载速度慢。并不是说版本越新越好,因为理论上说,Linux内核abi向后兼容,而不是向前兼容,所以较旧的内核头文件理论上来说适用性更好。
- elf2flt;这是SourceForge下载页面,不能wget。。。
准备工作
首先,需要安装一些软件包。如果是Ubuntu的话,用这条命令就够了,无需逐个安装bison之类的软件包:
$ sudo apt-get build-dep gcc binutils
然后,新建你的安装目录和编译目录
$ mkdir 某某某安装目录 某某编译目录
将下载的源码解压到编译目录下面
$ tar xf balabalabala -C 某某编译目录
其中,将cloog、gmp、isl、mpc、mpfr这几个包都复制到gcc源码目录下面,重命名以去掉版本后缀
$ mv cloog-0.18.1/ gcc-6.3.0/cloog
$ mv gmp-6.1.2/ gcc-6.3.0/gmp
$ mv isl-0.18/ gcc-6.3.0/isl
$ mv mpc-1.0.3/ gcc-6.3.0/mpc
$ mv mpfr-3.1.6/ gcc-6.3.0/mpfr
binutils、gcc最好不要在源码目录下面编译。为此在它源码目录外面新建编译文件夹
$ mkdir build_binutils build_gcc
方便起见,建立几个环境变量
$ export PREFIX=某某某安装目录
$ export TARGET=arm-uclinuxeabi
$ export PATH=$PATH:$PREFIX/bin
TARGET
设为arm-uclinuxeabi,配置binutils、gcc时传入--target=$TARGET
,就可以配置出arm-uclinuxeabi-xxx的工具链啦!
binutils
在build_bintuils编译文件夹里面configure;只需要prefix和target参数就行了。然后编译安装
$ ../binutils-2.25/configure --prefix=$PREFIX --target=$TARGET
$ make -j4
$ make install
因为安装目录已经添加到PATH
环境变量,所以此时有arm-uclinuxeabi-as之类的命令了。
gcc
前面已经将cloog、gmp、isl、mpc、mpfr放在源码目录下面了,编译时候就不用慢吞吞的从国外ftp下载了。
不过配置之前要修改gcc源码的配置文件!在configure时候,我们使用--with-multilib-list
来指定一批不同架构的配置。方便起见,我们使用代码里面原有的rtems的配置,来生成不同架构下面的库文件,即—with-multilib-list=rtems
。
rtems配置有一大堆不同arm架构的东西,有些配置不需要并且编译不过(可能因为binutils比较老),比方说cortex m0的专门配置(要知道Cortex M0比Cortex M3晚出),比方说一堆乱七八糟的fpu,比方说R系列的芯片等等。这些都删掉。配置文件是gcc/config/arm/t-rtems
;注意下面的MULTILIB_REQUIRED
,我只留了armv7-m的少量配置,足够Cortex M3、M4用了。这些都会在安装目录下面某个库文件夹下单独生成文件夹。
# Custom RTEMS multilibs for ARM
MULTILIB_OPTIONS = mthumb march=armv7-m/mcpu=cortex-m7 mfpu=vfp/mfpu=fpv4-sp-d16 mfloat-abi=hard
MULTILIB_DIRNAMES = thumb armv7-m vfp fpv4-sp-d16 fpv5-d16 hard
# Enumeration of multilibs
MULTILIB_EXCEPTIONS =
MULTILIB_REQUIRED =
MULTILIB_REQUIRED += mfpu=vfp/mfloat-abi=hard
MULTILIB_REQUIRED += mthumb/march=armv7-m/mfpu=fpv4-sp-d16/mfloat-abi=hard
MULTILIB_REQUIRED += mthumb/march=armv7-m
MULTILIB_REQUIRED += mthumb
另外还要修改gcc/config.gcc
,因为默认情况下并不支持rtems的配置,详见大约第3812行处。这个case一旦落入了default,就会出来说Error。我们在前面加上一段:
...
rtems)
tmake_file="${tmake_file} arm/t-rtems"
break
;;
default)
;;
*)
...
其实,自己命名一个配置也是可以的。仿照现成的写一下就行了。
然后就可以开始愉快的编译了。
在build_gcc编译文件夹里面configure。
$ ../gcc-6.4.0/configure --prefix=$PREFIX --target=$TARGET \
--disable-nls --disable-libssp --disable-shared --disable-threads \
--with-gnu-as --with-gnu-ld --enable-multilib --with-system-zlib \
--enable-languages=c,c++ \
--with-host-libstdcxx='-static-libgcc -Wl,-Bstatic,-lstdc++,-Bdynamic -lm' \
--with-multilib-list=rtems
这些参数都是一步步试错而得来的。
然后,不能直接一股脑地make,要先编译gcc,再编译libgcc,最后install;不然的话会死在某些别的地方。
$ make -j4 all-gcc
$ make -j4 all-target-libgcc
$ make install-gcc
$ make install-target-libgcc
然后就有arm-uclinuxeabi-gcc的命令了。
内核头文件
解压内核源代码,然后安装头文件
$ make ARCH=arm INSTALL_HDR_PATH=$PREFIX/arm-uclinuxeabi headers_install
之所以装在这里,可详见上面arm-uclinuxeabi-gcc -print-search-dirs
的输出。
uclibc
uclibc使用kconfig,需要一个.config文件。这里需要注意的几个配置是:
KERNEL_HEADERS
,即刚刚安装内核头文件的include目录;DEVEL_PREFIX
,编译的c库文件安装在哪。注意这是一个相对路径,我填了/lib/gcc/arm-uclinuxeabi/6.4.0/
;RUNTIME_PREFIX
和MULTILIB_DIR
都不用设置;CROSS_COMPILER_PREFIX
,填arm-uclinuxeabi-
;UCLIBC_EXTRA_CFLAGS
,填-march=armv7-m -mthumb
,这样才能生成正确的thumb2指令,见本文开头的描述。
然后,照常编译、安装:
$ make
$ make install
elf2flt
只适用于arm-uclinuxeabi工具链,而且只有arm-uclinuxeabi需要它。
它使用configure脚本,需要告诉它之前编译的binutils里面的libbfd、libiberty在哪。
$ ./configure --prefix=$PREFIX --target=$TARGET \
--with-bfd-include-dir=某某某/build-binutils/bfd \
--with-libbfd=某某某/build-binutils/bfd/libbfd.a \
--with-libiberty=某某某/build-binutils/libiberty/libiberty.a \
--with-binutils-build-dir=某某某/binutils-2.29
$ make
$ make install