GCC 编译工具入门教程


GCC编译过程和原理浅析

1. 什么是GCC

  • GCC(GNU C Compiler)编译器的作者是Richard Stallman,也是GNU项目的奠基者。
  • GCC是GNU Compiler Collection的缩写。最初是作为C语言的编译器,现在已经支持多种语言了,如C、C++、Java、Pascal、Ada、COBOL语言等。
  • GCC支持多种硬件平台,甚至对Don Knuth设计的MMIX这类不常见的计算机都提供了完善的支持。

2. GCC的主要特征

  • GCC是一个可移植的编译器,支持多种硬件平台。
  • GCC不仅仅是本地编译器,它还能跨平台交叉编译。
  • GCC有多种语言前段,用于解析不同的语言。
  • GCC是按模块化设计的,可以加入新的语言和新CPU架构的支持。
  • GCC是自由软件。

3. GCC编译程序的过程

例如使用一个hello.c文件编译的过程如下图所示:

GCC 编译工具入门教程

  1. 预处理(Pre-Processing):主要包括宏定义,文件包含,条件编译三部分。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对其进行响应和替换。预处理过程还会删除程序中的注释和多余空白字符。最后会生成 .i 文件。
  2. 编译器(Compiling):编译器会将预处理完的 .i 文件进行一些列的语法分析,并优化后生成对应的汇编代码。会生成 .s 文件。
  3. 汇编器(Assembling):汇编器会将编译器生成的 .s 汇编程序汇编为机器语言或指令,也就是可以机器可以执行的二进制程序。会生成 .o 文件。
  4. 链接器(Linking):链接器会来链接程序运行的所需要的目标文件,以及依赖的库文件,最后生成可执行文件,以二进制形式存储在磁盘中。

4. GCC编译过程

4.1 GCC常用选项

  • -o:生成目标( .i.s.o 、可执行文件等)
  • -c:通知 gcc 取消链接步骤,即编译源码并在最后生成目标文件。
  • -E:只运行 C 预编译器
  • -S:告诉编译器产生汇编语言文件后停止编译,产生的汇编语言文件扩展名为 .s
  • -Wall:使 gcc 对源文件的代码有问题的地方发出警告
  • -Idir:将dir目录加入搜索头文件的目录路径
  • -Ldir:将dir目录加入搜索库的目录路径
  • -llib:连接lib库
  • -g:在目标文件中嵌入调试信息,以便gdb之类的调试程序调试

4.2 预处理过程

我们以 hello.c 程序为例:

#include <stdio.h>

#define HELLOWORLD ("hello world\n")

int main(void)
{
    printf(HELLOWORLD); 
    return 0;
}

使用gcc -E hello.c -o hello.i命令,将 hello.c 文件预处理并且生成 hello.i 目标文件。

之前说道,预处理会将头文件包含进来并且会将宏定义进行替换,因此替换后的 hello.i 文件如下:

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
.........

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

typedef signed char __int8_t;
typedef unsigned char __uint8_t;
.........

.........
extern struct _IO_FILE *stdin;
extern struct _IO_FILE *stdout;
extern struct _IO_FILE *stderr;
extern FILE *fopen (const char *__restrict __filename,
      const char *__restrict __modes) ;
.........

.........

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

可以看到将 stdio.h 文件包含进来,并且原封不动的将 HELLOWORLD 宏进行了替换。

4.3 编译过程

使用gcc -S hello.i -o hello.s,将生成的hello.i文件编译为汇编程序hello.s

    .file   "hello.c"
    .section    .rodata
.LC0:
    .string "hello world"
    .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: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

可以看到hello.s文件中全部都是汇编指令,说明已经生成成功了。

4.4 汇编过程

汇编就是要将hello.s文件中的汇编指令全部转换为二进制的机器指令。

执行gcc -c hello.s -o hello.o命令。而生成的hello.o文件是二进制文件,我们用od -b hello.o命令看一下该二进制文件的八进制表示。

➜  test od -b -w8 hello.o
0000000 177 105 114 106 002 001 001 000
0000010 000 000 000 000 000 000 000 000
0000020 001 000 076 000 001 000 000 000
0000030 000 000 000 000 000 000 000 000
..........
0002710 000 000 000 000 000 000 000 000
0002720 001 000 000 000 000 000 000 000
0002730 000 000 000 000 000 000 000 000
0002740
// -b:八进制形式,1字节为1部分。
// -w8:每行显式8字节。

4.5 链接过程

链接hello.o程序运行的所需要的目标文件,以及依赖的库文件,最后生成可执行文件。

执行gcc hello.o -o hello不需要选项,生成hello二进制的可执行文件。同样可以使用od命令来查看。执行hello文件:

➜  test ./hello 
hello world

以上编译过程的分步骤进行,还可以直接执行gcc hello.c -o hello直接生成可执行文件。

至此,使用gcc编译程序的过程就介绍���毕。

5. GCC 编译多文件

  • 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

5.1 一次性编译

执行gcc -Wall hello.c main.c -o main命令,直接生成可执行文件main

➜  test gcc -Wall hello.c main.c -o main
➜  test ./main 
hello world

5.2 独立编译

先分别将main.chello.c编译生成main.ohello.o文件。然后将两个.o文件链接生成可执行文件newmain

➜  test gcc -Wall -c main.c -o main.o
➜  test gcc -Wall -c hello.c -o hello.o
➜  test gcc -Wall hello.o main.o -o newmain
➜  test ./newmain 
hello world

独立编译的好处就是,如果我们的代码发生更改,只需要独立编译更改的文件,最后在一起链接生成可执行文件。

外部库、共享库、静态库、动态库

1. 头文件与库文件

  • 在使用C语言和其他语言进行程序设计时,需要头文件来提供对常数的定义和对系统及函数调用的声明。
  • 库文件是一些预先编译好的函数集合,那些函数都是按照可重用原则编写的。他们通常有一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显式情况的函数(ncurses库)和数据库访问例程(dbm库)等。
    • 使用库的好处:
    • 模块化:将不同功能模块的文件编译成不同的库,有利用明确项目之间的分工。
    • 可重用性高:无需关心内部实现,直接调用即可。
    • 可维护性:当库文件的代码发生改变,无需更改使用库文件的代码,增强代码的可维护性。

2. 头文件与库文件的位置

  • /usr/include 及其子目录底下的 include 文件夹
  • /usr/loacl/include 及其子目录底下的 include 文件夹
  • /usr/lib
  • /usr/local/lib

3. 使用外部库(-l选项)

如下面这个例子:

#include <math.h>
#include <stdio.h>

int main(void)
{
    double d = pow(2.0, 4.0);
    printf("The cubed is %f\n", d);

    return 0;
}

我们想使用math.h库中的pow()函数,因此在编译时,需要连接对应的库。执行如下命令:

➜  test gcc -Wall calc.c -o calc -lm
➜  test ./calc 
The cubed is 16.000000

其中-lm表示要链接libm.so或者libm.a库文件。

4. 静态库与共享库

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候不再需要静态库。静态库会被链接到可执行文件中,比较占用磁盘空间,并且执行时会将库的代码加载到内存,占用更多的内存,并且多个程序无法共享。
  • 共享库(.so 或 .sa):程序在运行的时候才去链接共享库的代码,多个程序可以共享使用库的代码。共享库可以在多个程序间共享,所以动态链接使得可执行程序文件更小,节省了磁盘空间,操作系统采用虚拟内存机制允许物理内存中的一份共享库被要用到该库的所有进程共用,节省了内存和磁盘空间。

一个与共享库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数在目标文件的整个机器码。

在可执行文件开始运行以前,外部的机器码由操作系统从磁盘上的该共享库中复制到内存中,这个过程成为动态链接(dynamic linking)。

5. 生成静态库

生成静态库可以理解为将一个对.o文件的归档。将一个或多个.o文件打包生成.a文件。

可以使用ar工具,例如:

➜  test gcc -Wall -c hello.c -o hello.o 
➜  test ar rcs libhello.a hello.o      
➜  test ls -l libhello.a 
-rw-rw-r-- 1 menwen menwen 1518 716 14:33 libhello.a
// ar是gnu的归档工具
// rcs = replace、create、save

接下来就是将生成的.a库文件和调用库函数的文件生成可执行文件。

➜  test gcc -Wall main.c libhello.a -o main
➜  test ./main 
hello world

这样生成可执行文件时要注意main.c libhello.a这两个文件的书写顺序,否则编译会出错。

还有一种方法来生成可执行文件,就是通过-l选项。例如:

➜  makefile gcc -Wall -L. main.c -o main -lhello
➜  makefile ./main 
hello world
// -L.:-L选项来指定库的位置,在当前目录下

6. 生成共享库

生成共享库的过程如下:

➜  makefile gcc -Wall -c -fPIC hello.c              // 编译生成.o文件时一定要加上-fPIC选项
➜  makefile gcc -shared -fPIC hello.o -o libhello.so
➜  makefile ls -l libhello.so 
-rwxrwxr-x 1 menwen menwen 8024 716 14:58 libhello.so
// -shared:生成共享库格式
// -fPIC:产生位置无关码(position independent code)

链接库生成可执行文件:

gcc -Wall -L. main.o -o main -lhello
➜  test ./main 
./main: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory

但是执行可执行文件时报错:无法链接到共享库。因此我们要运行共享库,一共有三种方式:

  1. 拷贝.so文件到系统共享库路径下,一般是/usr/lib
  2. 更改LD_LIBRARY_PATH
  3. ldconfig。配置/etc/ld.so.conf,ldconfig更新ld.so.cache

我们将生成的.so库拷贝到系统共享库路径下,就可以成功执行

➜  test sudo cp libhello.so /usr/lib
➜  test ./main 
hello world

7. 库搜索路径

  • C_INCLUDE_PATH、LIBRARY_PATH(在~/.bash_profile文件中配置用户的环境变量)
  • 从左到右搜索-I、-L指定的目录
  • 由环境变量指定的目录
  • 由系统指定的目录

如果按照以上顺序无法找到链接的库,则会报错。

相关内容