一.编译工具

前言

1)编译器和目标程序运行在相同架构的编译过程,被称为本地编译
2)编译器和目标程序运行在不同架构的编译过程,被称为交叉编译
为什么需要交叉编译?因为编译过程往往需要很大的算力和空间,arm平台上往往不满足这个要求,因此需要在x86上编译,然后下载到arm上运行。

1.GCC简介

   Linux下编译C代码使用的是gcc,GCC(GNU Compiler Collection)是由 GNU这个组织 开发的编程语言编译器,最开始叫GNU C Compiler,只支持编译C语言,后面渐渐开始支持编译C++,Java等语言,因此改叫GNU Compiler Collection,缩写还是GCC。C语言的GCC由下面三部分组成:
1)gcc-core:gcc编译器,可以将源码编译成可执行程序
2)Binutils:除gcc-core之外的小工具的合集,里面包含了链接器ld,汇编器as等。
3)glibc:C语言的标准库,包含了printf等函数。
执行如下命令,可以查看当前gcc编译的目标程序是在x86上运行还是在arm上运行:

gcc -v

会输出gcc的版本信息,里面有target这一项,根据该项的值来判断:

1)target:x86_64-linux-gnu,表示编译生成的程序只能在x86架构上运行。
2)target:arm-linux-gnueabihf,表示编译生成的程序只能在arm架构上运行。
2.GCC编译过程

使用gcc编译代码基本语法如下:
gcc [选项] 输入的文件名

常用选项如下:
-o:小写字母“o”,指定生成的可执行文件的名字,不指定的话生成的可执行文件名为 a.out
-E:只进行预处理,既不编译,也不汇编,只是将include的文件和宏定义展开
-S:只编译,不汇编
-c:编译并汇编,但不进行链接
-g:生成的可执行文件带调试信息,方便使用 gdb 进行调试
-Ox:大写字母“O”加数字,设置程序的优化等级,如“-O0”“-O1”“-O2”“-O3”,数字
越大代码的优化等级越高,编译出来的程序一般会越小,但有可能会导致程序不正常运行

要理解上述选项,首先要知道gcc编译代码的几个阶段:预处理,编译,汇编,链接,如下:

Linux memtest编译_计算机网络


下面通过举例说明上述过程,假设有hello.c源文件:

#include <stdio.h>

int main()
{
        printf("hello, world! This is a C program.\n");
        for(int i = 0; i < 10; i++)
        {
                printf("output i = %d\n", i);
        }

        return 0;
}

那么执行如下命令,可以一步到位,直接从源码生成可执行文件并执行:

gcc hello.c -o hello
./hello

其实,gcc hello.c -o hello这条命令包含了如下过程:

# 1.预处理,可理解为把头文件和宏的代码展开汇总成 C 代码,把 *.c 转换得到 *.i 文件
gcc –E hello.c –o hello.i
# 2.编译,可理解为把 C 代码转换为汇编代码,把 *.i 转换得到 *.s 文件
gcc –S hello.i –o hello.s
# 3.汇编,可理解为把汇编代码转换为机器码,把 *.s 转换得到 *.o,即目标文件
gcc –c hello.s –o hello.o
# 4.链接,把不同文件之间的调用关系链接起来,把一个或多个 *.o 转换成最终的可执行文件
gcc hello.o –o hello

1)上述四个过程是可以分别执行的,也可以按照顺序组合在一起,如:

gcc -S hello.c -o hello.s    #相当与1和2两条命令的结合,同理
gcc –c hello.c –o hello.o   #相当于1,2,3这三条命令的结合

2)上述过程生成的文件是可以打开的,比如执行了gcc –E hello.c –o hello.i,可以直接用vim打开hello.i,就能看到相关头文件代码都被包含进来,宏也被替换展开了。
3) 如果执行了gcc –S hello.i –o hello.s之后,打开hello.s之后,便能看到生成的汇编代码。
4) 但是需要注意的是,执行gcc –c hello.s –o hello.o之后,如果用vim打开hello.o会发现是乱码, 因为.o文件是机器码,是为了让机器阅读的。.o文件可以用readelf工具打开:

readelf -a hello.o

命令执行结果如下:

Linux memtest编译_Linux memtest编译_02


从上图可以看到,目标文件里面包含了 ELF 头、程序头、节等内容。对于*.o目标文件和*.so库文件,链接器根据这些内容信息来链接生成可执行程序,对于可执行文件,系统在运行时会根据这些信息来加载程序运行。

注:linux下生成的.o目标文件、.so动态库文件、可执行文件等都是elf格式的,可以通过readelf工具来查看内容。

3.链接过程

链接分为两种:静态链接和动态链接。
动态链接: GCC 编译时的默认选项。动态是指在应用程序运行时才去加载外部的代码库,比如c语言的标准库,如果是动态链接生成的程序,那么都是在运行的时候才去加载。
静态链接: 链接时使用选项“–static”,它在编译阶段就会把所有用到的库打包到自己的可执行程序中。
静态链接的优点是兼容性好,程序基本不依赖于外部环境,但是生成的程序也比较大。
动态链接生成的程序兼容性差点,但是生成的程序比较小。

gcc hello.c -o hello   #动态链接生成
gcc hello.c -o hello_static --static   #静态链接生成

通过对比,发现动态链接生成的hello比静态链接生成的hello_static小很多。

4.交叉编译器简介

编译器和目标程序运行在不同的架构上,这种编译器叫交叉编译器,x86平台上gcc交叉编译器主要有:
1)arm-linux-gnueabihf-gcc:这个编译器名称中的Linux表明该编译器生成的目标程序运行在Linux系统上,程序可以使用Linux下的c标准库和Linux内核API。
2)arm-none-eabi-gcc:名称中的none表示该编译器编译生成的目标程序运行环境不需要操作系统,可以用于编译逻辑代码,uboot,内核代码等。

注意,arm-linux-gnueabihf-gcc也是可以编译uboot和内核代码的。但是推荐用arm-none-eabi-gcc。
上述两个编译器都是由Linaro组织提供的。Linaro 是由 ARM 发起,与其它 ARM SOC 公司共同投资的非盈利组织。大部分开发者都是使用Linaro组织提供的交叉编译器。

Linaro组织的编译器命名规则如下:

arch [-os] [-(gnu)eabi(hf)] -gcc

Linux memtest编译_编译器_03


其中,hf表示编译器的浮点模式是硬浮点hard-float(采用 fpu 参与浮点运算),如果没有hf则表明是软浮点soft-float(即使有 fpu 浮点运算单元也不用,而是使用软件模式)。

**注意,**如果目标平台的c库文件(比如glibc 的库文件 libc.so.6)都是硬浮点类型,那么使用不带hf的编译器编译出来的目标程序很可能无法在Linux平台上运行。使用如下命令可以查看库的类型:

readelf -h libc.so.6

假设在平台A上输出如下:

Linux memtest编译_wifi_04


可以看到,平台A库类型是hard-float,如果使用如下命令编译:

sudo arm-linux-gnueabi-gcc  hello.c -o hello

那么编译生成的hello无法在上述在平台A上运行,因为平台A的c库文件是hard-float,而hello是soft-float类型编译器编译出来的。
要想在平台A上运行,有两种方法,方法一是使用hard-float类型的编译器,

sudo arm-linux-gnueabihf-gcc hello.c -o hello_hf

方法二是使用静态编译:

sudo arm-linux-gnueabi-gcc hello.c -o hello_static  --static

则编译生成的hello_hf和hello_static均可以在平台A上运行。