程序编译流程与 GCC 编译器

目录

GUN 计划

在上世纪 7-80 年代,应用最为广泛的 UNIX 操作系统通常是一个闭源的商业软件。1983 年,麻省理工学院的程序员理查德·斯托曼提出了 GNU 计划,希望发展出一套完整的、开发源代码的操作系统以取代 UNIX,志在创建一个完全兼容 UNIX 的自由软件生态环境。

我们知道,一个完整的操作系统是需要包含许多软件的,除了最重要的操作系统内核之外,还需要有编辑器,编译器,Shell、视窗系统等等一系列软件作为支撑。直到 1989 年,GNU 计划中的其他部份都已经完成了,独缺一个操作系统内核。

1990 年,自由软件基金会开始将 Hurd 内核加入 GUN 计划。第二年,Linux 诞生,由于 Linux 诞生即开源,其良好的开放性使得几乎所有 GNU 计划中的、运行于用户空间的软件都可以在 Linux 上使用。于是许多开发者开始转向于 Linux,参与了 Linux 的开发与修改,Linux 也逐步成为了最受欢迎的 GNU 软件开发及运行平台。相反的,Hurd 内核直到 2013 年为止都还没能发布稳定的版本。

1992 年,Linux 与 GNU 计划结合,一个完全自由的操作系统正式诞生了。当时,理查德·斯托曼主张,因为 Linux 使用了许多的 GNU 软件,所以应该正名为 GNU/Linux,但这一提议并没有得到 Linux 社区的一致认同,后来还引发了 GNU/Linux 命名争议。但不管如何,虽然 Linux 本身并不属于 GNU 计划的一部份,但两者的关系早已宛如一体共生。

虽说 GNU 计划中的 Hurd 内核发展得并不如意,但 GNU 工程十几年以来创造了无数的著名的自由软件,例如:强健的 GCC 编译器,TeX 文本编辑器,X Window 视窗系统等等。

GCC 编译器

GCC(GNU Compiler Collection,GNU 编译器套件)是 Linux 下使用最广泛的 C/C++ 编译器。GCC 是以 GPL 许可证所发行的自由软件,也是 GNU 计划的关键部分。GCC 的初衷是为 GNU 操作系统专门编写一款编译器,现已被大多数类 Unix 操作系统,如:Linux、BSD、Mac OS X 等采纳为标准的编译器。GCC 支持多种计算机体系结构芯片,如 x86、ARM、MIPS 等,并已被移植到其他多种硬件平台。

GCC 仅仅是一个编译器,没有界面,必须在命令行模式下使用。通过 gcc 命令就可以将源文件编译成可执行文件。

上述示例通过 gcc 命令一次性完成编译和链接的整个过程,这样最方便。但实际上,gcc 命令也可以将编译和链接分开,每次只完成一项任务:

  1. 编译(Compiler):就将 hello.c 编译为 hello.o,一个源文件只会生成一个目标文件,默认的目标文件名字和源文件名字是一样的。
gcc -c hello.c
  1. 链接(Linker):在 gcc 命令后面紧跟目标文件的名字,就可以将目标文件链接成为可执行文件。
gcc hello.o

在这里插入图片描述

Clang 和 LLVM

GCC 目前作为跨平台编译器来说它的兼容性无异是最强大的,但兼容性肯定是以牺牲一定的性能为基础的。我们知道,在整个编译过程中,以中间代码为界,前面的词法分析、语法分析、语义分析等称之为前端处理,而后面的代码优化和目标代码生成称为后端处理。

GCC 作为兼容性最好的编译器,其为不同的高级语言单独写了一个前端,同时也为不同的处理器架构单独写了一个后端。你可以下载一份 GCC 源代码,通过配置 configure 来生成自己需要的编译器类型。但实际上这件事情并不容易,因为前端的主要功能是产生一个可供后端处理的抽象语法树,而语法树的结构实际上很难与处理器架构脱钩,这些都是编译器应用中需要解决的问题。

苹果为了能够针对自家的 Objective-C 编程语言提高编译性能,因此为 MAC 系统开发了一套专用的编译器软件 Clang 和 LLVM。自 XCode4 之后,苹果的默认编译器已经是 LLVM 了。

  • Clang 作为编译器前端:目的是输出代码对应的抽象语法树(Abstract Syntax Tree, AST),并将代码编译成 LLVM Bit Code。
  • LLVM 作为编译器后端:将 LLVM Bit Code 编译成平台相关(e.g. x86、ARM)的机器语言。

Clang 生成的 AST 所耗用掉的内存仅仅是 GCC 的 20% 左右。测试证明 Clang 编译 Objective-C 代码时,速度为 GCC 的 3 倍,还能针对用户发生的编译错误准确地给出建议。

GCC 的常用指令选项

  • -c:只编译,不链接成为可执行文件,通常用于编译不包含主程序的子程序文件。
  • -o <output_filename>:确定输出文件的名称,默认为 XXX.out。
  • -g:产生 GDB 符号调试工具所必要的符号信息,要对源代码进行调试,就必须加入这个选项。
  • -O:对程序进行优化编译、链接,采用这个选项,整个源代码会在编译、链接过程中进行优化处理,这样产生的可执行文件的执行效率可以提高,但是编译、链接的速度就相应地要慢一些。
  • -O2:比 -O 更好的优化编译、链接,当然整个编译、链接过程也会更慢。
  • -I <dirname>,将 dirname 指向的目录加入到 C 程序的头文件目录列表中,是在预处理过程中使用的参数。C 程序中的头文件包含两种情况∶
    • #include <myinc.h>:预处理程序 cpp 在系统预设包含文件目录(e.g. /usr/include)中搜寻相应的文件。
    • #include "myinc.h":预处理程序 cpp 在目标文件的文件夹内搜索相应文件。
  • -v:打印 gcc 执行时的详细过程。

GCC 所遵循的部分约定规则

  • .c 文件:C 语言源代码文件;
  • .h 文件:是程序所包含的头文件;
  • .o 文件:是编译后的目标文件;
  • .a 文件:是由目标文件构成的档案库文件;
  • .C、.cc 或 .cxx 文件:是 C++ 源代码文件且必须要经过预处理;
  • .i 文件:是 C 源代码文件且不应该对其执行预处理;
  • .ii 文件:是 C++ 源代码文件且不应该对其执行预处理;
  • .m 文件:是 Objective-C 源代码文件;
  • .mm 文件:是 Objective-C++ 源代码文件;
  • .s 文件:是汇编语言源代码文件;
  • .S 文件:是经过预处理的汇编语言源代码文件。

GCC 的编译流程

在这里插入图片描述

虽然我们称 GCC 是 C 语言的编译器,但由 C 语言源代码文件到生成可执行文件的过程不仅仅是编译的过程,而是要经历以下四个相互关联的步骤:

  1. 预处理(Preprocessing):GCC 首先调用预处理程序 cpp 进行预处理,在预处理过程中,.c 文件中的文件包含(include)、预处理语句(e.g. 宏定义 define 等)进行分析,并替换成为真正的内容。

  2. 编译(Compilation):接着调用 cc1 程序进行编译,这个阶段根据输入文件生成 .i 目标文件。

  3. 汇编(Assembly):汇编过程是针对汇编语言的步骤,调用 as 程序进行工作,一般来讲,.S 文件和 .s 文件经过预处理和汇编之后都会生成以 .o 的目标文件。

  4. 链接(Linking):当所有的目标文件都生成之后,GCC 就调用 ld 来完成最后的链接工作。所有的目标文件被安排在可执行程序中的恰当的位置,同时,该程序所调用到的库函数也从各自所在的档案库中链接到合适的地方。

在这里插入图片描述

GCC 的编译流程示例

示例代码

#include<stdio.h>

int main(void)
{
    printf("hello
");
    return 0;
}

预处理过程:这个过程处理宏定义和 include,去除注释,不会对语法进行检查。

gcc -E -I . main.c -o main.i

# -E 是让编译器在预处理之后就退出
# -I 指定头文件目录
# -o 指定输出文件名

下面可以看到预处理后,代码从 7 行扩展到了 845 行。.i 文件里面包含了所有 include 和宏定义的真正内容。

# 1 "main.c"
# 1 "<built-in>"
# 1 "<命令行>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<命令行>" 2
# 1 "main.c"
# 1 "/usr/include/stdio.h" 1 3 4
...

# 28 "/usr/include/bits/types.h" 2 3 4
...

typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;
...

编译过程:这个阶段,检查语法,生成汇编代码。

gcc -S -I . main.i -o main.s

# -S 让编译器在编译之后停止

下面可以看到编译后的汇编代码。

.file   "main.c"
        .section        .rodata
.LC0:
        .string "hello"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $.LC0, %edi
        call    puts
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)"
        .section        .note.GNU-stack,"",@progbits

汇编过程:这个阶段,生成目标代码,即将汇编代码转换成机器码。这时候的 main.o 文件就不是肉眼可以看明白的了。

$ gcc -c main.s -o main.o
# or
$ as main.s -o main.o

$ file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

链接过程:将多个目标文以及所需的库文件(e.g. .so etc.)链接成最终的可执行文件。

$ gcc main.o -o main.exe
# or
ld -o main.exe main.o inc/mymath.o ...libraries...

链接分为两种,一种是静态链接,另外一种是动态链接。静态连接就是把外部函数库,拷贝到可执行文件中,好处是适用范围比较广,依赖的动态链接库较少,对动态链接库的版本不会很敏感,具有较好的兼容性,不用担心用户机器缺少某个库文件。缺点是安装包会比较大,而且多个应用程序之间,无法共享库文件;动态连接的做法正好相反,外部函数库不进入安装包,只在运行时动态引用。好处是安装包会比较小,多个应用程序可以共享库文件;缺点是用户必须事先安装好库文件,而且版本和安装位置都必须符合要求,否则就不能正常运行。

现实中,大部分软件采用动态连接,共享库文件。这种动态共享的库文件,Linux 平台是后缀名为 .so 的文件,Windows 平台是 .dl l文件, Mac 平台是 .dylib 文件。

运行程序

$ ./main.exe
hello

编译多个文件

  • main.c
#include "hello.h"

int main(void)
{
    print("hello world");
    return 0;
}
  • hello.c
#include "hello.h"

void print(const char *str)
{
    printf("%s
", str);
}
  • hello.h
#ifndef _HELLO_H
#define _HELLO_H

#include <stdio.h>

void print(const char *str);

#endif

一次性编译:

$ gcc -Wall hello.c main.c -o main

$ ./main

独立编译

$ gcc -Wall -c main.c -o main.o
$ gcc -Wall -c hello.c -o hello.o
$ gcc -Wall hello.o main.o -o newmain

$ ./newmain

相关阅读:

原文地址:https://www.cnblogs.com/hzcya1995/p/13309383.html