C和Cpp编译链接过程

以下介绍中有2个约定:

  1. 下面介绍中使用的编译器为gcc/g++,所有执行命令中gcc可以与g++互换
  2. gcc/g++编译命令允许跳过中间文件的生成,而直接得到所需的输出文件。但为了逻辑连贯,默认每一阶段将以上一阶段的输出文件作为源文件,后续不在赘述

整体介绍

C和Cpp代码编译链接的整体过程如下:

compile_flow.png

狭义上来说,编译就是指上图中从预处理文件到汇编语言文件的这个阶段,但是广义上来说,编译是链接之前的所有阶段。每个人的叫法并不相同,需要具体看待。不过下面的分析中,为了更详细的介绍,所以遵循狭义上的概念

预编译

作用

预编译基本上完成了对源程序的“替代”工作。经过预处理后的代码与源代码相同,且依然可以被我们读懂,只是内容上有些修修剪剪

  1. 删除注释
  2. 展开头文件,即在#include位置上将对应的头文件代码展开
  3. 宏替换,即将#define宏对应的代码直接替换
  4. 条件编译,即对#ifndef#define#endif#if#else等条件进行判断检查。我们经常看到头文件会用#ifndef这一系列包围起来以防止重复引用,就是在这一步生效
  5. 特殊符号处理,例如在源程序中出现的__LINE__标识将被解释为当前行号(十进制)

示例

为了简单的测试,写3个文件。其中,extern "C"的介绍详见后续符号表部分,当前不用在意

  1. 头文件test.h
#ifdef __cplusplus
extern "C" {
#endif

int Multiply(int, int);

#ifdef __cplusplus
}
#endif

int Minus(int, int);
  1. 源文件test.cpp
#include "test.h"

int Minus(int a, int b)
{
    return (a - b);
}

int Multiply(int a, int b)
{
    return (a * b);
}
  1. main函数所在文件main.cpp
#include "test.h"

#define ADD(a, b) (b > 0 ? a : 0)

int main()
{
    // This is a test
    int b;
#if 0
    b = 2;
#else
    b = 4;
#endif
    int c = ADD(__LINE__, b);
    int d = Minus(__LINE__, b);
    int f = Multiply(__LINE__, b);

    // return
    return 0;
}
  1. 执行gcc -E main.cpp -o main.i或者cpp main.cpp > main.i都可以生成预编译文件。得到的内容如下:
# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "main.cpp"
# 1 "test.h" 1

extern "C" {


int Multiply(int, int);


}


int Minus(int, int);
# 2 "main.cpp" 2



int main()
{

    int b;



    b = 4;

    int c = (b > 0 ? 14 : 0);
    int d = Minus(15, b);
    int f = Multiply(16, b);


    return 0;
}

编译

编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码(当前一般为汇编代码)。这一部分的内容较深了,不做详细介绍。

稍微引申一点,词法分析和语法分析也不只有编译器才会关注。事实上,对于涉及到语言转换、优化一类的都会使用,例如数据库中的sql解析就有用到

照样以预编译处所说的代码为例,可以执行gcc -S main.i -o main.s将预编译的代码生成为汇编代码。此时的汇编代码想要查看需要通过其它软件,这里也不再详细介绍了

汇编

汇编过程实际上指把汇编语言代码翻译成机器指令的过程。可以执行gcc -c main.s -o main.o实现这一过程,.o文件也称为obj文件、object文件或者目标文件。目标文件大体分为数据段、代码段、符号表三个部分,这也是经常能看到的一些词眼,这里借用彻底理解链接器:二,符号决议中的一幅图来表示:

obj_file.png

符号表

此处并不对数据段和代码段多做展开,详细可以参考刚刚的链接中的这篇文章,写的挺不错。另外符号表的查看可以采用nm命令,具体可以参见Linux nm 命令使用及符号含义这篇文章。此处执行nm main.o结果如下

0000000000000000 T main
                 U Multiply
                 U _Z5Minusii

换成nm -C main.o命令的结果如下

0000000000000000 T main
                 U Multiply
                 U Minus(int, int)
  1. 可以看到C与C++符号表的不同之处,即C++的函数符号会进行编码,最后出来的无法直观阅读理解,而C的函数符号就是函数名。这也是C和C++代码在相互调用时需要对函数声明加上extern "C"的作用
  2. 此时MultiplyMinus两个函数都定义在test.cpp文件中,所以从main.o看来,其符号表的这个两个函数就处于未定义状态

链接

由汇编生成的文件并不能马上执行,其中还有一些没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件(其中的变量、函数等);在程序中可能调用了某个库文件中的函数。所有的这些问题,都需要经链接程序的处理才能解决。链接器的详细介绍推荐参看英文版的维基百科),中文版写的并不好,而且有疏漏。

链接程序的作用就是将有关的文件彼此相连接,使其成为一个整体,这个整体分为三种:

  1. 可执行文件,即可以被操作系统运行的程序
  2. 库文件。当前有两种,静态库文件和动态库文件
  3. 目标文件,即生成的仍然是一个.o文件

链接方式

根据链接方式的不同,链接处理可分为两种:静态链接和动态链接。下面是这两种方式的简单介绍,更详细的可以参考资料彻底理解链接器:三,库与可执行文件,这篇文章写的确实挺好,我自问弗如

静态链接

在这种方式下,代码将从所在的静态链接库被拷贝到可执行程序中。简而言之,最终生成的可执行文件包含了所有的静态库代码,以后运行就跟它们没关系了

动态链接

在这种方式下,代码仍然是放在动态链接库中的,此时链接程序只是记录下了这些库文件的少量信息。在可执行文件运行的时候,还需要跑到这些库文件中找到对应的代码才行。简而言之,可执行文件和这些动态库绑在一块儿了,以后运行离了它们不行

示例

仍然以之前的程序为例,执行gcc test.o main.o -o main得到可执行文件main。对链接程序生成的上述3种结果,可以通过ldd查看对应的链接库调用关系,执行ldd main得到结果如下:

linux-vdso.so.1 (0x00007ffd245fc000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fb759af2000)
libm.so.6 => /lib64/libm.so.6 (0x00007fb759770000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fb759558000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb759196000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb759e87000)

此时可执行文件main相对简单,所以依赖的全都是系统库文件。修改编译命令,将test.cpp生成一个动态库文件,如下:

gcc -shared test.cpp -o libtest.so
gcc main.cpp -o main -L. -ltest

先修改配置下库文件搜索路径,执行export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH,然后再次查看链接关系如下:

linux-vdso.so.1 (0x00007fff3a7a0000)
libtest.so => /home/vscode/RemoteWorking/test/libtest.so (0x00007fe158806000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fe158471000)
libm.so.6 => /lib64/libm.so.6 (0x00007fe1580ef000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fe157ed7000)
libc.so.6 => /lib64/libc.so.6 (0x00007fe157b15000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe158a08000)

参考资料

  1. C/C++程序编译过程详解
  2. Linker (computing))
  3. 彻底理解链接器:二,符号决议
  4. 彻底理解链接器:三,库与可执行文件
  5. Linux nm 命令使用及符号含义