程序的链接、装载与库:从源码到可执行文件的底层奥秘
程序的链接、装载与库从源码到可执行文件的底层奥秘简介一个标准的 C/C 程序员如果只会写业务代码、通过编译器一键编译生成可执行文件那远远不够。理解程序从源码到运行的完整链路——预编译、编译、汇编、链接以及 ELF 文件格式、动态库版本管理、符号解析等底层知识是写出高质量 C/C 程序的基础也是排查运行时疑难问题的关键能力。本文基于《程序员的自我修养——链接、装载与库》的学习笔记结合实际操作和工具使用系统性地梳理这些底层知识。一、从源码到可执行文件的完整过程以经典的 Hello World 程序为例#includestdio.hintmain(){printf(Hello World\n);return0;}通常我们一条命令即可完成编译和运行$ gcc hello.c-ohello $ ./hello Hello World但在这个简单的命令背后实际上经历了四个阶段预编译 → 编译 → 汇编 → 链接。每一个阶段都有其特定的作用和底层实现。hello.c → [预编译] → hello.i → [编译] → hello.s → [汇编] → hello.o → [链接] → hello[图片占位符编译四步骤流程图]二、预编译Preprocessing2.1 作用预编译器处理所有以#开头的预处理器指令主要完成以下工作展开宏定义将#define定义的宏替换为实际值处理条件编译根据#if、#ifdef、#ifndef等条件决定包含哪些代码展开头文件包含将#include引用的头文件内容插入到当前位置删除注释移除所有//和/* */注释添加行号信息用于编译错误定位2.2 命令$ gcc-Ehello.c-ohello.i打开hello.i文件可以看到预处理后的代码已经没有宏定义、条件编译和注释了只剩下纯粹的 C 代码。对于一个简单的 Hello World展开stdio.h后文件可能有上千行。2.3 底层实现虽然通过gcc -E命令来实现预编译但底层实际调用的是cppC Preprocessor程序$ cpp hello.c-ohello.i两种方式完全等效。三、编译Compilation3.1 作用编译器将预编译后的 C/C 代码翻译为汇编代码。每条汇编指令对应一条机器指令此时已经是相当低级的语言了。3.2 命令$ gcc-Shello.i-ohello.s查看hello.s可以看到汇编代码.file hello.c .section .rodata .LC0: .string Hello World .text .globl main .type main, function main: .LFB0: .cfi_startproc leal 4(%esp), %ecx .cfi_def_cfa 1, 0 andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp ...3.3 底层实现底层通过cc1编译器实现。cc1通常不在环境变量 PATH 中需要使用完整路径$ /usr/libexec/gcc/i686-redhat-linux/5.3.1/cc1 hello.c-ohello.s3.4 编译器优化的层次编译过程包含了词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等多个步骤。现代编译器如 GCC、Clang会进行大量优化例如常量折叠、循环展开、内联函数等。四、汇编Assembly4.1 作用汇编器将汇编代码翻译为目标文件Object File即把每条汇编指令转换为对应的机器码二进制的 0 和 1。目标文件虽然是二进制格式但还不能直接运行。4.2 命令$ gcc-chello.c-ohello.o4.3 查看目标文件的机器码使用objdump反汇编查看生成的机器码$ objdump-dhello.o hello.o: 文件格式 elf32-i386 Disassembly of section .text: 00000000main:0: 8d 4c2404 lea 0x4(%esp),%ecx4:83e4 f0 and$0xfffffff0,%esp7: ff71fc pushl -0x4(%ecx)a:55push %ebp b:89e5 mov %esp,%ebp d:51push %ecx e:83ec 04 sub$0x4,%esp11:83ec 0c sub$0xc,%esp14:6800 00 00 00 push$0x019: e8 fc ff ff ff call 1amain0x1a1e:83c410add$0x10,%esp21: b8 00 00 00 00 mov$0x0,%eax...可以看到汇编指令已经被转换为十六进制机器码。例如lea 0x4(%esp),%ecx对应的机器码是8d 4c 24 04占用 4 个字节。4.4 底层实现底层通过as汇编器实现$ as hello.s-ohello.o五、链接Linking5.1 为什么需要链接也许你会问编译成机器码之后不就可以直接运行了吗为什么还需要链接原因在于多文件项目实际工程中通常有多个源文件每个文件编译后生成独立的目标文件需要将它们合并为一个可执行文件。外部引用即使单个文件也会使用外部函数如printf需要在链接阶段找到这些函数的定义并关联起来。链接的核心任务就是符号解析Symbol Resolution和地址重定位Relocation。5.2 命令$ gcc hello.o-ohello注意gcc没有专门的仅链接选项通过-o指定输出文件名即可。5.3 底层实现底层通过ld链接器实现完整命令如下$ ld hello.o-ohello\/usr/lib/crt1.o\/usr/lib/crti.o\/usr/lib/crtn.o\-lc\-dynamic-linker /lib/ld-linux.so.2各部分说明crt1.o / crti.o / crtn.oC 运行时启动代码C Runtime-lc链接标准 C 库libc.so-dynamic-linker指定动态链接器路径5.4 程序并不是从 main 开始的启动代码crt的作用初始化栈指针初始化环境变量、全局变量、静态变量调用main函数退出程序所以程序实际上并不是从main函数开始执行的而是从启动代码开始。以下代码可以验证这一点#includecstdioclassA{public:A(){printf(A Constructor\n);}~A(){}};A a;// 全局变量的初始化在 main 之前执行intmain(){printf(Hello World\n);return0;}输出结果A Constructor Hello World全局变量a的构造函数在main函数之前就被调用了这正是启动代码的功劳。六、ELF 文件格式ELFExecutable and Linkable Format是 Linux 系统下的标准可执行文件格式。目标文件.o、共享库.so和可执行文件都采用 ELF 格式。6.1 ELF 文件类型类型说明ET_REL可重定位文件.o 目标文件ET_EXEC可执行文件ET_DYN共享目标文件.so 动态库ET_CORE核心转储文件core dump6.2 ELF 文件结构------------------ | ELF Header | 文件头魔数、架构、入口地址等 ------------------ | .text section | 代码段编译后的机器指令 ------------------ | .data section | 数据段已初始化的全局/静态变量 ------------------ | .bss section | BSS段未初始化的全局/静态变量 ------------------ | .rodata section | 只读数据段常量、字符串字面量 ------------------ | .symtab section | 符号表函数和变量的地址映射 ------------------ | .strtab section | 字符串表符号名称字符串 ------------------ | Section Headers | 段表各段的偏移、大小等元信息 ------------------6.3 查看工具# 查看 ELF 文件头信息readelf-hhello# 查看所有段Section信息readelf-Shello.o# 查看符号表readelf-shello.o# 查看依赖的动态库readelf-dhello# 或者使用 nm 命令查看符号nm hello.o# 查看动态符号表nm-Dhello七、静态链接与动态链接7.1 静态链接静态链接在编译时将所有依赖的库代码直接拷贝到可执行文件中。优点可执行文件独立运行不依赖外部库缺点文件体积大库更新需要重新编译$ gcc-statichello.c-ohello_static7.2 动态链接动态链接在运行时才加载共享库多个程序可以共享同一个库。优点文件体积小库更新无需重新编译缺点运行时依赖库文件版本兼容性需要注意# 查看动态库依赖$ ldd hello linux-vdso.so.1(0x00007ffc239e6000)libc.so.6/lib64/libc.so.6(0x00007f8a1c200000)/lib64/ld-linux-x86-64.so.2(0x00007f8a1c5d0000)八、动态库版本管理8.1 三种库名称Linux 动态库有三种命名方式名称格式说明realnamelibfoo.so.x.y.z实际的库文件包含完整版本号sonamelibfoo.so.x共享库名称只包含主版本号linker-namelibfoo.so链接时使用的名称指向 soname# 编译时指定 soname$ gcc-shared-fPIC-Wl,-soname,libfoo.so.1-olibfoo.so.1.2.3 foo.c8.2 ldconfig 管理符号链接# 更新 /usr/lib 和 /lib 下的动态库符号链接sudoldconfig# 为指定目录生成符号链接sudoldconfig-n/path/to/shared/library/directoryldconfig会扫描指定目录中的动态库根据 soname 自动创建或更新符号链接。九、符号版本与 GLIBC 兼容性9.1 符号版本机制符号版本是 soname 机制的扩展通过给函数增加版本号来实现同一函数的多个版本共存$ nm /usr/lib/libc.so.6|grepGLIBC|grepfgetpos00142580 T fgetpos64GLIBC_2.1 00069830 T fgetpos64GLIBC_2.2 00142430 T fgetposGLIBC_2.0 00066ce0 T fgetposGLIBC_2.2注意和的区别表示非默认版本表示默认版本9.2 GLIBC 兼容性问题这就是为什么在高版本 GLIBC 环境编译的程序在低版本上运行会报错./my_program: /lib64/libc.so.6: version GLIBC_2.22 not found(required by ./my_program)GLIBC 保证向后兼容但不保证向前兼容。使用 GLIBC 2.22 编译的程序可以在 GLIBC 2.22 的环境下运行但不能在 GLIBC 2.22 的环境下运行。GLIBCXXC 标准库的符号版本也遵循同样的规则。9.3 查看符号版本依赖# 查看程序需要的 GLIBC 版本$ objdump-Tmy_program|grepGLIBC# 查看系统支持的 GLIBC 版本$ strings /lib64/libc.so.6|grepGLIBC十、动态库搜索路径优先级动态链接器按照以下优先级搜索共享库优先级从高到低 1. LD_PRELOAD 指定的文件最高优先级 2. LD_LIBRARY_PATH 指定的目录 3. /etc/ld.so.cache 缓存中记录的路径 4. 默认路径先 /usr/lib然后 /lib10.1 LD_LIBRARY_PATH# 临时添加库搜索路径exportLD_LIBRARY_PATH/my/custom/lib:$LD_LIBRARY_PATH# 运行程序时会优先在指定路径搜索动态库./my_program10.2 LD_PRELOADLD_PRELOAD比LD_LIBRARY_PATH还要优先。它可以指定预先装载的共享库或目标文件在动态链接器按固定规则搜索共享库之前装载。# 预加载自定义库常用于函数拦截/mockexportLD_PRELOAD/my/lib/my_malloc.so ./my_program实际应用场景替换malloc/free为自己的实现来检测内存泄漏在测试中 mock 系统调用修复有 bug 的库函数而无需重新编译10.3 LD_DEBUG 调试LD_DEBUG是动态链接器的调试利器可以打印出完整的装载过程# 查看所有调试信息LD_DEBUGall ./my_program# 常用的调试选项LD_DEBUGbindings ./my_program# 显示符号绑定过程LD_DEBUGlibs ./my_program# 显示共享库查找过程LD_DEBUGversions ./my_program# 显示符号版本依赖LD_DEBUGreloc ./my_program# 显示重定位过程LD_DEBUGsymbols ./my_program# 显示符号表查找过程LD_DEBUGstatistics ./my_program# 显示统计信息LD_DEBUGfiles ./my_program# 显示共享库文件名LD_DEBUG甚至可以显示每个依赖符号所在的动态库位置对于排查动态链接问题非常有用。十一、strip 删除符号信息正常编译出来的共享库或可执行文件包含符号信息和调试信息虽然调试时有用但对发布版本来说这些信息只会增加文件体积。strip工具可以清除这些信息# 直接 strip 可执行文件或共享库strip libfoo.so strip my_program# 也可以在编译时通过链接器选项实现gcc -Wl,-s-omy_program main.o# 消除所有符号信息gcc -Wl,-S-omy_program main.o# 消除调试符号信息注意strip 后的文件仍然可以正常运行但无法使用 GDB 进行源码级调试。建议在发布时保留一份未 strip 的版本用于调试。十二、共享库构造函数与析构函数12.1__attribute__((constructor))/__attribute__((destructor))GCC 提供了一种机制可以让共享库中的函数在库被加载和卸载时自动执行#includestdio.h// 构造函数在 main 函数之前执行// 加载 so 后立刻执行在 dlopen 返回前执行__attribute__((constructor))voidinit_function(){printf(Library loaded: init_function\n);}// 析构函数在 main 函数结束后执行// 在 dlclose 返回前执行__attribute__((destructor))voidfini_function(){printf(Library unloaded: fini_function\n);}12.2 优先级控制可以为构造/析构函数指定优先级数值越小越先执行// 优先级 1011-100 为系统保留__attribute__((constructor(101)))voidearly_init(){printf(Early initialization\n);}__attribute__((destructor(101)))voidlate_fini(){printf(Late finalization\n);}12.3 应用场景插件系统的自动注册全局资源的初始化与清理性能监控库的自动启动与停止日志系统的自动配置十三、共享库脚本共享库不一定是真正的二进制文件还可以是符合一定格式的链接脚本文件。通过这种脚本可以把几个现有的共享库组合起来从用户的角度看就是一个新的共享库。13.1 示例组合多个库创建libfoo.so文件内容如下GROUP( /lib/libc.so.6 /lib/libm.so.2 )这样libfoo.so就同时包含了 C 运行库和数学库链接时使用-lfoo即可同时链接两个库。13.2 系统中的实际例子查看 Linux 系统中的/usr/lib/libc.so/* GNU ld script Use the shared library, but some functions are only in the static library, so try that secondarily. */ OUTPUT_FORMAT(elf32-i386) GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux.so.2 ) )这个脚本将动态库libc.so.6、静态库libc_nonshared.a和动态链接器ld-linux.so.2组合在一起形成一个完整的 C 运行时库。十四、strace 系统调用跟踪strace是一个强大的诊断工具用于跟踪程序执行时的系统调用# 安装dnfinstallstrace# Fedora/RHELaptinstallstrace# Debian/Ubuntu# 跟踪程序的所有系统调用stracels# 只跟踪特定系统调用strace-eopen,read,writels# 跟踪已运行的进程strace-pPID# 统计系统调用strace-cls输出示例execve(/bin/ls, [ls], 0x7fff...) 0 brk(NULL) 0x55a000 open(/etc/ld.so.cache, O_RDONLY|O_CLOEXEC) 3 fstat(3, {st_modeS_IFREG|0644, ...}) 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE, 3, 0) 0x7f... close(3) 0 ...通过strace可以清晰地看到程序执行了哪些系统调用、参数是什么、返回值是什么对于排查文件权限、网络连接、内存分配等问题非常有效。十五、常用二进制分析工具速查表场景推荐工具说明基本二进制分析file识别文件类型readelf查看 ELF 文件信息objdump反汇编、查看段信息nm查看符号表ldd查看动态库依赖size查看各段大小动态追踪strace跟踪系统调用ltrace跟踪库函数调用perf性能分析bpftraceeBPF 高级追踪调试gdbGNU 调试器rr记录与回放调试valgrind内存错误检测cfiltC 符号名还原逆向工程radare2开源逆向框架GhidraNSA 开源逆向工具IDA Pro业界标准逆向工具性能分析perfLinux 性能分析ftrace内核函数追踪SystemTap系统级性能分析安全检查checksec检查二进制安全特性upx可执行文件压缩strip删除符号信息常用命令速查# 识别文件类型filehello# hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), ...# 查看符号表nm hello.o# 0000000000000000 T main# U printf# 查看动态符号表nm-Dlibfoo.so# 查看 ELF 文件头readelf-hhello# 查看段信息readelf-Shello.o# 查看依赖库ldd hello# 反汇编objdump-dhello.o# C 符号还原cfilt _ZNSt8ios_base4InitC1Ev总结本文系统性地梳理了程序从源码到运行的完整底层知识编译四步骤预编译cpp处理宏和头文件编译cc1生成汇编代码汇编as生成目标文件链接ld合并目标文件生成可执行文件。ELF 文件格式理解 ELF 的段结构.text、.data、.bss、.rodata、.symtab 等是二进制分析的基础。动态库版本管理soname/realname/linker-name 三级命名结合 ldconfig 管理符号链接。符号版本与兼容性GLIBC 通过符号版本实现多版本共存保证向后兼容但不保证向前兼容。动态库搜索与调试掌握 LD_LIBRARY_PATH、LD_PRELOAD、LD_DEBUG 三大利器。高级技巧共享库构造/析构函数、共享库脚本、strip 优化、strace 系统调用跟踪。这些知识不仅是理解程序运行原理的关键也是排查编译错误、运行时崩溃、性能瓶颈等实际问题的必备工具。建议结合实际项目动手实践每一个命令和工具逐步建立对底层系统的深刻理解。原始笔记来源E:/Work/Notes/zyh/程序的链接、装载、库.md– 基于《程序员的自我修养》的学习笔记预编译/编译/汇编/链接四步骤详解、ELF文件结构、nm/readelf/objdump/ldd工具、动态库版本管理/soname/符号版本、GLIBC兼容性、动态库搜索路径/LD_LIBRARY_PATH/LD_PRELOAD、LD_DEBUG调试、strip、ldconfig、共享库构造函数__attribute__((constructor))、共享库脚本、strace
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2583687.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!