# C和Cpp编译链接过程

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

# 整体介绍
C和Cpp代码编译链接的整体过程如下：

![compile_flow.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1630209018685/KsE5wZ5XI.png)

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

# 预编译
## 作用
预编译基本上完成了对源程序的“替代”工作。经过预处理后的代码与源代码相同，且依然可以被我们读懂，只是内容上有些修修剪剪
1. 删除注释
2. 展开头文件，即在`#include`位置上将对应的头文件代码展开
3. 宏替换，即将`#define`宏对应的代码直接替换
4. 条件编译，即对`#ifndef`、`#define`、`#endif`、`#if`、`#else`等条件进行判断检查。我们经常看到头文件会用`#ifndef`这一系列包围起来以防止重复引用，就是在这一步生效
5. 特殊符号处理，例如在源程序中出现的`__LINE__`标识将被解释为当前行号（十进制）

## 示例
为了简单的测试，写3个文件。其中，`extern "C"`的介绍详见后续[符号表](#符号表)部分，当前不用在意
1. 头文件`test.h`

```c++
#ifdef __cplusplus
extern "C" {
#endif

int Multiply(int, int);

#ifdef __cplusplus
}
#endif

int Minus(int, int);
```

2. 源文件`test.cpp`

```c++
#include "test.h"

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

int Multiply(int a, int b)
{
    return (a * b);
}
```

3. main函数所在文件`main.cpp`

```c++
#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;
}
```

4. 执行``gcc -E main.cpp -o main.i``或者``cpp main.cpp > main.i``都可以生成预编译文件。得到的内容如下：

```c++
# 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`文件或者目标文件。目标文件大体分为数据段、代码段、符号表三个部分，这也是经常能看到的一些词眼，这里借用[彻底理解链接器：二，符号决议](https://segmentfault.com/a/1190000016433829)中的一幅图来表示：

![obj_file.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1630209033321/3wsuTnBgI.png)

## 符号表
此处并不对数据段和代码段多做展开，详细可以参考刚刚的链接中的这篇文章，写的挺不错。另外符号表的查看可以采用`nm`命令，具体可以参见[Linux nm 命令使用及符号含义](https://blog.csdn.net/xuq09/article/details/87970376)这篇文章。此处执行`nm main.o`结果如下
```shell
0000000000000000 T main
                 U Multiply
                 U _Z5Minusii
```

换成`nm -C main.o`命令的结果如下
```shell
0000000000000000 T main
                 U Multiply
                 U Minus(int, int)
```

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

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

链接程序的作用就是将有关的文件彼此相连接，使其成为一个整体，这个整体分为三种：
1. 可执行文件，即可以被操作系统运行的程序
2. 库文件。当前有两种，静态库文件和动态库文件
3. 目标文件，即生成的仍然是一个`.o`文件

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

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

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

## 示例
仍然以之前的程序为例，执行`gcc test.o main.o -o main`得到可执行文件`main`。对链接程序生成的上述3种结果，可以通过`ldd`查看对应的链接库调用关系，执行`ldd main`得到结果如下：
```shell
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`生成一个动态库文件，如下：
```shell
gcc -shared test.cpp -o libtest.so
gcc main.cpp -o main -L. -ltest
```

先修改配置下库文件搜索路径，执行`export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH`,然后再次查看链接关系如下：
```shell
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++程序编译过程详解](https://www.cnblogs.com/mickole/articles/3659112.html)
2. [Linker (computing)](https://en.wikipedia.org/wiki/Linker_(computing))
3. [彻底理解链接器：二，符号决议](https://segmentfault.com/a/1190000016433829)
4. [彻底理解链接器：三，库与可执行文件](https://segmentfault.com/a/1190000016433897)
5. [Linux nm 命令使用及符号含义](https://blog.csdn.net/xuq09/article/details/87970376)

