C语言编译过程

源文件(.h, .c)
     \
      \
       +-------->预编译(预处理器 cpp)-------+
                                               \
                                                \
                                          被修改的源程序(.i 文本)
                                                /
                                               /
        <---------编译(编译器 ccl)<---------+
       /
      /
汇编程序(.s 文本)
     \
      \
       +-------->汇编(汇编器 as) ----------+
                                              \
                                               \
                     .a/.so               可重定位目标程序(.o 二进制)
                        |                      /
                        v                     /
       +------- 链接(链接器 ld)  <---------+
      /                 ^
     /                  |
    /                .o/.obj
   /
可执行目标程序(二进制)

以 gcc 编译器为例查看编译过程中的文件,首先查看 C 源文件:

$ file main.c
main.c: ASCII C program text

预处理(cpp): 预处理将处理以 # 开头的的命令,例如如果源文件包含 #include <stdio.h>,预处理器(cpp)会读取系统头文件 stdio.h 的内容,并把它直接插入的程序文本中。

$ gcc main.c -E -o main.i
$ file main.i
main.i: UTF-8 Unicode C program text

这时查看 main.i 可以看到预处理插入的内容。

编译(ccl): 编译器(ccl)将文本文件 main.i 翻译为文本文件 main.s,它包含的是汇编语言程序。

$ gcc main.i -S -o main.s
$ file main.s
main.s: ASCII assembler program text

汇编(as): 汇编器(as)将 main.s 翻译成机器语言指令,把这些指令打包成 可重定位目标程序 的格式,并将结果保存在目标文件 main.o中。

$ gcc main.s -c -o main.o
$ file main.o
main.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

链接(ld): 链接器(ld)将目标文件与其他目标文件链接,例如 main 程序中使用了函数 printf,链接器负责把 printf.o 合并到main程序中,得到可执行目标文件 main

$ gcc main.o -o main
$ file main
main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shaed libs), for GUN/Linux 2.6.18, not stripped

目标文件(Object File)

  • 可重定位目标文件:将单个 .c 文件编译为 .o 文件,文件中代码段 .text,数据段 .data, .bss 中引用的全局变量,静态变量,函数等地址并未最终确定
  • 可执行目标文件:链接器合并 .o 目标文件的代码段与数据段,并将代码中定义的符号重定位到最终确定的变量,函数地址

接下来我们将预处理、编译、汇编统称为编译,只考虑由 .c 文件编译为 .o 文件以及由 .o 文件链接为可执行目标文件的过程。

无论是 C 还是 C++,要生成可执行程序,首先要把源文件编译成中间目标文件,在 Windows 下也就是 .obj 文件,UNIX 下是 .o 文件,即 Object File。链接过程(link)负责把这些 Object File 合成可执行目标文件。

编译时,编译器只考虑语法的正确(静态检查),比如函数的声明、定义、调用是否匹配、变量的声明、定义、使用是否匹配(即使被调用函数未被声明,也可以生成 Object file,编译器会给出一个警告,但是如果函数有声明、定义、调用,那么它们必须匹配,否则在编译阶段会报错,无法生成 Object File)。

你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在 C/C++ 文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(.o文件或是.obj文件)。

我们可以使用这些中间目标文件(.o文件或是.obj文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只需要函数的中间目标文件(Object File)。链接器主要是链接函数和全局变量,在所有的 Object File 中寻找函数的实现,如果找不到,链接器会报错。

静态库(.a)

链接时需要明显地指出中间目标文件名,如果中间目标文件太多,会在执行编译或者分发文件时带来麻烦,这时可以将所有目标文件打包成静态库。

静态库由目标文件(.o)打包而成,在链接过程中,链接器在静态库中寻找需要的目标文件,并与我们的目标文件一起链接到可执行文件中,链接方式称为静态链接

  • 静态库对函数库的链接是放在编译时期完成的
  • 程序在运行时与函数库再无瓜葛,移植方便
  • 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件

动态库(.so)

动态库编译时并不会链接到可执行文件中,而是在程序运行时才被载入,由动态连接器执行重定位并完成链接。

动态链接器会将动态库与可执行文件的代码段和数据段分别加载到内存,然后重定位变量与函数的地址。

示例

编写 add.c, add.h,提供计算 2 数之和功能,内容如下:

// add.h
#ifndef ADD_H__
#define ADD_H__

int add(int a, int b);

#endif  // ADD_H__
// add.c
#include <stdio.h>
#include "add.h"

int add(int a, int b)
{
    int res = a + b;
    printf("In shared library, %d + %d = %d\n", a, b, res);
    return res;
}

编写 main.c 使用 add 函数,内容如下:

// main.c
#include <stdio.h>
#include "add.h"

int main(int argc, char *argv[])
{
    printf("%d\n", add(1, 2));
    return 0;
}

可重定位目标文件示例

$ gcc -c add.c
$ gcc -c main.c
$ gcc -o main main.o add.o
$ ./main
In shared library, 1 + 2 = 3
3

静态库示例

编译 add.c,得到重定位目标文件 add.o,并打包成静态库 libadd.a

$ gcc -c add.c
$ ar rcs libadd.a add.o

编译 main.c,将得到的 main.olibadd.a 静态链接:

$ gcc -c main.c
$ gcc -static -o main main.o ./libadd.a

或者:

$ gcc -c main.c
$ gcc -static -o main main.o -L. -ladd

运行可执行目标文件 main

$ ./main
In shared library, 1 + 2 = 3
3

动态库示例

  1. compile library source code into position-independent code (PIC)

     $ gcc -c -Wall -Werror -fpic add.c
    
  2. creating a shared library from an object file

     $ gcc -shared -o libadd.so add.o
    

或者可以将以上 2 步合并:

$ gcc add.c -pie -shared -o libadd.so

编译得到的 libadd.so 就是需要的动态库

编译 main,使用 -ladd 让 gcc 链接 libadd.so,使用 -L . 告诉 gcc 在当前目录下寻找动态库

$ gcc main.c -L. -ladd -o main

现在我们得到了可执行文件 main

$ export LD_LIBRARY_PATH=.
$ ./main
In shared library, 1 + 2 = 3
3

linux

$ ldd main

macos

$ otool -L main
动态库查找路径
  • LD_LIBRARY_PATH
  • rpath
  • /etc/ld.so.conf

运行时加载和链接共享库

#include <dlfcn.h>

// 打开某个库
void *dlopen(const char *filename, int flag);

// 查找库中的符号, 主要就是函数,获取函数地址之后我们就可以调用了
void *dlsym(void *handle, char *symbol);

// 关闭某个库
int dlclose(void *handle);

// 上述过程中如果出错了,调用这个函数获取错误信息
const char *dlerror(void);
// dyn_main.c
#include <stdio.h>
#include <dlfcn.h>

int main(int argc, char *argv[])
{
    void *dl = NULL;
    int (*add)(int a, int b);
    char *error;

    /* Dynamically load the shared library containing add() */
    dl = dlopen("./libadd.so", RTLD_LAZY);
    if (dl == NULL) {
        printf("so loading error.\n");
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }

    /* Get a pointer to the add() function we just loaded */
    add = (int(*)(int, int))dlsym(dl, "add");
    if ((error = dlerror()) != NULL) {
        printf("fun load error.\n");
        fprintf(stderr, "%s\n", error);
        return 1;
    }

    /* Now we can call add() just like any other function */
    printf("%d\n", add(1, 2));

    /* Unload the shared library */
    if (dlclose(dl) < 0) {
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }

    return 0;
}
$ gcc dyn_main.c -l dl -o dyn_main
$ ./dyn_main
In shared library, 1 + 2 = 3
3

参考