代码编译过程

代码编译过程可以分为以下几个阶段:

编译阶段功能产物产生目标产物的命令执行程序

预编译

展开宏、删除注释

.i

gcc -E

cpp

编译

生成汇编代码

.s

gcc -S -masm=intel

ccl

汇编

生成目标文件

.o

g++ -c -o

as

链接

对目标文件进行链接

.out

gcc -o

ld

预编译阶段不进行语法解析,只进行简单的宏展开

使用 objdump -d 可以反编译一个二进制文件

位置独立代码[1]

所谓 位置独立代码 (Position Independent Code) 可以理解为代码无绝对跳转,跳转都为相对跳转。生成动态库时,需要加上 -fPIC 选项。

添加 -fPIC 选项生成的动态库,是位置无关的。这样的代码本身就能被放到线性地址空间的任意位置,无需修改就能正确执行。动态库在内存中只需要加载一次,而可以被映射到多个进程的虚拟空间中。

实际上,即使是不加 -fPIC 选项也可以生成动态库,但是这要求动态库 不能与引用其他代码 (这里我认为实际上是不能引用其他动态库中的函数)

不添加 -fPIC 生成的动态库,生成时假定它被加载在地址 0 处。加载时它会被加载到一个地址 (base),需要进行一次重定位,代码、数据段中所有的地址加上这个 base 的值。这时代码运行时就能使用正确的地址了。

位置相关的动态库在每次被引用时都会生成一份新的副本,但是由于不需要进行内存映射,因此加载速度比较快

静态库、动态库和小库[2]

在解释静态库和动态库之前,需要简单了解一下什么是目标文件。目标文件常常按照特定格式来组织,在linux下,它是 ELF (Executable Linkable Format) ,而在 Windows 下是 PE (Portable Executable)

目标文件有三种:

  • 可执行目标文件。即我们通常所认识的,可直接运行的二进制文件。

  • 可重定位目标文件。包含了二进制的代码和数据,可以与其他可重定位目标文件合并,并创建一个可执行目标文件。

  • 共享目标文件。它是一种在加载或者运行时进行链接的特殊可重定位目标文件。

将第二种目标文件以一种特定的方式打包成一个单独的文件,并且在链接生成可执行文件时,从这个单独的文件中“拷贝”它自己需要的内容到最终的可执行文件中。这个单独的文件,称为静态库。Linux 中通常以 .a (archive) 为后缀。而 Windows 下以 .lib (library) 结尾。

  • 静态库使用 ar 创建:[1]

    g++ -c math.cpp
    ar -crv libmath.a math.o
  • 静态库使用 static 链接:

    gcc -static -o main main.o -lmath

    这里 -lmath 需要放在后面,否则的话编译期在扫描的时候先扫描前面的库文件,后面的目标文件就没办法找到需要的函数了。

动态库和静态库类似,但是它并不在链接时将需要的二进制代码都“拷贝”到可执行文件中,而是仅仅添加一些重定位和符号表信息用来帮助程序在运行时加载二进制代码。Linux中通常以 .so(Shared Object)作为后缀。Windows 下以 .dll (Dynamic Link Library) 结尾。

动态库不拷贝所有信息不代表不需要信息,编译时链接动态库依然需要拷贝必要的信息,这部分信息在 Linux 下由动态库文件提供,在 Windows 由导入库(.lib)提供。
  • 动态库使用 shared 创建

    g++ -fPIC -shared math.cpp -o libmath.so
  • 使用动态库

    链接时默认就是使用动态库,无需任何指定

    g++ main.cpp -lmath

    动态库的需要放在以下位置之一操作系统才能找到:

    • 可执行文件同路径

    • 系统默认库路径(/usr/lib 或 C:/windows/)

小库 (Little libraries)

小库与动态库类似,在运行时加载,但是编译时不需要任何额外信息,代价是需要手动装入库和解析库函数。

小库使用 dlopendlclosedlsym 用来打开库、关闭库、解析库。[2]

小库使用 -ldl 编译

静态库

下面描述如何手动创建静态库:

  1. 创建源文件

    // FUNC.cpp
    #include <iostream>
       void FUNC(void){
       std::cout<<"FUNC lib was called"<<std::endl;
    }
    // test.cpp
    void FUNC(void);
    int main(){
       FUNC();
       return 0;
    }
  2. 将源文件编译为二进制文件(使用 gcc -c)

gcc -c FUNC.cpp
  1. 将二进制文件编译为库文件(使用 ar crs)

ar crs libFUNC.a FUNC.o
  1. 链接库 (使用 - l + 库名的形式,如 - liostream)

gcc test.cpp -L. -lFUNC -lstdc++ -o test
或者
gcc test.cpp libFUNC.a -lstdc++ -o test

动态库

使用的代码与上述相同,但是编译命令变为

gcc -shared -o libFUNC.so FUNC.cpp
gcc test.cpp -L. -lFUNC -o test -lstdc++

C 库是默认加载的,因此无需显式指定,但是数学库、线程库需要手动加载

Windows 下的动态库

与 Linux 有所不同的是,Windows 将动态库拆分为两部分,一部分是运行时需要的 dll 文件,另一部分是编译期需要的 lib 文件。

因此,Windows 下使用动态库有两种方式:

  • 使用 lib 文件直接链接

  • 手动解析

第一种可以使用 #pragma comment(lib, "ExportClass.lib") 的形式显示加载动态库

第二种方式代码如下:

HINSTANCE hDll = LoadLibrary(L"ExportClass.dll");
using funPtr = InterfaceClass *(*)();
funPtr getInstance = (funPtr)GetProcAddress(hDll, "getInstance");
InterfaceClass* ptr = getInstance();

在 Linux 也有类似的 dlopendlclose 方法。说实话一般而言我并不建议手动打开动态库并进行解析,这样很麻烦,而且有一种 bad smell 的感觉。只有两种情况应当使用这种方式:

  • 你手里只有动态库,没有头文件和 dll

  • 有些库是运行时才能确认的(比如插件),而在编译时无法确定

另外有一点需要注意的是标准库(和 Boost)不提供二进制兼容性,在使用时需要注意。一种比较好的方式是使用 d 指针。

导出函数和类

导出函数很简单,只需要使用 extern "C" 就行了。但是由于 C 语言中不存在类,所以没法导出类。类需要使用 __declspec(dllexport) 导出

说实话我不知道为什么需要用这个 API,可能是由于我开发一般是在 Linux + CMake。CMake 帮我做了这件事,所以我基本上没有关心过如何导出库的问题。另一方面很多人导出类都是先写一个创建对象的函数,然后将这个函数导出就行了。

二进制构成

C++ 二进制文件由五部分组成:[3]

  • BSS (Block Started by Symbol) 用来存放程序中未初始化的全局变量的一块内存区域

  • 数据段:通常是指用来存放程序中已初始化的全局变量和静态变量的一块内存区域

  • 代码段:用来存放程序执行代码的一块内存区域,通常只读,也有可能包含一些只读的常数变量,例如字符串常量等

  • 堆:于存放进程运行中被动态分配的内存段

  • 栈:用户存放程序临时创建的局部变量

段错误

段错误一般可能由以下情况引起:

  • 使用了空指针

  • 数组越界

  • 迭代器失效

  • 迭代太深导致栈溢出

找不到符号

找不到符号通常由以下原因:

  • 没有链接库。

  • 符号不同。

    C 语言和 C++ 的符号形式不同,如果在 C++ 中引入和 C 头文件而没有添加 extern "C" 就很容易导致链接问题。这种问题检查的方式很简单:C 的符号通常以下划线开始,且只有函数名(因为 C 没有重载),而 C++ 的符号没有下划线起始,且报错时提供的是完整的函数签名。
  • 库链接顺序问题。

  • 符号被裁剪掉。

Last moify: 2024-12-23 07:55:59
Build time:2025-07-18 09:41:42
Powered By asphinx