C++ 跨平台开发 - 二进制分发

从 12 年开始接触安卓 NDK 开发,到 17 年接触 iOS ObjC/C++ 混编,再到去年接触鸿蒙 NDK 开发和 WebAssembly 开发,也算是自诩有过较丰富的跨平台开发经验。

但直到最近适配 Windows 平台,才越来越意识到:搞定了 Windows,才有资格说自己做过真正的跨平台 – 因为它是唯一同时支持三大编译器(Clang/GCC/MSVC)的平台,编译环境可谓是最复杂的。

编译器的差异,再叠加各操作系统的差异,使得 C++ 二进制的跨平台分发变得复杂。

运行时环境

这里的“运行时”并不是 JVM/Dalvik/ART 那种动态解释型语言的运行时,而是不同编译器的 C++ 标准库实现(ISO C++ 只制定标准,不严格限制具体实现,臭名昭著的 C++ 二进制兼容问题也由此而来)。

GCC

  • 标准库实现为 libstdc++;

  • 随编译器发布,静态/动态链接均可;

  • 更新编译器即可享受最新 C++ 标准 feature;

  • 应用需要带上标准库,可能存在兼容问题。

Clang

  • 标准库实现为 libc++;

Android/HarmonyOS

  • 分发方式同 GCC;优势和劣势也类似;

Apple

  • 随系统发布;

  • 所有应用共用,不用担心兼容问题;

  • 如果使用 C++ 新特性,不光代码要通过 feature 宏判断,编译时还要判断 target 系统版本;

MSVC

  • 运行时包含:系统自带的 UCRT(Universal C RunTime) + 各个版本对应的 vcruntime 和 msvcp 的 dll;

  • 要么静态链接,要么安装各版本 dll(Visual C++ Redistributable);

MinGW

  • 使用 POSIX 兼容层(如 winpthreads)模拟 Unix 行为;

  • 与其他编译器兼容性差,仅适用于:完全不依赖 Windows API、且需要严格兼容 POSIX;

Clang-CL

  • Clang-CL 本质是 Windows 平台的 LLVM 驱动 - 模拟 MSVC 编译器前端(cl.exe)的行为;

  • 默认链接 MSVC 的运行时库;

  • 跨平台首选:既能享受 Clang 的优势,又能调用 Windows API。

二进制格式

编译产物格式

ELF

即 Executable and Linkable Format,是 Linux/Android/HarmonyOS/WebAssembly 的二进制格式;

ELF Header
├── e_ident (魔数 + 标识)
│   ├── EI_MAG0-EI_MAG3 (魔数 "ELF")
│   ├── EI_CLASS (类:32/64 位)
│   ├── EI_DATA (数据编码:小端/大端)
│   ├── EI_VERSION (版本)
│   ├── EI_OSABI (OS/ABI 标识)
│   └── EI_ABIVERSION (ABI 版本)
├── e_type (文件类型:ET_REL, ET_EXEC, ET_DYN)
├── e_machine (架构:EM_X86_64, EM_AARCH64)
├── e_version (版本)
├── e_entry (程序入口地址)
├── e_phoff (Program Header 表偏移)
├── e_shoff (Section Header 表偏移)
├── e_flags (处理器特定标志)
├── e_ehsize (ELF 头大小)
├── e_phentsize (每个 Program Header 大小)
├── e_phnum (Program Header 数量)
├── e_shentsize (每个 Section Header 大小)
├── e_shnum (Section Header 数量)
└── e_shstrndx (包含段名的字符串表索引)

Program Headers (仅可执行文件/共享库有)
├── p_type (段类型:PT_LOAD, PT_DYNAMIC 等)
├── p_offset (段在文件中的偏移)
├── p_vaddr (段在内存中的虚拟地址)
├── p_paddr (物理地址,通常为 0)
├── p_filesz (段在文件中的大小)
├── p_memsz (段在内存中的大小)
├── p_flags (权限标志:PF_R, PF_W, PF_X)
└── p_align (对齐要求)

Section Headers (描述每个段:.text, .data, .symtab 等)
├── sh_name (段名称偏移)
├── sh_type (段类型:SHT_PROGBITS, SHT_SYMTAB 等)
├── sh_flags (段标志:SHF_ALLOC, SHF_EXECINSTR 等)
├── sh_addr (段在内存中的地址)
├── sh_offset (段在文件中的偏移)
├── sh_size (段大小)
├── sh_link (关联节索引)
├── sh_info (额外信息)
├── sh_addralign (对齐要求)
└── sh_entsize (条目大小)

Sections (实际代码/数据)
├── .text (代码段)
├── .data (已初始化数据段)
├── .bss (未初始化数据段)
├── .rodata (只读数据段)
├── .symtab (符号表)
├── .strtab (符号名称字符串表)
└── .shstrtab (段名称字符串表)

Symbol Table (符号表)
├── st_name (符号名称偏移)
├── st_value (符号值/地址)
├── st_size (符号大小)
├── st_info (符号类型和绑定)
├── st_other (保留字段)
└── st_shndx (符号所在段索引)
  • .o:编译器生成的可重定位编译产物;

  • .a:静态库,本质为 .o 归档;

  • .so:动态库;

  • 可执行文件无扩展名;

Mach-O

即 Mach Object file format,是 Apple 生态系统专属的二进制格式;

Mach-O Header
├── magic (魔数)
│   ├── MH_MAGIC_64 (0xfeedfacf) 小端
│   └── MH_CIGAM_64 (0xcffaedfe) 大端
├── cputype (CPU 类型:CPU_TYPE_X86_64, CPU_TYPE_ARM64)
├── cpusubtype (CPU 子类型:如 ARM64E)
├── filetype (文件类型:MH_OBJECT, MH_EXECUTE, MH_DYLIB)
├── ncmds (Load Commands 数量)
├── sizeofcmds (Load Commands 总大小)
├── flags (标志:如 PIE, NOUNDEFS)
└── reserved (仅 64 位有,保留)

Load Commands (描述段和其他元数据)
├── cmd (命令类型:LC_SEGMENT_64, LC_SYMTAB 等)
├── cmdsize (命令大小)
└── 具体命令数据

Segment Commands (描述每个段:__TEXT, __DATA 等)
├── segname (段名称)
├── vmaddr (虚拟地址)
├── vmsize (虚拟大小)
├── fileoff (文件偏移)
├── filesize (文件大小)
├── maxprot (最大保护级别)
├── initprot (初始保护级别)
├── nsects (节数量)
└── flags (段标志)

Sections (实际代码/数据)
├── sectname (节名称)
├── segname (所属段名称)
├── addr (节地址)
├── size (节大小)
├── offset (节在文件中的偏移)
├── align (对齐要求)
├── reloff (重定位表偏移)
├── nreloc (重定位条目数)
├── flags (节标志)
├── reserved1 (保留字段)
├── reserved2 (保留字段)
└── reserved3 (保留字段)

Symbol Table (符号表)
├── nlist (符号记录)
│   ├── n_strx (符号名称偏移)
│   ├── n_type (符号类型)
│   ├── n_sect (符号所在节索引)
│   ├── n_desc (描述符)
│   └── n_value (符号值)
└── string table (符号名称字符串表)
  • .o:编译器生成的可重定位编译产物;

  • .a:静态库,本质为 .o 归档;

  • .dylib:动态库;

  • 可执行文件无扩展名;

PE/COFF

即 Common Object File Format,原本用于 Unix System V;后 Windows 扩展为 PE(Portable Executable):

PE = COFF + Windows 特定头(DOS Header, NT Headers, Section Table 等)

COFF File Header
├── Machine (架构标识)
├── NumberOfSections (段数量)
├── TimeDateStamp (时间戳)
├── PointerToSymbolTable (符号表偏移)
├── NumberOfSymbols (符号数量)
├── SizeOfOptionalHeader (可选头大小,.obj 中为 0)
└── Characteristics (文件属性)

Optional Header (仅 PE 可执行文件有,.obj 没有)
├── Magic (PE 标识)
├── Linker Version (链接器版本)
├── SizeOfCode (代码段大小)
├── SizeOfInitializedData (初始化数据段大小)
├── SizeOfUninitializedData (未初始化数据段大小)
├── AddressOfEntryPoint (入口点地址)
├── BaseOfCode (代码段基址)
├── ImageBase (镜像基址)
├── SectionAlignment (内存对齐)
├── FileAlignment (文件对齐)
├── MajorOperatingSystemVersion (操作系统主版本号)
├── MinorOperatingSystemVersion (操作系统次版本号)
├── MajorImageVersion (镜像主版本号)
├── MinorImageVersion (镜像次版本号)
├── MajorSubsystemVersion (子系统主版本号)
├── MinorSubsystemVersion (子系统次版本号)
├── Win32VersionValue (保留字段)
├── SizeOfImage (映像大小)
├── SizeOfHeaders (所有头部大小)
├── CheckSum (校验和)
├── Subsystem (子系统类型)
├── DllCharacteristics (DLL 特性)
├── SizeOfStackReserve (栈预留大小)
├── SizeOfStackCommit (栈提交大小)
├── SizeOfHeapReserve (堆预留大小)
├── SizeOfHeapCommit (堆提交大小)
├── LoaderFlags (加载器标志)
└── NumberOfRvaAndSizes (数据目录项数量)

Section Headers (描述每个段:.text, .data, .rdata 等)
├── Name (段名称)
├── Misc (物理大小或虚拟大小)
├── VirtualAddress (虚拟地址)
├── SizeOfRawData (原始数据大小)
├── PointerToRawData (原始数据偏移)
├── PointerToRelocations (重定位表偏移)
├── PointerToLinenumbers (行号表偏移)
├── NumberOfRelocations (重定位条目数)
├── NumberOfLinenumbers (行号条目数)
└── Characteristics (段属性)

Sections (实际代码/数据)
├── .text (代码段)
├── .data (已初始化数据段)
├── .rdata (只读数据段)
└── 其他自定义段

Symbol Table (符号表)
├── Symbol Records (符号记录)
└── String Table (符号名称存储区)
  • .obj:编译器生成的可重定位编译产物;

  • .lib:静态库,本质为 .obj 归档;

  • .dll:动态库;

  • .exe:可执行文件;

调试符号格式

DWARF

即 Debug With Arbitrary Record Formats,调试符号格式行业标准,被 GCC/GDB、Clang/LLDB 广泛支持:

DWARF
├── Compilation Unit Header
│   ├── Length of Compilation Unit Info (total length)
│   ├── DWARF Version (number)
│   ├── Offset into .debug_abbrev section
│   └── Address Size
├── Debugging Information Entries (DIEs)
│   ├── Tag (e.g., DW_TAG_subprogram, DW_TAG_variable)
│   ├── Attribute Name and Value Pairs
│   │   ├── Attribute name (defines what the attribute represents)
│   │   └── Attribute value (the actual value for that attribute)
│   └── Child DIEs (if any)
├── .debug_info
│   └── Contains the debugging information entries.
├── .debug_abbrev
│   └── Contains encoding descriptions for the debugging information entries.
├── .debug_str
│   └── Contains strings referenced from the debugging information entries.
├── .debug_line
│   └── Maps instruction addresses to source file lines.
├── .debug_loc
│   └── Location lists for describing objects that have varying locations.
├── .debug_ranges
│   └── Non-contiguous address ranges for variables or other entities.
├── .debug_frame
│   └── Call frame information for unwinding stack during debugging.
└── Other sections (.debug_macinfo, .debug_pubnames, etc.)
    └── Additional debugging support data.
  • Linix ELF 调试符号默认内嵌到编译产物的 .debug_* 段,发布阶段需使用相关工具手动分离到 .debug 文件;

  • Apple 调试符号发布阶段自动输出到 .dSYM 文件;

CodeView

微软针对 PE/COFF 设计的调试符号格式,早于 DWARF。

CodeView Debug Information
├── Type Records (类型记录)
│   ├── Basic Types (基本类型, 如 int, char)
│   ├── Derived Types (衍生类型, 如 pointers, arrays)
│   └── User-Defined Types (用户自定义类型, 如 structs, classes)
├── Symbol Records (符号记录)
│   ├── Global Symbols (全局符号)
│   ├── Local Symbols (局部符号)
│   └── Function Symbols (函数符号)
└── Line Number Information (行号信息)
    ├── Source File Names (源文件名)
    └── Line Numbers Mapping (行号映射)
  • 编译器将调试信息以 CodeView 格式 写入 .obj 文件的特定段;

  • 链接器读取这些段,并聚合生成独立的 .pdb 文件;

  • 最终的 .exe/.dll 中只保留一个指向 .pdb 的路径和 GUID,不包含实际调试数据。

PDB

PDB(Program Database)是微软专有的二进制数据库格式,最初是为解决 CodeView 嵌入目标文件导致链接慢、符号重复等问题:

  • 存储:

    • 符号表(函数、变量名);

    • 类型信息(结构体、类布局);

    • 源码行号映射(类似 DWARF 的 .debug_line);

    • 局部变量位置(寄存器/栈偏移);

  • 支持高效随机访问(通过 GUID 快速加载符号);

  • 支持事务和回滚;

  • 支持增量链接。

PDB File
├── Stream Directory (流目录)
│   ├── Stream 1: Symbol Records (函数/变量符号)
│   ├── Stream 2: Type Records (结构体/类定义)
│   ├── Stream 3: Line Number Info
│   └── Stream 4: Module Info
├── Pages (固定大小块,类似数据库页)
└── Root Stream (描述其他流的位置)

CMake 在检测到 clang-cl 作为编译器且目标平台为 Windows 时,会自动启用 CodeView 调试信息(即隐式添加 -gcodeview),前提是启用了调试信息(如使用 Debug 构建类型或显式设置 CMAKE_BUILD_TYPE=Debug)。

静态库 & 动态库

我们知道 Android NDK 和鸿蒙 NAPI 对外发布的 C/C++ 库都是动态库(.so),但很多时候我们需要进行平台层二次封装,涉及到二进制中间分发,这种场景可能更适合静态库。

我们首先梳理下三种二进制格式对应的动态库加载流程,充分认识动态库对冷启动新年的影响,再综合对比下静态库/动态库的特性。

动态库加载流程

ELF

Dynamic Library Loading (ELF)
├── dlopen (API 调用)
│   ├── Search Path Resolution (查找路径解析)
│   │   ├── LD_LIBRARY_PATH Environment Variable
│   │   ├── /etc/ld.so.conf.d/
│   │   ├── Default Paths (e.g., /lib, /usr/lib)
│   │   └── RPATH/RUNPATH in ELF Header
│   ├── Open File (打开动态库文件)
│   ├── Read ELF Headers (读取 ELF 头)
│   │   ├── Program Headers (程序头)
│   │   ├── Section Headers (段头)
│   ├── Map Segments to Memory (将段映射到内存)
│   │   ├── PT_LOAD Segments (加载段)
│   │   ├── PT_DYNAMIC Segment (动态链接信息)
│   ├── Process Dynamic Section (处理动态段)
│   │   ├── Resolve Dependencies (解析依赖库)
│   │   │   ├── Load Dependent Libraries (加载依赖库)
│   │   │   ├── Relocate Symbols (重定位符号)
│   │   │   └── Bind Symbols (绑定符号)
│   ├── Initialize Global Variables (初始化全局变量)
│   ├── Call _init Function (调用 _init 函数)
│   │   ├── Constructor Functions (构造函数)
│   │   └── Initialization Code (初始化代码)
└── Return Handle (返回句柄)

ELF 动态库加载过程中,通过 ld.so 提供基本的安全性保障。

Mach-O

dyld2
Dynamic Library Loading (Mach-O dyld2)
├── dlopen (API 调用)
│   ├── Search Path Resolution (查找路径解析)
│   │   ├── DYLD_LIBRARY_PATH Environment Variable
│   │   ├── @rpath in Mach-O Header
│   │   ├── /usr/lib, /System/Library/Frameworks
│   │   └── Framework Search Paths
│   ├── Open File (打开动态库文件)
│   ├── Read Mach-O Headers (读取 Mach-O 头)
│   │   ├── Load Commands (加载命令)
│   │   ├── LC_SEGMENT_64 Segments (段)
│   ├── Map Segments to Memory (将段映射到内存)
│   │   ├── __TEXT, __DATA, __LINKEDIT 段
│   ├── Process LC_LOAD_DYLIB Commands (处理 LC_LOAD_DYLIB 命令)
│   │   ├── Resolve Dependencies (解析依赖库)
│   │   │   ├── Load Dependent Libraries (加载依赖库)
│   │   │   ├── Bind Symbols (绑定符号)
│   │   │   └── Rebase Pointers (重定向指针)
│   ├── Apply Dyld Bindings (应用 dyld 绑定)
│   │   ├── Lazy Binding (延迟绑定)
│   │   ├── Weak Binding (弱绑定)
│   │   └── Flat Namespace Binding (扁平命名空间绑定)
│   ├── Call Initializers (调用初始化器)
│   │   ├── +load Methods (Objective-C +load 方法)
│   │   ├── C++ Constructors (C++ 构造函数)
│   │   └── Other Initialization Code (其他初始化代码)
└── Return Handle (返回句柄)
dyld3

dyld3 主要是安全性和加载性能方面的优化。

ynamic Library Loading (Mach-O dyld3)
├── dlopen (API 调用)
│   ├── Pre-Flight Checks (预检)
│   │   ├── Validate Code Signature (验证代码签名)
│   │   ├── Check Entitlements (检查权限)
│   │   └── Verify Library Integrity (验证库完整性)
│   ├── Search Path Resolution (查找路径解析)
│   │   ├── Same as dyld2 (与 dyld2 相同)
│   ├── Open File (打开动态库文件)
│   ├── Read Mach-O Headers (读取 Mach-O 头)
│   │   ├── Load Commands (加载命令)
│   │   ├── LC_SEGMENT_64 Segments (段)
│   ├── Map Segments to Memory (将段映射到内存)
│   │   ├── __TEXT, __DATA, __LINKEDIT 段
│   ├── Process LC_LOAD_DYLIB Commands (处理 LC_LOAD_DYLIB 命令)
│   │   ├── Resolve Dependencies (解析依赖库)
│   │   │   ├── Load Dependent Libraries (加载依赖库)
│   │   │   ├── Bind Symbols (绑定符号)
│   │   │   └── Rebase Pointers (重定向指针)
│   ├── Apply Dyld Bindings (应用 dyld 绑定)
│   │   ├── Optimized Bindings (优化绑定)
│   │   ├── Prebinding (预绑定)
│   │   └── Fast Path for Common Libraries (常见库的快速路径)
│   ├── Call Initializers (调用初始化器)
│   │   ├── +load Methods (Objective-C +load 方法)
│   │   ├── C++ Constructors (C++ 构造函数)
│   │   └── Other Initialization Code (其他初始化代码)
│   ├── Post-Initialization (后初始化)
│   │   ├── Runtime Checks (运行时检查)
│   │   ├── Security Enhancements (安全增强)
│   │   └── Performance Monitoring (性能监控)
└── Return Handle (返回句柄)

PE/COFF

Dynamic Library Loading (Windows)
├── LoadLibrary (API 调用)
│   ├── Search Path Resolution (查找路径解析)
│   │   ├── Current Directory
│   │   ├── Application Directory
│   │   ├── System Directory (e.g., C:\Windows\System32)
│   │   ├── PATH Environment Variable
│   │   └── Known DLLs (预加载的系统 DLL)
│   ├── Open File (打开动态库文件)
│   ├── Map File to Memory (将文件映射到内存)
│   │   ├── Headers (PE Header)
│   │   ├── Sections (段:.text, .data 等)
│   ├── Process Import Table (处理导入表)
│   │   ├── Resolve Imports (解析导入符号)
│   │   │   ├── Load Dependent Libraries (加载依赖库)
│   │   │   ├── Bind Symbols (绑定符号)
│   │   │   └── Fixup Addresses (修正地址)
│   ├── Initialize Data Sections (初始化数据段)
│   ├── Call DllMain (调用 DllMain 函数)
│   │   ├── DLL_PROCESS_ATTACH (进程附加)
│   │   ├── DLL_THREAD_ATTACH (线程附加)
│   │   └── DLL_THREAD_DETACH (线程分离)
└── Return Handle (返回句柄)

PE/COFF 动态库加载过程中的安全性校验较少。

可以看到,三种二进制动态库加载流程虽有细节差异,但都包含三个核心步骤:动态查询依赖、内存映射、处理符号表,涉及到密集的 CPU 和 IO 操作,严重影响冷启动速度。

静态库/动态库对比

动态库

优势
  • linker 负责加载主库的间接依赖,外部不用感知;
劣势
  • 影响冷启动速度:动态加载、符号解析、依赖分析、mmap、符号重定位等操作涉及密集的 CPU 和 IO 操作;(iOS 对动态库还有额外要求,如签名校验等)

  • 影响包体积:动态库是已完成链接的独立编译单元,为保证二进制兼容,不便做跨模块死代码消除,最终产物可能包含冗余符号;

  • 可能出现运行异常:加载顺序问题、符号缺失、符号二进制兼容问题等;

  • 暴露依赖细节:所有依赖的动态库都会直接出现在最终编译产物中。

静态库

优势
  • 本质是对 .o 文件的归档,包含所有编译符号,便于跨模块死代码消除,最终产物体积较小;

  • 对于 Android/鸿蒙等二次封装最终输出 so 的,静态库会被直接打进 jni.so,不会直接暴露;

  • 包含所有原始符号,有符号冲突会提前暴露,不会运行时异常;

劣势
  • 本身包容较大,不便于开发过程中的二进制分发;

  • 需要手动传递间接依赖。

动态库以运行时灵活性换取启动性能与部署复杂度,静态库以构建时复杂度换取运行时效率与封装性。
所以,内部分发二进制尽量采用静态库,既方便最终产物死代码消除从而减小包容,更重要的是避免最终交付产物包含过多动态库拖慢冷启动速度。

工具链

前面讲 DWARF 时提到,有时需要手动提取或剔除调式符号;

优化包容时,可能需要查看二进制内是否包含了未使用的符号;

解决符号链接问题导致的编译或运行时异常时,需要查看指定二进制是否包含了目标符号;

这些都需要用到一系列二进制相关工具链。

字符串

strings

用于从二进制提取可打字符串,LLVM 和 GNU 工具链都支持。

符号提取

strip

用于从二进制剔除某类符号,Apple 和 GNU 都支持,但参数不同:

  • 移除所有符号:GNU 是 --strip-all,Apple 是 -s

  • 移除 debug 符号:GNU 是 --strip-debug,Apple 是 -d

  • 移除非全局符号:GNU 是 --strip-unneeded,Apple 是 -x

建议:

  • 静态库需格外谨慎使用 strip,否则影响链接;

  • 调试阶段应保留 debug 符号;

  • 对外发布需保留全局符号。

objcopy

属于 LLVM 工具链,不仅支持符号剔除,还能指定保留某些符号、绑定 debug 符号:

# 导出 debug 符号
objcopy --only-keep-debug program  x.debug

# strip 主程序
strip --strip-all x

# 让 strip 后的二进制指向调试文件(用于 gdb)
objcopy --add-gnu-debuglink=x.debug x

导出符号表

nm

用于查看二进制符号表,LLVM 和 GNU 工具链都支持;

-C 参数表示还原被 mangle 的 C++ 符号:

nm -C x.so

输出的符号前的字母表示符号类型,大写表示全局符号,小写表示静态符号:

  • A

    • 绝对符号,之后经过任何链接都不会改变;
  • B/b

    • 零初始化或未初始化;

    • 位于 BSS 数据段;

    • 强符号,冲突时报错;

  • C/c

    • 来源于 C 语言未初始化的符号;

    • 编译时不分配地址(暂存于 COMMON),同名会合并;

  • D/d

    • 已初始化符号;
  • G/g

    • 已初始化的小对象;

    • 强符号;

  • I

    • 该符号是另一个符号的间接引用;
  • i

    • 对于 PE,表示该函数实现位于 DLL;

    • 对于 ELF,表示运行时重定向的函数;

  • N

    • 调试符号;
  • n

    • 该符号位于非数据、非代码、非调试的只读段;
  • p

    • 该符号位于 stack unwind 段;
  • R/r

    • 表示符号位于只读数据段;
  • S/s

    • 零初始化或未初始化的小对象;
  • T/t

    • 普通函数符号;

    • 位于文本(代码)段;

  • U

    • 未定义符号(在当前文件中被引用,但定义在其他地方,需链接时解析);
  • u

    • ELF 特有;

    • 表示全局唯一符号;

  • V/v

    • 弱对象符号,通常用 __attribute__((weak)) 定义;

    • 链接时遇到同名强符号(如 T/D),会被忽略;

  • W/w

    • 弱函数符号;

    • 链接时遇到同名强符号,会被忽略;

二进制重组

内部分发静态库时,如果涉及到第三方库,通常涉及多个静态库,为了便于平台封装层接入,可考虑先将多个静态库还原为 .o 集合,然后归档为一个静态库。

当然前提是:

  • 只有静态库(本身就只是 .o 的归档),动态库重组会影响链接;

  • 必须确保多个静态库编译环境一致。

工具

各平台都有自己的相关工具(Linux/macOS 的 ar,Windows 的 lib 等),这里仅介绍 LLVM 工具链。

llvm-ar: 模拟 ar,支持 ELF 和 Mach-O,不支持 COFF;

llvm-lib:模拟 Windows 的 lib.exe,仅支持 COFF。

静态库解档

llvm-ar t libfoo.a #仅列出文件名,不解压
llvm-ar x libfoo.a #解压

# Windows 只能逐个提取
llvm-lib /LIST mylib.lib > members.txt
foreach ($obj in Get-Content members.txt) {
    llvm-lib /EXTRACT:$obj mylib.lib
}

归档为静态库

llvm-ar rcs sdk-linux.a linux/*.o
llvm-ar rcs sdk-mac.a mac/*.o

llvm-lib /OUT:sdk-win.lib win/*.obj

直接合并静态库

先解档再归档不仅麻烦,而且可能出现冲突,可直接合并,更安全。

llvm-ar rcs combined.a lib1.a lib2.a

llvm-lib /OUT:combined.lib lib1.lib lib2.lib