【Linux系统】PIC位置无关码

前言

我们在制作动态库的时候:

gcc -fPIC -o math.o math.c

通常需要以选项 -fPIC来说明我们编译的源文件是用来制作动态库的,可是为什么需要这个PIC(Position - Independent Code)选项。

下面小编会一步一步带大家了解这个 PIC 的作用。

从中我们可以体会两点

  • 体会什么是虚拟地址/虚拟地址来自哪里?
  • 什么是PIC

1. 汇编文件中的地址

在了解.o文件之前,我们先来了解一个可执行程序中形成的地址

1.1 可执行程序中的虚拟地址

可执行程序就是众多.o文件,通过一个连接器(ld)形成的可执行程序。

  • 了解:平坦模式 VS 分段模式。

现代的C/C++可执行程序都是以平坦模式(Flat Memory Model)进行编译的。也就是意味着:程序都以一个平坦模型来看待内存,即单一的、连续的、线性的地址。

  • 我们可以知道:实际一个可执行程序是已经有了地址的概念了!!

如何证明呢?下面我们写一个简单C语言的程序

/***  main.c******/
#include <stdio.h>

int g_val = 10;

int add(int x, int y)
{
    return x + y;
}

int main()
{
    int a = 0;
    printf("%d\n", a);
    return 0;
}

我们使用gcc编译器将它形成一个可执行程序

gcc -o main.exe main.c

然后我们使用工具objdump来查看该可执行程序的反汇编
(文件较大,我们重定向到一个文本文件)

objdump -d main.exe > disassembly.txt
  • 来看文件:disassembly.txt

    在这里插入图片描述
    被标记的位置就是代码执行的地址!!
    可是我们的代码并没有被加载到内存之中,为什么会有地址的概念了呢?

    说明:在程序加载到物理内存之前,就有了虚拟地址的概念

1.2 汇编形成的.o文件

上面谈到,一个可执行程序有自己对应的虚拟地址。那么我们往前看,在形成可执行程序之前的汇编结束形成的.o文件(目标文件)

在其内部也有对应的虚拟地址

gcc -c main.o main.c;objdump -d main.o > tmp.txt
  • 来看结果:

    在这里插入图片描述

  • 现象

    add函数的起始地址是00,main函数的起始地址是18。显然是事实上不可能的。

    这是可重定位目标文件(.o 文件)中代码段(.text 段)内的相对偏移地址。这是为了告诉连接器:“我希望我能得到一个合适的地址,你连接器在合适的时候给我一个合适的地址(这个地址就是虚拟地址)”

    来看形成可执行之后的该程序:
    在这里插入图片描述
    明显:addmain的地址发生了变化!!

  • 结论

    1. 汇编形成的.o文件中含有地址。.o文件中的地址是可重定位的,即需要连接器为其分配真正需要的虚拟地址。
    2. 所以我们的.o文件要求:连接器在连接.o文件的时候需要对我内部的代码的地址(函数的地址……)进行分配一个合理的地址!!!

2. PIC

一个动态库可能是多个.o文件链接而成

gcc -shared libxx.so a.o b.o

这段命令:就是将a.o文件和b.o文件链接形成一个动态库libxx.so

既然是链接过程,那么必然地,我们的动态库连接器一定需要为两个目标文件的代码分配对应虚拟地址。因为我们.o文件是要求:链接器在链接的时候需要给我的代码一个合适的地址。

  • 一个事实:动态库的链接器是无法完成这个工作的。因为他不知道这个动态库未来会被加载到进程地址空间的哪一个位置,他无法提供一个有效的绝对地址(而第二次程序与库进行链接的静态链接器拥有绝对的上帝视角,可以对地址进行分配)

所以,如果我们就采用普通的方式(不带选项-fPIC)来编译汇编我们的源文件,这样形成的.o文件,对于我们的动态库链接器来说:是无法满足.o文件中地址确定的需求的

  • 为什么静态链接和动态链接会有这样的差异?

    主要原因是因为:OS的综合考虑(资源、效率、目的……)!动态库链接形成.so文件和最后.o文件链接形成可执行程序的时候链接器的身份和地位不同

    • 动态库链接器和静态链接器设计的理念不同,我们从如下几个方面来说明:
特性静态链接动态链接
主要目的创建一个独立、高效、可移植的可执行程序共享代码、节省内存
发生时机编译时运行时
可用信息全部信息:所有源代码、所有库代码局部信息:只知道当前要加载的库
关键约束结果是一个完全封闭、完整的整体必须和未来未知组件整合

然而有了编译选项fPIC就不一样了:

  • PIC选项就是在编译的时候告诉编译器:“你要用GOT来解决地址重定位问题哦”。编译器就通过全局偏移表(GOT) 将绝大多数对绝对地址的依赖从代码段转移到了数据段。

    • 现在,访问一个全局变量 global_var 的指令不再是直接访问某个内存地址,而是变成了:
      1. 计算GOT表本身的起始地址(如何找到GOT表项,细节就不说了)
      2. 找到变量global_val在表中的表项,取出该变量的真实地址,存入寄存器
      3. 最后通过寄存器中的的真实地址对数值进行访问
  • 在创建动态库时(即使用 gcc -shared),链接器可以看到 global_var@GOTPCREL 也是一个需要重定位的项。但这次,这个重定位项是针对GOT这个数据结构的某个表项,而不是针对代码段中的指令。

    链接器可以安心地完成这个重定位:它在GOT表中为 global_var 预留一个位置,并生成一个重定位项(可以简单理解为偏移量),记录:“运行时加载器,当你把这个库加载到某个基址X后,请计算一下 global_var 的实际地址(X + 它的偏移量),然后把这个地址值填到GOT表的这个位置里。”

所以,-fPIC对于.o文件中重定位的要求进行了其它处理:

  1. 将大量对代码段的重定位,转变为了对数据段(GOT表)的重定位。
  2. 代码段本身因为使用相对寻址,变得非常“干净”,几乎不需要修改。
  3. 数据段(GOT)是可写的,在运行时被修改是预期之内的行为,不影响代码段的只读共享特性。

简单来说:以前需要的代码所需要的一个绝对定址,现在只需要一个偏移量

简单总结一下:

  • 因为动态库是共享的等等设计因素,决定了动态链接器特性就无法像静态链接器那样拥有地址分配的能力。其次由于.o文件要求对其中代码段的地址进行重定位,而动态库链接器无法完成这样的工作所以就需要PIC。通过GOT对重定位进行转化,间接实现重定位。

那么最后加载器在加载程序的时候,我们的动态库就可以被加载到进程地址空间的任意位置,我们加载器填充GOT表项信息的时候,只需要将动态库加载的起始地址 + 偏移量(偏移量我们也可以直接存储在GOT表项中,之后加上起始地址即可。但是具体实现肯定有所差异)就可以得到代码的虚拟地址了。最后CPU执行指令的时候就执行这些虚拟地址,就会通过页表转化找到物理地址下的动态库。(至于如何找到GOT表的细节我们就不再说了)

<think>我们正在讨论Linux动态链接库(SharedObject,.so)与Windows动态链接库(DLL)的区别。根据引用内容,我们知道两者都是动态链接库,但实现机制和细节上存在差异。以下从多个维度进行对比:###1.**文件格式与结构**|**特性**|WindowsDLL|LinuxSO(.so)||----------------|----------------------------------------------|-------------------------------------------||**文件格式**|PE(PortableExecutable)格式|ELF(ExecutableandLinkableFormat)格式||**入口函数**|`DllMain`(可选)|无强制入口,可使用`_init`和`_fini`(旧版)||**符号导出**|需显式声明`__declspec(dllexport)`|默认导出所有全局符号,可用`__attribute__((visibility("hidden")))`隐藏|>**本质差异**:PE与ELF格式的差异导致加载机制不同[^1]。###2.**加载与链接机制**|**过程**|WindowsDLL|LinuxSO||----------------|----------------------------------------------|-------------------------------------------||**加载方式**|`LoadLibrary()`+`GetProcAddress()`|`dlopen()`+`dlsym()`||**运行时链接**|显式链接需手动管理句柄|显式链接需手动管理句柄||**隐式链接**|通过导入库(.lib)在编译时声明依赖|通过编译选项`-l<name>`声明依赖||**路径搜索**|按特定顺序搜索系统目录、当前目录、PATH等[^2]|按`LD_LIBRARY_PATH`、/etc/ld.so.conf等配置|>**关键区别**:Linux的符号版本控制机制更灵活,允许同一库文件包含多个版本的符号。###3.**符号处理与版本管理**|**特性**|WindowsDLL|LinuxSO||------------------|----------------------------------------------|-------------------------------------------||**符号冲突**|全局命名空间易引发冲突(DLLHell)|支持符号版本控制(SymbolVersioning)||**版本管理**|依赖文件命名(如`libv1.dll`,`libv2.dll`)|可在单文件中嵌入多版本符号(如`GLIBC_2.4`)||**延迟绑定**|支持(通过`/DELAYLOAD`选项)|默认使用PLT(过程链接表)实现延迟绑定|>**Linux优势**:符号版本控制有效解决兼容性问题,避免“DLLHell”[^1]。###4.**性能优化差异**|**优化技术**|WindowsDLL|LinuxSO||------------------|----------------------------------------------|-------------------------------------------||**地址无关代**|部分支持(/DYNAMICBASEASLR)|默认编译为PIC位置无关)||**预链接优化**|支持(通过`bind.exe`工具)|支持(通过`prelink`工具)||**热补丁能力**|有限支持(需预留跳转指令)|通过`LD_PRELOAD`实现运行时替换|>**性能影响**:LinuxPIC增加寄存器开销(约3%性能损失),但提升ASLR安全性[^3]。###5.**开发与调试支持**|**工具**|WindowsDLL|LinuxSO||------------------|----------------------------------------------|-------------------------------------------||**依赖查看**|`dumpbin/dependents`|`ldd`||**符号查看**|`dumpbin/exports`|`nm-D`||**调试工具**|WinDbg,VisualStudio|GDB,objdump||**错误诊断**|`GetLastError()`获取详细错误|`dlerror()`返回字符串描述|###6.**典型应用场景对比**|**场景**|WindowsDLL方案|LinuxSO方案||------------------|----------------------------------------------|-------------------------------------------||**插件系统**|通过COM接口或自定义加载器|直接`dlopen()`+`dlsym()`||**系统核心库**|Kernel32.dll,User32.dll等|libc.so.6,ld-linux-x86-64.so.2等||**图形库**|OpenGL32.dll(实际为转发库)|libGL.so(由Mesa或NVIDIA驱动提供)|```mermaidgraphTDA[动态链接库核心目标]-->B[代共享]A-->C[模块化开发]B-->D(WindowsDLL:物理内存共享)B-->E(LinuxSO:Copy-on-Write映射)C-->F(Windows:COM组件模型)C-->G(Linux:.so直接加载)```###总结关键差异1.**文件结构**:PEvsELF格式导致加载器行为不同2.**符号管理**:Linux的符号版本控制解决兼容性问题更优雅3.**安全机制**:Linux默认PIC支持ASLR,安全性更高4.**开发体验**:Linux工具链更透明,Windows集成环境更易用§§相关问题§§1.如何解决Linux动态链接库的版本冲突问题?2.WindowsDLLHell有哪些具体解决方案?3.为什么Linux动态库默认编译为位置无关PIC)?</think>### Linux动态链接库(.so) vs Windows DLL核心区别详解 #### 1. **文件格式与结构差异** | **特性** | Windows DLL | Linux共享对象(.so) | |----------------|----------------------------------------------|-------------------------------------------| | **二进制格式** | PE (Portable Executable) 格式 | ELF (Executable and Linkable Format) 格式 | | **入口函数** | `DllMain` (提供加载/卸载事件处理) | 无强制入口函数,可选`_init()`和`_fini()` | | **符号导出** | 需显式声明`__declspec(dllexport)` | 默认导出所有全局符号,可通过`visibility`属性控制 | | **依赖信息** | 存储在配套的`.lib`导入库文件中[^3] | 直接嵌入ELF文件的动态段(.dynamic) | > **本质差异**:PE与ELF格式的设计理念不同,导致加载机制和符号处理方式存在根本区别[^1]。 #### 2. **加载与链接机制对比** | **过程** | Windows DLL | Linux .so | |----------------|----------------------------------------------|-------------------------------------------| | **显式加载** | `LoadLibrary()` + `GetProcAddress()` | `dlopen()` + `dlsym()` | | **隐式加载** | 通过导入库(.lib)在链接时声明依赖 | 编译时指定`-l<name>`链接选项 | | **运行时解析** | IAT(导入地址表)重定向 | PLT(过程链接表)+GOT(全局偏移表)机制 | | **路径搜索** | 系统目录 > 当前目录 > PATH环境变量 | `/lib`, `/usr/lib`, `LD_LIBRARY_PATH`等 | ```mermaid graph LR A[程序启动] --> B{动态链接类型} B -->|Windows隐式加载| C[加载器解析IAT] B -->|Linux隐式加载| D[动态链接器解析PLT/GOT] B -->|显式加载| E[调用LoadLibrary/dlopen] ``` #### 3. **符号处理与版本管理** | **特性** | Windows DLL | Linux .so | |------------------|----------------------------------------------|-------------------------------------------| | **符号冲突** | 全局命名空间易引发冲突(DLL Hell) | 支持符号版本控制(Symbol Versioning) | | **版本管理** | 依赖文件命名(如`libcurl_v1.dll`) | 单文件多版本符号(如`GLIBC_2.4@@GLIBC_2.4`)| | **延迟绑定** | 有限支持(通过`/DELAYLOAD`选项) | 默认启用PLT延迟绑定 | > **关键优势**:Linux的符号版本控制允许不同版本API共存于同一库文件,彻底解决"DLL Hell"问题[^1]。 #### 4. **安全与性能优化** | **维度** | Windows DLL | Linux .so | |----------------|------------------------------------------|-------------------------------------------| | **地址随机化** | 支持ASLR但需`/DYNAMICBASE`选项 | 默认编译为PIC(位置无关)支持完整ASLR | | **热加载** | 需特殊设计(如FreeLibrary后重新加载) | 支持`LD_PRELOAD`环境变量实现运行时替换 | | **性能优化** | 绑定导入表加速(`bind.exe`) | 预链接优化(`prelink`工具) | > **性能数据**:LinuxPIC在x86-64架构下平均增加约3%指令数,但显著提升安全性[^1]。 #### 5. **开发工具链差异** | **工具类型** | Windows | Linux | |----------------|----------------------------------|----------------------------------| | **依赖查看** | `dumpbin /dependents` | `ldd` | | **符号查看** | `dumpbin /exports` | `nm -D` | | **调试工具** | Dependency Walker, WinDbg | `objdump`, `readelf` | | **错误诊断** | `GetLastError()`返回错误 | `dlerror()`返回可读错误信息 | #### 6. **典型应用场景** ```mermaid graph TD A[动态库应用场景] --> B[Windows] A --> C[Linux] B --> D[系统API封装<br>如kernel32.dll] B --> E[COM组件] C --> F[核心系统库<br>如glibc.so] C --> G[内核模块<br>通过insmod加载] ``` ### 总结关键差异 1. **格式兼容性**:Windows跨平台能力弱,Linux ELF格式被多种UNIX系统支持 2. **版本控制**:Linux的符号版本控制解决Windows饱受诟病的"DLL Hell"问题 3. **安全基础**:Linux默认PIC编译提供更强ASLR保护 4. **工具生态**:Linux提供更透明的调试工具链(ldd/objdump/readelf)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值