交叉编译的常识

制作交叉工具链,会涉及到三个架构的机器:

  • build,哪个机器制作的这个编译器
  • host,哪个机器将运行编译器
  • target,编译器生成代码将会跑在哪个机器上

如果build == host == target,就叫native编译器。这是最正常的情况。
如果build == host != target,就叫cross编译器。这就是常规意义下的交叉编译器。
如果build != host != target,就叫canadian编译器。据说在社区讨论这种情况的编译器时候,加拿大国会有三个党,于是就这样开玩笑了。。。这种情况一般用在Compile farm里面。编译农场或许就是一堆树莓派,它用来编译不同架构的软件包,制作这个编译器的或许就是某台高性能的x86机器。

其他情况也有不同的叫法,参考这个回答

对于Cortex M,有哪些工具链可选?

个人意见

  • 如果要弄旧的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_daddprintf;前者是为实现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还需要下载clooggmpislmpcmpfr。后面的几位如果不手动下载,gcc编译过程中会下载,国外的ftp下载的特别慢。。。
  • uclibc-ng;ng代表new generation,老uclibc已经不再更新了。不同的libc编译方式不同,uclibc是make menuconfigmake install这种风格的;uclinux推荐用这个。如果是newlib,则是./configure --target=xxxxxx ...makemake 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_PREFIXMULTILIB_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

大功告成