1.为什么要有交叉编译?

主要解决编译链程序和目标程序运行环境不同的问题,如在x86环境上使用编译工具进行编译汇编链接,而生成的程序需要运行在ARM开发板上。

2.如何安装交叉编译链?

下载交叉编译链压缩包,下载链接:
https://developer.arm.com/open-source/gnu-toolchain
https://releases.linaro.org/components/toolchain/binaries/

使用如下命令解压:

//解压到根目录
sudo tar -xvf gcc-arm-11.2-2022.02-x86_64-arm-none-linux-gnueabihf.tar.xz -C /opt

注意:不同的压缩格式,命令也不同。
.tar.gz 格式解压为 tar -zxvf xx.tar.gz
.tar.bz2 格式解压为 tar -jxvf xx.tar.bz2

最后编辑 vim ~/.bashrc 或者 sudo vim /etc/environment,在文件末尾追加PATH环境变量:

export PATH=$PATH:/opt/gcc-arm-11.2-2022.02-x86_64-arm-none-linux-gnueabihf/bin

使用 source ~/.bashrc 或者 source /etc/environment 使得新的PATH环境变量在当前shell生效。
或者通过sudo vi /etc/environment编辑环境变量,将交叉编译链的bin文件夹路径添加到环境变量中,通过source /etc/environment重新加载环境变量。

若交叉编译工具在32bit系统下生产的,需要在64bit下运行,则需要安装32bit对应的库。使用以下方法:

sudo apt-get install ia32-libs
//或者
sudo apt-get install libc6:i386
//或者
sudo yum install libXtst.i686
//可能还需要安装
sudo apt-get install lib32z1

安装完后使用gcc -v查看是否正常显示。

[root@localhost bin]# ./arm-none-linux-gnueabihf-gcc -v
Using built-in specs.
COLLECT_GCC=./arm-none-linux-gnueabihf-gcc
COLLECT_LTO_WRAPPER=/opt/gcc-arm-11.2-2022.02-x86_64-arm-none-linux-gnueabihf/bin/../libexec/gcc/arm-none-linux-gnueabihf/11.2.1/lto-wrapper
Target: arm-none-linux-gnueabihf
Configured with: /data/jenkins/workspace/GNU-toolchain/arm-11/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-11/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-11/build-arm-none-linux-gnueabihf/host-tools --with-mpfr=/data/jenkins/workspace/GNU-toolchain/arm-11/build-arm-none-linux-gnueabihf/host-tools --with-mpc=/data/jenkins/workspace/GNU-toolchain/arm-11/build-arm-none-linux-gnueabihf/host-tools --with-isl=/data/jenkins/workspace/GNU-toolchain/arm-11/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 Arm Architecture 11.2-2022.02 (arm-11.14)'
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 11.2.1 20220111 (GNU Toolchain for the Arm Architecture 11.2-2022.02 (arm-11.14)) 

3.交叉编译工具集介绍

常用的工具有gcc、readelf、size、nm、strip、strings、objdump和addr2line。可以使用-h选项查看各个命令的使用方法。下面我们对各个工具逐一介绍。
使用简单的helloworld作为示例。

#include <stdlib.h>
#include <stdio.h>

int g_val = 12;
int g_uninit;
const char * str = "who am I?";
static char s_uninit;

int main()
{
	printf("hello world!\n");
	int* p = malloc(sizeof(int));
	static int s_tmp = 0;
	free(p);
	return 0;
}

3.1 gcc命令

3.1.1 常用编译命令选项

  1. 无选项编译链接
    用法:#gcc main.c
    作用:将main.c预处理、汇编、编译并链接形成可执行文件。这里未指定输出文件,默认输出为a.out。

  2. 选项 -o
    用法:#gcc main.c -o main
    作用:将main.c预处理、汇编、编译并链接形成可执行文件main。-o选项用来指定输出文件的文件名。

  3. 选项 -E
    用法:#gcc -E main.c -o main.i
    作用:将main.c预处理输出main.i文件。

  4. 选项 -S
    用法:#gcc -S main.i
    作用:将预处理输出文件main.i汇编成main.s文件。

  5. 选项 -c
    用法:#gcc -c main.s
    作用:将汇编输出文件main.s编译输出main.o文件。

  6. 无选项链接
    用法:#gcc main.o -o main
    作用:将编译输出文件main.o链接成最终可执行文件main。

  7. 选项-O
    用法:#gcc -O1 main.c -o main
    作用:使用编译优化级别1编译程序。级别为1~3,级别越大优化效果越好,但编译时间越长。

3.1.2 多源文件的编译方法

如果有多个源文件,基本上有两种编译方法:
[假设有两个源文件为main.c和testfun.c]

  1. 多个文件一起编译
    用法:#gcc testfun.c main.c -o main
    作用:将testfun.c和main.c分别编译后链接成main可执行文件。

  2. 分别编译各个源文件,之后对编译后输出的目标文件链接。
    用法:
    #gcc -c testfun.c //将testfun.c编译成testfun.o
    #gcc -c main.c //将main.c编译成main.o
    #gcc -o testfun.o test.o -o main //将testfun.o和test.o链接成main

以上两种方法相比较,第一中方法编译时需要所有文件重新编译,而第二种方法可以只重新编译修改的文件,未修改的文件不用重新编译。

3.1.3 库文件链接

开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助许多函数库的支持才能够完成相应的功能。从程序员的角度看,函数库实际上就是一些头文件(.h)和库文件(so、或lib、dll)的集合。。虽然Linux下的大多数函数都默认将头文件放到/usr/include/目录下,而库文件则放到/usr/lib/目录下;Windows所使用的库文件主要放在Visual Stido的目录下的include和lib,以及系统文件夹下。但也有的时候,我们要用的库不再这些目录下,所以GCC在编译时必须用自己的办法来查找所需要的头文件和库文件。

例如我们的程序test.c是在linux上使用c连接mysql,这个时候我们需要去mysql官网下载MySQL Connectors的C库,下载下来解压之后,有一个include文件夹,里面包含mysql connectors的头文件,还有一个lib文件夹,里面包含二进制so文件libmysqlclient.so

其中inclulde文件夹的路径是/usr/dev/mysql/include,lib文件夹是/usr/dev/mysql/lib

(1) 编译成可执行文件
首先我们要进行编译main.c为目标文件,这个时候需要执行

gcc –c –I /usr/dev/mysql/include main.c –o main.o

(2) 链接最后我们把所有目标文件链接成可执行文件:

gcc –L /usr/dev/mysql/lib –lmysqlclient main.o –o main
Linux下的库文件分为两大类分别是动态链接库(通常以.so结尾)和静态链接库(通常以.a结尾),二者的区别仅在于程序执行时所需的代码是在运行时动态加载的,还是在编译时静态加载的。

(3) 强制链接时使用静态链接库
默认情况下, GCC在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接库。

在/usr/dev/mysql/lib目录下有链接时所需要的库文件libmysqlclient.so和libmysqlclient.a,为了让GCC在链接时只用到静态链接库,可以使用下面的命令:

gcc –L /usr/dev/mysql/lib –static –lmysqlclient main.o –o main

静态库链接时搜索路径顺序:

  1. ld会去找GCC命令中的参数-L
  2. 再找gcc的环境变量LIBRARY_PATH
  3. 再找内定目录 /lib /usr/lib /usr/local/lib 这是当初compile gcc时写在程序内的

动态链接时、执行时搜索路径顺序:

  1. 编译目标代码时指定的动态库搜索路径
  2. 环境变量LD_LIBRARY_PATH指定的动态库搜索路径
  3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径
  4. 默认的动态库搜索路径/lib
  5. 默认的动态库搜索路径/usr/lib

有关环境变量:
LIBRARY_PATH环境变量:指定程序静态链接库文件搜索路径
LD_LIBRARY_PATH环境变量:指定程序动态链接库文件搜索路径

这里,我们使用简单的编译命令。

[root@localhost toolchains]# arm-none-linux-gnueabihf-gcc -g main.c -o main
[root@localhost toolchains]# ll
总用量 14
-rwxrwxrwx 1 root root 13116 7月   2 23:00 main
-rwxrwxrwx 1 root root   242 7月   2 22:59 main.c
[root@localhost toolchains]# file main
main: 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

3.2 readelf命令

显示可执行程序的elf文件信息。
常用的选项:
-S --section-headers Display the sections’ header
-e --headers Equivalent to: -h -l -S
-s --syms Display the symbol table
-x --hex-dump=<number|name>
Dump the contents of section <number|name> as bytes
-p --string-dump=<number|name>
Dump the contents of section <number|name> as strings

[root@localhost toolchains]# arm-none-linux-gnueabihf-readelf -h main
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x1039d
  Start of program headers:          52 (bytes into file)
  Start of section headers:          11636 (bytes into file)
  Flags:                             0x5000400, Version5 EABI, hard-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         37
  Section header string table index: 36

有ultraEdit软件打开可执行程序main,可以看到文件首部内容和readelf打印对应。
在这里插入图片描述

查看程序各个段头:

[root@localhost toolchains]# arm-none-linux-gnueabihf-readelf -S main
There are 37 section headers, starting at offset 0x2d74:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        00010154 000154 000019 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            00010170 000170 000020 00   A  0   0  4
  [ 3] .hash             HASH            00010190 000190 000030 04   A  5   0  4
  [ 4] .gnu.hash         GNU_HASH        000101c0 0001c0 000034 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          000101f4 0001f4 000070 10   A  6   1  4
  [ 6] .dynstr           STRTAB          00010264 000264 000058 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          000102bc 0002bc 00000e 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         000102cc 0002cc 000030 00   A  6   1  4
  [ 9] .rel.dyn          REL             000102fc 0002fc 000008 08   A  5   0  4
  [10] .rel.plt          REL             00010304 000304 000030 08  AI  5  21  4
  [11] .init             PROGBITS        00010334 000334 00000c 00  AX  0   0  4
  [12] .plt              PROGBITS        00010340 000340 00005c 04  AX  0   0  4
  [13] .text             PROGBITS        0001039c 00039c 0000e0 00  AX  0   0  4
  [14] .fini             PROGBITS        0001047c 00047c 000008 00  AX  0   0  4
  [15] .rodata           PROGBITS        00010484 000484 000140 00   A  0   0  4
  [16] .ARM.exidx        ARM_EXIDX       000105c4 0005c4 000008 00  AL 13   0  4
  [17] .eh_frame         PROGBITS        000105cc 0005cc 000004 00   A  0   0  4
  [18] .init_array       INIT_ARRAY      00020f08 000f08 000004 04  WA  0   0  4
  [19] .fini_array       FINI_ARRAY      00020f0c 000f0c 000004 04  WA  0   0  4
  [20] .dynamic          DYNAMIC         00020f10 000f10 0000f0 08  WA  6   0  4
  [21] .got              PROGBITS        00021000 001000 000028 04  WA  0   0  4
  [22] .data             PROGBITS        00021028 001028 000010 00  WA  0   0  4
  [23] .bss              NOBITS          00021038 001038 000010 00  WA  0   0  4
  [24] .comment          PROGBITS        00000000 001038 000057 01  MS  0   0  1
  [25] .ARM.attributes   ARM_ATTRIBUTES  00000000 00108f 000035 00      0   0  1
  [26] .debug_aranges    PROGBITS        00000000 0010c8 0000c8 00      0   0  8
  [27] .debug_info       PROGBITS        00000000 001190 000673 00      0   0  1
  [28] .debug_abbrev     PROGBITS        00000000 001803 000274 00      0   0  1
  [29] .debug_line       PROGBITS        00000000 001a77 000346 00      0   0  1
  [30] .debug_frame      PROGBITS        00000000 001dc0 000034 00      0   0  4
  [31] .debug_str        PROGBITS        00000000 001df4 000482 01  MS  0   0  1
  [32] .debug_line_str   PROGBITS        00000000 002276 000025 01  MS  0   0  1
  [33] .debug_rnglists   PROGBITS        00000000 00229b 000038 00      0   0  1
  [34] .symtab           SYMTAB          00000000 0022d4 000700 10     35  87  4
  [35] .strtab           STRTAB          00000000 0029d4 000241 00      0   0  1
  [36] .shstrtab         STRTAB          00000000 002c15 00015f 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), y (purecode), p (processor specific)

查看某个符号(函数或者变量等)的地址:

arm-sgmstar-gnueabihf-9.1.0-readelf -s main

3.3 objdump命令

显示程序信息,函数汇编等,常用于调试。
-D, --disassemble-all Display assembler contents of all sections
-S, --source Intermix source code with disassembly

把反汇编内容输出到main.txt中:

[root@localhost toolchains]# arm-none-linux-gnueabihf-objdump -D main > main.txt
[root@localhost toolchains]# ll
总用量 82
-rwxrwxrwx 1 root root 13152 7月   2 23:42 main
-rwxrwxrwx 1 root root   255 7月   2 23:30 main.c
-rwxrwxrwx 1 root root 69373 7月   2 23:53 main.txt

3.4 size命令

显示可执行程序中各个段占用的大小。

[root@localhost toolchains]# arm-none-linux-gnueabihf-size main
   text	   data	    bss	    dec	    hex	filename
   1143	    304	     16	   1463	    5b7	main

我们在main.c中增加全局变量int aaa = 2;,重新编译后可以看到data段增加了4个字节:

[root@localhost toolchains]# arm-none-linux-gnueabihf-gcc -g main.c -o main
[root@localhost toolchains]# arm-none-linux-gnueabihf-size main
   text	   data	    bss	    dec	    hex	filename
   1143	    308	     16	   1467	    5bb	main

3.5 nm命令

该命令主要用于显示可执行程序的符号。

[root@localhost toolchains]# arm-none-linux-gnueabihf-nm main
00010170 r __abi_tag
         U abort@GLIBC_2.4
00010488 r all_implied_fbits
00010534 r all_implied_fbits
00021048 B __bss_end__
00021048 B _bss_end__
00021038 B __bss_start
00021038 B __bss_start__
000103c0 t call_weak_fn
00021038 b completed.0
00021028 D __data_start
00021028 W data_start
000103e4 t deregister_tm_clones
00010434 t __do_global_dtors_aux
00020f0c d __do_global_dtors_aux_fini_array_entry
0002102c D __dso_handle
00020f10 d _DYNAMIC
00021038 D _edata
00021048 B __end__
00021048 B _end
0001047c T _fini
0001044c t frame_dummy
00020f08 d __frame_dummy_init_array_entry
000105cc r __FRAME_END__
         U free@GLIBC_2.4
00021000 d _GLOBAL_OFFSET_TABLE_
         w __gmon_start__
0002103c B g_uninit
00021030 D g_val
00010334 T _init
00010484 R _IO_stdin_used
         U __libc_start_main@GLIBC_2.34
00010450 T main
         U malloc@GLIBC_2.4
         U puts@GLIBC_2.4
00010408 t register_tm_clones
0001039c T _start
00021044 b s_tmp.0
00021034 D str
00021040 b s_uninit
00021038 D __TMC_END__

符号说明:

对于每一个符号来说,其类型如果是小写的,则表明该符号是local的。大写则表明该符号是global(external)的

A:该符号的值是绝对的,在以后的链接过程中,不允许改变。这样的符号,常常出现在中断向量表中,例如用符号来表示各个中断向量函数在中断向量表中的位置。
B:该符号的值出现在非初始化数据段BSS中。例如,一个文件中定义全局 static int s_int。则符号s_int 类型为b,位于bss section中。其值表示该符号在bss段的偏移。一般而言,bss段分配于RAM中。
C:该符号为common。common symbol是未初始化数据段。该符号没有包含于一个普通section中。只有在链接过程中才进行分配。符号的值表示要分配的字节数。例如,在一个c文件中,定义int g_no_init,并且该符号在别的地方会被引用,则该符号类型就是C,否则为B。
D:该符号位于初始化数据段中。一般来说,分配到data section中。比如,全局变量 int g_init = 2;
G:该符号也位于初始化数据段。主要用于small object,提高访问small data object的一种方式。
I:该符号是对另一个符号的间接引用。
N:该符号是一个debugging符号
R:该符号位于只读数据区。比如,全局变量 const int const_int = 0; 如果在一个函数中定义 const char* test = “abc”; const int a = 2;使用nm都不会得到符号信息。但是字符串"abc"分配于只读存储器中,test 在rodata section中,大小为4
S:符号位于非初始化数据区,用于 small object
T:符号位于代码区 text section
U:符号在当前文件中是未定义的,即该符号的定义在别的文件中。比如,当前文件中调用另一个文件中的函数,在这个本目标文件中,函数就是未定义的。但是在定义它的文件中,类型为T。但是对于全局变量来说,在定义它的文件中,符号类型是C,在使用它的文件中,类型是U。
V:该符号是一个weak object
?:该符号类型没有定义

3.6 addr2line命令

将函数在可执行程序文件中的地址转换成源代码文件名和行数。
命令:
arm-none-linux-gnueabihf-addr2line -e main -psfC addr地址

[root@localhost toolchains]# arm-none-linux-gnueabihf-gcc -g main.c -o main
[root@localhost toolchains]# arm-none-linux-gnueabihf-readelf -s main|grep main
    69: 00000000     0 FILE    LOCAL  DEFAULT  ABS main.c
   107: 00010451    44 FUNC    GLOBAL DEFAULT   13 main
[root@localhost toolchains]# arm-none-linux-gnueabihf-addr2line -e main -psfC 0x10451
main at main.c:11
[root@localhost toolchains]# cat -n  main.c 
     1	#include <stdlib.h>
     2	#include <stdio.h>
     3	
     4	int g_val = 12;
     5	int g_uninit;
     6	int aaa = 2;
     7	const char * str = "who am I?";
     8	static char s_uninit;
     9	
    10	int main()
    11	{
    12		printf("hello world!\n");
    13		int* p = malloc(sizeof(int));
    14		static int s_tmp = 0;
    15		free(p);
    16		return 0;
    17	}

3.7 objcopy命令

将目标文件的一部分或者全部内容拷贝到另外一个目标文件中,或者实现目标文件的格式转换。通过指定输入目标为二进制文件(例如-O binary),objcopy可以生成原始格式的二进制文件。当objcopy生成一个原始格式的二进制文件的时候,它会生成输入的目标文件的基本内存拷贝,然后所有的标号和可重定位信息都会被去掉。内存拷贝开始于最低段的加载地址,拷贝到输出文件。
常用的选项有:

[root@localhost test]# objcopy -O srec main main.srec #将文件转换成S-record格式

[root@localhost test]# objcopy -O binary main main.bin #将文件转换成rawbinary 格式

[root@localhost test]# objcopy -S main main.stripall #生成一个不含重定位以及标号目标文件

[root@localhost test]# objcopy -R .comment main main.remove #去掉指定名称的节

[root@localhost test]# objcopy --add-section mysection=hello_text main main.add #添加一个自定义的节到可执行文件并将一个文件内容添加到其中

[root@localhost test]# objcopy -j mysection main.add section_hello #将指定的段拷贝出来

[root@localhost test]# objcopy --only-keep-debug main.debug main.debuginfo # 生成调试信息文件

[root@localhost test]# objcopy --strip-debug main.debug main.stripdebug #生成 不含调试信息的可执行文件

[root@localhost test]# objcopy --add-gnu-debuglink=main.debuginfo main.stripdebug #为不含调试信息的可执行文件添加调试信息

3.8 stings命令

显示可执行程序中能打印出来的字符串。可以看到代码里面写的常量字符串“who am I?”、“hello world!”等等。

[root@localhost toolchains]# arm-none-linux-gnueabihf-strings main
/lib/ld-linux-armhf.so.3
malloc
__libc_start_main
puts
free
abort
libc.so.6
GLIBC_2.4
GLIBC_2.34
__gmon_start__
F{`xh
who am I?
hello world!
GCC: (GNU Toolchain for the Arm Architecture 11.2-2022.02 (arm-11.14)) 11.2.1 20220111
aeabi
.shstrtab
.interp
.note.ABI-tag
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.dyn
.rel.plt
.init
.text
.fini
.rodata
.ARM.exidx
.eh_frame
.init_array
.fini_array
.dynamic
.got
.data
.bss
.comment
.ARM.attributes

3.9 strip命令

该命令主要用于剥离可执行程序中的符号表。可以看到,执行strip后可执行程序变小很多。嵌入式开发往往会用到strip命令节省flash空间。

[root@localhost toolchains]# ll
总用量 14
-rwxrwxrwx 1 root root 13116 7月   2 23:00 main
-rwxrwxrwx 1 root root   242 7月   2 22:59 main.c
[root@localhost toolchains]# arm-none-linux-gnueabihf-strip main
[root@localhost toolchains]# ll
总用量 6
-rwxrwxrwx 1 root root 5600 7月   2 23:01 main
-rwxrwxrwx 1 root root  242 7月   2 22:59 main.c

4. 总结

本文简要介绍了交叉编译链及常见的工具集的使用方法。由于水平有限,如有纰漏,请指正。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐