版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。
Copyright (C) 2021 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
2.1 嵌入式开发之交叉编译
2.1.1 交叉编译介绍
在 X86 架构 Linux 系统下进行 C 程序开发时, 我们使用系统的 gcc 编译器进行代码的编译, 编译生成的可执行程序直接在 X86 架构下的 PC 下运行的,这个过程叫做 本地编译 (Native Compile) 。 而如果该C程序想要编译出来后放到ARM处理器架构的系统上运行, 则需要在 X86 架构Linux系统下使用支持 ARM 的编译器编译, 这个编译器我们通常称为 交叉编译器 (Cross Compiler)。
而在一种平台上编译出能在另外一种体系结构完全不同处理器上运行程序的编译过程,叫做 交叉编译 (Cross Compile)。比如在PC平台(X86 CPU)上编译出能运行在以ARM为内核的CPU平台上的程序,编译得到的程序在X86 CPU平台上是不能运行的,必须放到ARM CPU平台上才能运行,虽然两个平台用的都是Linux系统。
交叉编译工具链是一个由编译器、连接器和解释器组成的综合开发环境,交叉编译工具链主要由binutils、gcc和glibc三个部分组成。有时出于减小 libc 库大小的考虑,也可以用别的 c 库来代替 glibc,例如 uClibc 或 newlib。
之所以几乎所有的ARM开发板开发都选择交叉编译,这是因为这些开发板生产出来后并没有系统,这时需要在PC上使用交叉编译器交叉编译操作系统源码,为它构建一个完整的 Linux 系统。另外,由于CPU处理能力、外存和内存存储空间的大小限制,它们不足以能够运行 gcc 编译环境,所以嵌入式开发绝大部分的过程都是交叉编译。
2.1.2 常用的交叉编译器
Ubuntu交叉编译器
Ubuntu之所以能成为嵌入式系统开发的首选Linux发行版本,正是因为它的软件包在线安装仓库中包含有海量的开发工具/软件,其中就包括嵌入式系统开发所需的交叉编译器、dtc等开发工具,这也就是为什么几乎所有主流的半导体厂商在发布SDK时都推荐使用Ubuntu系统。
在Ubuntu 系统中提供了如下四个版本的交叉编译器软件包:
gcc-arm-linux-gnueabi —- armel,ARM EABI Little-endian
gcc-arm-linux-gnueabihf —- armhf,ARM Hard Float
gcc-aarch64-linux-gnu —- arm64,用于编译64位ARM处理器系统
gcc-arm-none-eabi —- bare metal, 用于编译ARM架构的单片机程序,使用的是newlib库
在上面的软件包中,gcc-arm-none-eabi 和 gcc-aarch64-linux-gnu 都比较好理解。而 gcc-arm-linux-gnueabi 与 gcc-arm-linux-gnueabihf 有什么区别呢?这就涉及到 ARM 处理器架构中的浮点运算相关知识了。
出于低功耗、封装限制等种种原因,以前的一些ARM处理器没有独立的硬件浮点运算单元,需要使用软件来实现浮点运算。随着技术发展,现在高端的ARM处理器基本都具备了硬件执行浮点操作的能力。这样,新旧两种架构之间的差异,就产生了两个不同的嵌入式应用程序二进制接口(EABI)——软浮点(SoftFP)与矢量浮点(VFP)。但是软浮点(soft float)和硬浮点(hard float)之间有向前兼容却没有向后兼容的能力,也就是软浮点的二进制接口(EABI)仍然可以用于当前的高端ARM处理器。
在ARM体系架构内核中,有些有浮点运算单元(fpu,floating point unit),有些没有。对于没有fpu内核,是不能使用armel和armhf的。在有fpu的情况下,就可以通过gcc的选项-mfloat-abi来指定使用哪种,有如下四种值:
soft:不用fpu计算,即使有fpu浮点运算单元也不用。
armel:(arm eabi little endian)也即softfp,用fpu计算,但是传参数用普通寄存器传,这样中断的时候,只需要保存普通寄存器,中断负荷小,但是参数需要转换成浮点的再计算。
armhf:(arm hard float)也即hard,用fpu计算,传参数用fpu中的浮点寄存器传,省去了转换性能最好,但是中断负荷高。
arm64:64位的arm默认就是hard float的,因此不需要hf的后缀。
在之前的EABI中,armel(低端ARM硬件)在执行浮点运算之前,浮点参数必须首先通过整数寄存器,然后传递到浮点运算单元。新的EABI ,也就是armhf,通过直接传递参数到浮点寄存器优化了浮点运算的调用约定。相比我们熟悉的armel,armhf代表了另一种不兼容的二进制标准。
在一些社区的支持下,armhf目前已经得到了很大的发展。像Ubuntu,已经计划在之后的发行版中放弃armel,转而支持armhf编译的版本。正如目前依然很火热的Raspberry Pi(早期版本使用ARM11),由于ubuntu只支持armv7架构的编译,Raspberry Pi将不能直接安装ubuntu系统。而 BB Black(Cortex-A8)和 Cubietruct(Cortex-A7)则同时支持ubuntu的armel与armhf的编译。
kernel、rootfs和app编译的时候,指定的必须保持一致才行。使用softfp模式,会存在不必要的浮点到整数、整数到浮点的转换。而使用hard模式,在每次浮点相关函数调用时,平均能节省20个CPU周期。对ARM这样每个周期都很重要的体系结构来说,这样的提升无疑是巨大的。在完全不改变源码和配置的情况下,在一些应用程序上,虽然armhf比armel硬件要求(确切的是指fpu硬件)高一点,但是armhf能得到20-25%的性能提升。对一些严重依赖于浮点运算的程序,更是可以达到300%的性能提升。
因为我们的 i.MX6ULL 处理器带有硬件浮点FPU,所以这里我们选择安装硬浮点交叉编译器。下面安装的 gcc 的是 C 程序编译器,而 g++ 则是 C++ 程序的编译器。
guowenxue@ubuntu20:~$ sudo apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-gcc -v
Using built-in specs.
COLLECT_GCC=arm-linux-gnueabihf-gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc-cross/arm-linux-gnueabihf/9/lto-wrapper
Target: arm-linux-gnueabihf
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libitm --disable-libquadmath --disable-libquadmath-support --enable-plugin --enable-default-pie --with-system-zlib --without-target-system-zlib --enable-libpth-m2 --enable-multiarch --enable-multilib --disable-sjlj-exceptions --with-arch=armv7-a --with-fpu=vfpv3-d16 --with-float=hard --with-mode=thumb --disable-werror --enable-multilib --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=arm-linux-gnueabihf --program-prefix=arm-linux-gnueabihf- --includedir=/usr/arm-linux-gnueabihf/include
Thread model: posix
gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04)
ARM官方交叉编译器
在NXP的官方开发文档中,其推荐使用ARM官方提供的交叉编译器。为与此前各版本Linux系统移植所用交叉编译器保持兼容,我们选择 gcc-arm-10.3-2021.07 这个版本,点此链接进入其下载页面。
因为我们是在 X86_64 位 Ubuntu 服务器来做开发,而 IMX6ULL 处理器是带硬件浮点的 32位处理器,所以我们需要下载 gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf.tar.xz 这个文件。
guowenxue@ubuntu20:~$ wget https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel//gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf.tar.xz
接下来我们将其解压缩并安装到 /opt 路径下,并重命名为 gcc-aarch32-10.3-2021.07。
guowenxue@ubuntu20:~$ sudo tar -xJf gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf.tar.xz -C /opt/
guowenxue@ubuntu20:~$ sudo mv /opt/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf/ /opt/gcc-aarch32-10.3-2021.07/
使用下面命令可以查看所安装的交叉编译器版本信息。
guowenxue@ubuntu20:~$ /opt/gcc-aarch32-10.3-2021.07/bin/arm-none-linux-gnueabihf-gcc -v
Using built-in specs.
COLLECT_GCC=/opt/gcc-aarch32-10.3-2021.07/bin/arm-none-linux-gnueabihf-gcc
COLLECT_LTO_WRAPPER=/opt/gcc-aarch32-10.3-2021.07/bin/../libexec/gcc/arm-none-linux-gnueabihf/10.3.1/lto-wrapper
Target: arm-none-linux-gnueabihf
Configured with: /data/jenkins/workspace/GNU-toolchain/arm-10/src/gcc/configure --target=arm-none-linux-gnueabihf --prefix= --with-sysroot=/arm-none-linux-gnueabihf/libc --with-build-sysroot=/data/jenkins/workspace/GNU-toolchain/arm-10/build-arm-none-linux-gnueabihf/install//arm-none-linux-gnueabihf/libc --with-bugurl=https://bugs.linaro.org/ --enable-gnu-indirect-function --enable-shared --disable-libssp --disable-libmudflap --enable-checking=release --enable-languages=c,c++,fortran --with-gmp=/data/jenkins/workspace/GNU-toolchain/arm-10/build-arm-none-linux-gnueabihf/host-tools --with-mpfr=/data/jenkins/workspace/GNU-toolchain/arm-10/build-arm-none-linux-gnueabihf/host-tools --with-mpc=/data/jenkins/workspace/GNU-toolchain/arm-10/build-arm-none-linux-gnueabihf/host-tools --with-isl=/data/jenkins/workspace/GNU-toolchain/arm-10/build-arm-none-linux-gnueabihf/host-tools --with-arch=armv7-a --with-fpu=neon --with-float=hard --with-mode=thumb --with-arch=armv7-a --with-pkgversion='GNU Toolchain for the A-profile Architecture 10.3-2021.07 (arm-10.29)'
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 10.3.1 20210621 (GNU Toolchain for the A-profile Architecture 10.3-2021.07 (arm-10.29))
因为该交叉编译器并没有安装到Linux的标准系统路径下,这样每次使用交叉编译器时都要使用绝对路径。我们可以在 Bash Shell 的默认配置文件 ~/.bashrc 中,将交叉编译器的路径添加到 PATH 环境变量中去。
guowenxue@ubuntu20:~$ vim ~/.bashrc
export PATH=$PATH:/opt/gcc-aarch32-10.3-2021.07/bin/
重启 Shell 或者使用 source 命令使配置文件生效后,就可以不用绝对路径直接使用交叉编译器了。
guowenxue@ubuntu20:~$ source ~/.bashrc
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-gcc -v
Using built-in specs.
... ...
gcc version 10.3.1 20210621 (GNU Toolchain for the A-profile Architecture 10.3-2021.07 (arm-10.29))
2.1.3 交叉编译测试
接下来我们以大家熟悉的 hello world 程序为例,讲解嵌入式交叉编译过程。
2.1.3.1 本地编译运行hello程序
首先,我们使用 vim 编辑器编写 hello.c 测试程序:
guowenxue@ubuntu20:~$ vim hello.c
/*********************************************************************************
* Copyright: (C) 2021 LingYun IoT System Studio
* All rights reserved.
*
* Filename: hello.c
* Description: This file is hello world test program.
*
* Version: 1.0.0(2021-12-10)
* Author: Guo Wenxue <guowenxue@gmail.com>
* ChangeLog: 1, Release initial version on "2021-12-10 22:41:49"
*
********************************************************************************/
#include <stdio.h>
int main (int argc, char **argv)
{
printf("Hello, LingYun IoT System Studio.\n");
return 0;
}
我们知道,C程序必须要使用编译器编译生成可执行程序,才能执行。接下来我们使用 Linux服务器上的 gcc 编译该程序并运行:
guowenxue@ubuntu20:~$ gcc hello.c -o hello
guowenxue@ubuntu20:~$ ./hello
Hello, LingYun IoT System Studio.
我们可以使用 file 命令查看 hello 程序的相关信息,由此可知该程序应该在X86-64位的系统上运行:
guowenxue@ubuntu20:~$ file hello
hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=848c2886fa91f39e64ed699ed02c11ed532cf760, for GNU/Linux 3.2.0, not stripped
我们将该程序下载到开发板上运行试试看,这时系统会提示可执行文件格式出错,不能正常运行。
root@imx6ull:~# tftp -gr hello 192.168.2.2
root@imx6ull:~# chmod a+x hello
root@imx6ull:~# ./hello
-bash: ./hello: cannot execute binary file: Exec format error
2.1.3.2 交叉编译运行hello程序
我们知道C程序具有可移植性,使用PC上的编译器编译生成的程序应该在PC上运行,而不能在其他处理器架构上运行,那怎样让 hello.c 程序在ARM开发板上运行呢?这里就需要使用ARM的交叉编译器来对该程序进行交叉编译,这样编译输出的可执行程序就能在ARM开发板上运行了。
接下来,我们使用Ubuntu系统的 ARM交叉编译器编译该 hello.c 程序,并尝试在 X86-64位 Linux服务器上运行,我们会发现 Linux 服务器上默认并不能运行该程序:
guowenxue@ubuntu20:~$ arm-linux-gnueabihf-gcc hello.c -o hello
guowenxue@ubuntu20:~$ ./hello
/lib/ld-linux-armhf.so.3: No such file or directory
接下来我们使用 file 命令查看该程序的相关信息,由此可知该程序应该在ARM处理器系统上运行:
guowenxue@ubuntu20:~$ file hello
hello: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, BuildID[sha1]=fd113c370ad4fd166fc594d3af3de0b9f36f27e4, for GNU/Linux 3.2.0, not stripped
接下来我们在IGKBoard开发板上使用 scp
命令,将服务器上的程序拷贝到开发板上来。
root@igkboard:~# scp -P 2288 guowenxue@192.168.2.2:~/hello .
guowenxue@192.168.2.2's password:
hello 100% 13KB 758.4KB/s 00:00
scp 是SSH中最方便有用的命令了,他可以在两个系统之间通过SCP协议直接传送文件;
-P 2202 如果目标服务器的 SSH2 协议端口不是默认的22号端口,则需要通过该选项指定端口号;
guowenxue@192.168.2.2 这是 SSH2 协议登录的服务器地址和用户名;
:~/hello 此后跟冒号(:) 并加路径名表示要拷贝的目标文件,这里是指 guowenxue 这个账号主目录(~)下的 hello 文件。如果是文件夹的话,注意scp命令要加上 -r 选项。
. 表示将目标文件或文件夹拷贝到当前路径下。
接下来,我们给该程序相应的执行权限,就可以运行了。
root@igkboard:~# chmod a+x hello
root@igkboard:~# ./hello
Hello, LingYun IoT System Studio.
由此可见:
C程序如果想在PC上运行,则应该用PC的编译器来编译;而该程序想要在ARM开发板上运行,则必须用ARM的交叉编译器对源码重新进行交叉编译;
C程序具有可移植性是指,C程序源代码不用作任何的修改,使用不同的编译器编译生成的可执行程序可以在不同的处理器架构平台上运行;
2.1.4 交叉编译器链介绍
通常编译工具链由编译器、链接器和解释器构成,具体到组件上是由Binutils、GCC、Glibc和GDB构成的。我们看看ARM 官方的交叉编译器链里都提供了哪些文件。
guowenxue@ubuntu20:~$ ls /opt/gcc-aarch32-10.3-2021.07/bin/
arm-none-linux-gnueabihf-addr2line arm-none-linux-gnueabihf-elfedit arm-none-linux-gnueabihf-gcov arm-none-linux-gnueabihf-ld arm-none-linux-gnueabihf-ranlib
arm-none-linux-gnueabihf-ar arm-none-linux-gnueabihf-g++ arm-none-linux-gnueabihf-gcov-dump arm-none-linux-gnueabihf-ld.bfd arm-none-linux-gnueabihf-readelf
arm-none-linux-gnueabihf-as arm-none-linux-gnueabihf-gcc arm-none-linux-gnueabihf-gcov-tool arm-none-linux-gnueabihf-ld.gold arm-none-linux-gnueabihf-size
arm-none-linux-gnueabihf-c++ arm-none-linux-gnueabihf-gcc-10.3.1 arm-none-linux-gnueabihf-gdb arm-none-linux-gnueabihf-lto-dump arm-none-linux-gnueabihf-strings
arm-none-linux-gnueabihf-c++filt arm-none-linux-gnueabihf-gcc-ar arm-none-linux-gnueabihf-gdb-add-index arm-none-linux-gnueabihf-nm arm-none-linux-gnueabihf-strip
arm-none-linux-gnueabihf-cpp arm-none-linux-gnueabihf-gcc-nm arm-none-linux-gnueabihf-gfortran arm-none-linux-gnueabihf-objcopy
arm-none-linux-gnueabihf-dwp arm-none-linux-gnueabihf-gcc-ranlib arm-none-linux-gnueabihf-gprof arm-none-linux-gnueabihf-objdump
下表列出了交叉编译器链中各个工具的作用:
工具名 |
工具说明 |
---|---|
gcc |
C程序源码编译前端工具,它会调用Binutils提供的工具来对源码进行预处理、编译、汇编、最后链接生成可执行文件 |
g++ |
C++程序源码编译前端工具,它会调用Binutils提供的工具来对源码进行预处理、编译、汇编、最后链接生成可执行文件 |
cpp |
C程序预处理器(C preprocessor) |
as |
该工具用来将汇编源码汇编成目标机器代码.o文件 |
ar |
该工具用来将多个可重定位的.o文件归档为一个静态库.a文件 |
ranlib |
产生归档.a文件索引,并将其保存到这个归档文件中,因为 ar 命令支持该特性,所以现在很少使用了 |
ld |
链接器,用来将多个目标文件.o、静态库.a文件、动态库.so 文件链接生成一个可执行文件 |
readelf |
列出 ELF 格式可执行文件的相关信息 |
nm |
列出目标文件中的函数符号表 |
size |
列出目标文件中每个段(text、data、bss等)的大小 |
strings |
列出目标文件中能打印出来的字符串,如代码中的字符串常量”Hello, World”,”Password”等 |
strip |
去掉目标文件中一些无关调试信息等,这样可以减小文件的大小 |
objcopy |
把一种目标文件中的内容复制到另一种目标文件中,裸机开发经常会用这个命令将ELF格式的文件转换成二进制文件 |
objdump |
该工具常用于对二进制文件进行反汇编,默认输出到标准输出,所以一般配合重定向一起使用 |
addr2line |
该工具可以将程序地址换为文件名、函数名和源代码行号,主要用来调试或反汇编 |
使用交叉编译器 gcc 命令交叉编译生成 hello 程序
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-gcc hello.c -o hello
可以使用 file 命令查看 hello 程序的相关信息.
guowenxue@ubuntu20:~$ file hello
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, with debug_info, not stripped
使用交叉编译器 ar 命令工具制作静态库文件.
guowenxue@ubuntu20:~$ vim file1.c
int func_add(int a, int b)
{
return a+b;
}
guowenxue@ubuntu20:~$ vim file2.c
int func_sub(int a, int b)
{
return a-b;
}
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-gcc -c file1.c file2.c
guowenxue@ubuntu20:~$ ls file*.o
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-ar -rcs libalg.a file1.o file2.o
使用交叉编译器 readelf 命令查看 hello 程序 ELF信息.
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-readelf -a hello
使用交叉编译器 nm 命令查看 hello 程序符号表。
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-nm hello
使用交叉编译器 strings 命令显示可执行程序中能打印出来的字符串,如代码中的字符串常量 “Hello, World”等.
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-strings hello
使用交叉编译器 size 命令查看 hello 程序各个段大小,单片机裸机开发环境(如STM32CubeIDE)在编译生成可执行文件后,通常会使用该工具列出相关段信息。
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-size hello
text data bss dec hex filename
1102 324 4 1430 596 hello
使用交叉编译器 objcopy 命令将 ELF 可执行文件转换成单片机Flash烧写的 binary格式.bin文件 或 摩托罗拉 .srec 格式文件。单片机裸机开发环境(如STM32CubeIDE)在编译生成ELF文件后会将其转换成 .bin文件.
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-objcopy -O binary hello hello.bin
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-objcopy -O srec hello hello.srec
使用交叉编译器 objdump 命令反汇编 hello 程序
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-objdump -D hello > hello.s
使用交叉编译器 strip 命令去掉 hello 调试信息,可以看到文件明显变小。
guowenxue@ubuntu20:~$ ls -l hello
-rwxrwxr-x 1 guowenxue guowenxue 8152 Jul 31 14:50 hello
guowenxue@ubuntu20:~$ arm-none-linux-gnueabihf-strip hello
guowenxue@ubuntu20:~$ ls -l hello
-rwxrwxr-x 1 guowenxue guowenxue 5524 Jul 31 14:59 hello
guowenxue@ubuntu20:~$ file hello
hello: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, BuildID[sha1]=2874be72251c350c355f9ce98b7b5f99016b4a6a, for GNU/Linux 3.2.0, stripped