程序编译流程与 GCC 编译器

目录

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

在这里插入图片描述

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\n");
    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\n", 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 
发布了500 篇原创文章 · 获赞 1352 · 访问量 188万+

猜你喜欢

转载自blog.csdn.net/Jmilk/article/details/105266905