嵌入式之 C 语言编译器(五)

  • 来源:网络
  • 更新日期:2020-08-04

摘要:系统运维 我们在嵌入式的开发中经常会见到 GCC 和 gcc,那么它们两有何不同呢?GCC(GNU Compile Collection) 是指 GNU 编译器集合,包含

系统运维

        我们在嵌入式的开发中经常会见到 GCC 和 gcc,那么它们两有何不同呢?GCC(GNU Compile Collection) 是指 GNU 编译器集合,包含众多语言的编译器,如 C、C++、Java、D、Objective-C 等;而 gcc 则是特指 GCC 中的 C 语言编译器。那么 GCC 与嵌入式的关系是怎样的呢?多数嵌入式操作系统都是基于 GCC 进行源码编译,如 Linux、VxWorks 以及 Android 等。在实际的开发中,内核相关的开发用的是 gcc,而应用开发用的是 gcc/g++/gdc 等。

        下来我们来看看一个嵌入式开发中的高端大气上档次的词语:交叉编译。那么为什么会有交叉编译呢?在以往的嵌入式设备往往都是资源受限的,不可能直接在嵌入式上直接对处理器进行编程。那么此时的解决方案便是在开发主机(PC)上对源码进行编译,最终生成目标主机(嵌入式设备)的可执行程序。gcc 是如何进行交叉编译的呢?1、配置目标主机的编译工具链(如arm-linux);2、配置工具链的具体版本:根据具体的目标代码选择相应的工具链版本,正确使用关于硬件体系的特殊编译选项。下来我们来看看大型企业的嵌入式开发环境是怎样的,如下

        这个服务器集群相当于是我们自己公司的内部服务器,版本控制则是指由原来的版本经过我们一些代码的修改之后产生的新版本,用于各个版本的控制的。文件追踪则是指在服务器上面可以看到那部分的代码是具体由哪个人进行改写的,可具体到文件以及部分代码。我们来看看编译器是怎样的,如下

        编译器其实是由预处理期、编译器、汇编器以及链接器构成的。我们平时所说的由哪个编译器编译生成的文件,此时的编译器便是指广范围的编译器。那么狭义上的编译器则是指我们在平时所听到的生产一个某语言的编译器,此时的编译器则是指将具体的语言翻译成目标平台代码而已。我们来看看一个 .c 文件是怎样编译成 .o 文件的,具体步骤如下所示

        我们看到并不是我们所想象的直接一步就由 .c 文件直接编译成为 .o 可执行文件了,而是经过那么多的步骤才会生成最终的可执行程序的。那么此时便扩展一个问题,我们是如何理解“多语言混合开发”?我们在平时可能会听到多语言混合开发,是指由好几种语言混合进行一个应用程序开发的。那么为什么会产生这种混合的开发方式呢?比如说一个项目是由 C++ 完成的,但是其中的某些部分是可以通过 C# 完成的,此时精通 C++ 的人很少(相应工资就要的很高了),而 C# 的工程师由一大堆,我们就可以需要两个精通 C++ 的工程师和好几个 C# 的工程师来共同完成这个项目,达到以最小的开支完成此项目的效果。或者是你们小组内每个人擅长的语言方向不一样,为了发挥每个人的最大效率便可以采取这种混合开发的方式。下来我们来看看几种多语言混合开发的方式

        方式一,如下

        此方式是通过由几种语言经过汇编得到目标平台的汇编语言,再由目标平台汇编器统一链接生成可执行程序。行业典型的案例就是 .net framework,它便是由 C#、C++ 以及 VB 混合开发得到的,如下


        方式二,如下

        它是由各自的语言生成相应的库再通过目标平台链接器统一链接为可执行程序。典型的案例便是 QQ 了,如下

        方式三,如下

        它是经过各自的编译器先生成可执行程序 .exe,再通过进程间通信协议进而生成可执行程序。行业案例:Eclipse,如下

        下来我们来看看 gcc 关键编译选项。

        gcc 关键编译选项一:a> 预处理指令是:gcc -E file.c -o file.i;b > 编译指令:gcc -S file.i -o file.s;c> 汇编指令:gcc -c file.s -o file.o。

        下来我们来看看效果分别是怎样的


func.h 源码

#include <stdio.h>

void func()
{
#ifdef TEST
    printf("TEST = %s\\n", TEST);
#endif

    return;
}


test.c 源码

#include <stdio.h>
#include "func.h"

int g_global = 0;
int g_test = 1;

int main(int argc, char *argv[])
{
    func();
    
    printf("&g_global = %p\\n", &g_global);
    printf("&g_test = %p\\n", &g_test);
    printf("&func = %p\\n", &func);
    printf("&main = %p\\n", &main);
    
    return 0;
}

        我们来看看预处理的效果,打开 test.i 文件看看,开头是这样的

        第一行的 1 表示下面的内容是属于 test.c 文件的内容,下面是一些头文件的包含。

        # 2 "test.c" 2 的意思是 test.c 头文件的包含已经结束了,# 1 "func.h" 1 表示 func.h 相关内容的开始。最后便是 test.c 文件 main 函数的内容了。下面看看编译指令生成的 .s 文件

        都是一些生成的汇编命令。下面来看看最后的汇编指令生成 .o 文件

        gcc 关键编译选项二:a> 生成映射文件:gcc -WI,-Map=test.map file.c;b> 宏定义:gcc -D'TEST="test"' file.c;c> 获取系统头文件路径:gcc -v file.c。

        gcc 关键编译选项三:生成依赖关系。a> 获取目标完整的依赖关系:gcc -M test.c;b> 获取目标的部分依赖关系:gcc -MM test.c。

        下来我们来看看 -M 和 -MM 的效果分别是怎样的,如下

        我们看到包含了那么多的头文件,它的格式类似于 makefile 中的目标与依赖的关系。其中依赖是 test.c 和众多的头文件以及我们自己包含的 func.h 头文件。再来看看 -MM 的效果

        我们看到 -MM 的效果是指依赖于 test.c 和 func.h,并没有那些别的头文件。

        gcc 关键编译选项四:指定库文件及库文件搜索路径。-L 选项是指定库文件的搜索路径;-l 是指定库文件,如 gcc test.c -L -lfunc。


func.c 源码

#include <stdio.h>

void func()
{
#ifdef TEST
    printf("TEST = %s\\n", TEST);
#endif

    return;
}


test.c 源码

#include <stdio.h>

int g_global = 0;
int g_test = 1;

int main(int argc, char *argv[])
{
    func();
    
    printf("&g_global = %p\\n", &g_global);
    printf("&g_test = %p\\n", &g_test);
    printf("&func = %p\\n", &func);
    printf("&main = %p\\n", &main);
    
    return 0;
}

        编译结果如下

        我们看到经过 ar crs 命令将 func.o 打包成 libfunc.a 文件后,再通过 gcc test.c -L. -lfunc 命令生成可执行程序 a.out(其中 -L 后面的点代表在当前目录下)。

新网虚拟主机