Linux 编译与链接

Linux 下 C/C++ 代码编译链接生成可执行程序的过程,主要包括如下几个步骤。

预处理

1
2
#只进行预处理并把预处理结果输出
g++ -E source.cpp -o source.i

预处理过程主要处理源代码文件中以 # 开始的预编译指令,包括如下规则。

  • 将所有的 #define 删除,并且展开所有的宏定义。
  • 处理所有的条件预编译指令,包括 #if#ifdef#ifndef#elif#else#endif 等。
  • 处理 #include 预编译指令,将被包含的文件插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件,也会同样被插入。
  • 过滤所有的注释中的内容,包括 ///* */
  • 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
  • 保留所有的 #pragma 编译器指令,因为编译器需要使用它们。

编译

1
2
#输出编译后的汇编代码文件
g++ -S source.i -o source.s

编译过程就是把预处理完的文件进行如下的一系列操作后产生相应的汇编代码文件。这个过程往往是整个程序构建的核心部分,也是最复杂的部分之一。

  • 词法分析
  • 语法分析
  • 语义分析
  • 源代码优化
  • 代码生成
  • 目标代码优化
    这里不展开一一赘述。

通过如下命令,我们可以直接从源文件得到目标文件/中间代码文件,如 .o 文件。

1
2
#只编译不链接
g++ -c source.cpp

链接

各个源代码模块独立地编译成目标文件,链接就是把这些目标文件和库链接形成可执行文件的过程。
这里的“库”指的是:一组目标文件的包,也就是将一些常用的代码编译成目标文件后打包存放,便于使用。
链接分为静态链接和动态链接。

静态链接

对函数库的链接是放在编译时期完成的是静态链接。
静态库文件命名格式为:libNAME.a 。
有如下 5 个文件:

1
2
3
4
5
//add.h
#ifndef _ADD_H_
#define _ADD_H_
int add(int a, int b);
#endif
1
2
3
4
5
6
//add.cpp
#include "add.h"
int add(int a, int b)
{
return a + b;
}
1
2
3
4
5
//sub.h
#ifndef _SUB_H_
#define _SUB_H_
int sub(int a, int b);
#endif
1
2
3
4
5
6
//sub.cpp
#include "sub.h"
int sub(int a, int b)
{
return a - b;
}
1
2
3
4
5
6
7
8
9
10
11
//main.cpp
#include "add.h"
#include "sub.h"
#include <iostream>
using namespace std;
int main()
{
cout << "1+2=" << add(1, 2) << endl;
cout << "1-2=" << sub(1, 2) << endl;
return 0;
}

生成静态库并链接形成可执行文件的过程如下:

将 add.cpp 和 sub.cpp 编译成 .o 文件

1
2
g++ -c add.cpp
g++ -c sub.cpp

由 .o 文件创建静态库

1
ar cr libmymath.a sub.o add.o

链接静态库并生成可执行文件 main

1
2
3
# -L 额外指定库搜索路径
# -l 额外指定链接的库
g++ -o main main.cpp -L. -lmymath

ar 命令用于创建和维护库文件。

c 选项:不管是否已存在,创建一个库。

r 选项: 在库中插入或替换模块。

tv 选项: 显示库中有哪些目标文件,显示文件名、时间、大小等详细信息。

运行

1
./main

动态链接

对库函数的链接载入推迟到程序运行时期就是动态链接。
动态库文件命名格式为:libNAME.so 。
同样以上面的 5 个文件为例。

如果你边跟着本文顺序,边在动手实践的话,此时请暂时将静态库文件 libmymath.a 删除或重命名成其他名字。减少干扰。

生成动态库并链接形成可执行文件的过程如下:

生成动态库

1
2
3
4
5
6
7
# 1.
g++ -fPIC -o add.o -c add.cpp
g++ -fPIC -o sub.o -c sub.cpp
g++ -shared -o libmymath.so add.o sub.o
# 或者
# 2.
g++ -fPIC -shared -o libmymath.so add.cpp sub.cpp

-fPIC 编译为位置独立的代码。

生成目标文件

1
g++ -o main main.cpp -L. -lmymath

运行

1
./main
CentOS 等 Linux 系统下

直接运行会报错
./main: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory

使用 ldd 命令来查看目标文件
shell ldd main

shell linux-vdso.so.1 => (0x00007ffefc135000) libmymath.so => not found libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007ff6d629e000) libm.so.6 => /lib64/libm.so.6 (0x00007ff6d5f9c000) libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007ff6d5d86000) libc.so.6 => /lib64/libc.so.6 (0x00007ff6d59c4000) /lib64/ld-linux-x86-64.so.2 (0x00007ff6d65af000)

这是因为系统运行 main 时,会按照如下顺序搜索动态库:

  1. 编译目标代码时指定的动态库搜索路径
  2. 环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径
  3. 配置文件 /etc/ld.so.conf 中指定的动态库搜索路径
  4. 默认的动态库搜索路径 /lib
  5. 默认的动态库搜索路径 /usr/lib
    也就找不到我们生成的 libmymath.so 。

相应地有以下几种方法可以告诉系统如何找到我们需要的动态库。

  • 编译时增加 -Wl,-rpath来指定额外的动态库搜索路径。当指定多个动态库搜索路径时,路径之间用冒号 分隔。
    shell g++ -o main main.cpp -L. -lmymath -Wl,-rpath=.

  • 修改环境变量 LD_LIBRARY_PATH
    shell export LD_LIBRARY_PATH=/PATH_TO_LIB:$LD_LIBRARY_PATH

  • 修改配置文件 /etc/ld.so.conf

sudo vi /etc/ld.so.conf
将动态库所在路径添加到 ld.so.conf
PATH_TO_LIB
使修改生效
sudo ldconfig

  • 将我们的库添加到默认路径中
1
2
3
4
5
6
7
sudo cp libmymath.so /usr/lib

#
sudo cp libmymath.so /lib

# 如果复制后还是找不到动态库文件,可执行下述命令,详情请看 参考里的 ldconfig 详解
# sudo ldconfig
MacOS 下

系统会自动在目标文件所在路径寻找动态库文件,所以可以直接运行不会报错。
Mac 下没有 ldd命令, 可通过 otool -L来查看目标文件的依赖。

1
otool -L main
1
2
3
4
main:
libmymath_one.so (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 307.5.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.60.2)

动态库和静态库同名问题

以上述文件为例,我们在同一目录中分别执行命令生成动态库和静态库,即 libmymath.solibmymath.a
此时执行 g++ -o main main.cpp -L. -lmymath,然后运行目标文件 ./main,会看到
./main: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory
也就是说编译器优先采用了动态链接来生成目标文件:首先到path目录下搜索 libXXX.so动态库文件,如果没有找到,才会搜索 libXXX.a静态库文件。

动态库与静态库的对比

  • 动态库
    • 有利于进程间资源共享,减少资源占用。
    • 方便升级。只要接口不变,程序无需重新编译。
    • 链接载入完全由程序员在程序代码中控制。
  • 静态库
    • 执行速度快。
    • 编译后静态库已打包在目标文件中,减少了依赖。

g++ 与 gcc 的对比

  • g++ 和 g++ 都可以编译 C++ 代码。在编译阶段 g++ 会自动调用 gcc ,两者是等价的。
  • 后缀为 .c 的代码, gcc 把它当作是 C 程序, 而 g++ 当作 C++ 程序;后缀为 .cpp 的,两者都会将其当作 C++ 程序。
  • 链接可以用 g++ 或 gcc-lstdc++ 。 gcc 命令不能自动和 C++ 程序使用的库链接。
  • __cplusplus宏标志着编译器会把代码按 C 还是 C++ 语法来解释。对于 g++ 来说,该宏都会定义;对于 gcc 来说,该宏是否定义取决于文件后缀。

    以下述 .c 文件为例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #source.c
    #include <stdio.h>

    int main()
    {
    #ifdef __cplusplus
    printf("define __cplusplus\n");
    #else
    printf("undefine __cplusplus\n");
    #endif
    return 0;
    }

    使用 gcc 编译生成目标文件并运行

    1
    2
    gcc -o source source.c
    ./source

    会看到

    1
    undefine __cplusplus

    将后缀改成 .cpp ,重新编译运行

    1
    2
    3
    mv source.c source.cpp
    gcc -o source source.cpp
    ./source

    会看到

    1
    define __cplusplus
  • g++ 和 gcc 对采用了 extern "C"的 symbol 都会以 C 的方式来命名。

    以下述文件为例

    1
    2
    3
    4
    5
    // me.h
    #ifndef _ME_H_
    #define _ME_H_
    extern "C" void CppPrintf(void);
    #endif
    1
    2
    3
    4
    5
    6
    7
    8
    // me.cpp
    #include <iostream>
    #include "me.h"
    using namespace std;
    void CppPrintf(void)
    {
    cout << "Hello" << endl;
    }
    1
    2
    3
    4
    5
    6
    7
    // test.cpp
    #include "me.h"
    int main()
    {
    CppPrintf();
    return 0;
    }

    执行下述命令

    1
    2
    3
    4
    5
    gcc -S me.cpp
    # 或者
    # g++ -S me.cpp
    # 效果相同
    less me.s

    可以看到

    1
    call    CppPrintf

    将 me.h 中的 extern "C"删除后,执行下述命令

    1
    2
    3
    4
    5
    gcc -S me.cpp
    # 或者
    # g++ -S me.cpp
    # 效果相同
    less me.s

    可以看到

    1
    call    _Z9CppPrintfv

    参考

  • 《后台开发:核心技术与应用实践》第 4 章 编译

  • ldconfig 命令