1. 动态库缺少依赖符号怎么排查
首先要清楚动态库和静态库的区别
以及动态库的编译过程和相关命令
-fPIC
是用于生成位置无关代码
位置无关码PIC详解:原理、动态链接库、代码重定位_位置无关代码-CSDN博客
https://www.bilibili.com/video/BV1nh4y1S7TG/
位置无关代码(Position-Independent Code,PIC)是一种能够在内存中任意地址加载并正确执行的代码。它广泛应用于共享库、动态链接库和地址空间布局随机化(ASLR)等场景,以提高灵活性和安全性。以下是其核心要点:
1. 为什么需要PIC?
- 动态库加载:不同进程可能将同一共享库加载到不同内存地址,PIC避免代码依赖固定基址。
- 安全性:支持ASLR,随机化代码加载地址,增加攻击者利用内存漏洞的难度。
- 代码复用:同一份库代码可被多个进程共享,节省内存。
2. 工作原理
代码段(Text Section)
- 相对跳转/调用:使用相对于当前指令指针(如x86的
EIP/RIP
)的偏移量,而非绝对地址。call printf@PLT ; 通过PLT跳转,地址在运行时解析 jmp .L1 ; 相对标签跳转,不依赖加载地址
- 数据访问:通过全局偏移表(GOT)间接寻址:
- 编译时生成GOT,存储全局变量和函数的绝对地址。
- 代码通过GOT + 偏移量访问数据,例如:
mov rax, [rip + GOT_OFFSET] ; RIP相对寻址获取GOT条目 mov rax, [rax] ; 获取实际数据
关键数据结构
- GOT(Global Offset Table):存储外部符号的绝对地址,动态链接时由加载器填充。
- PLT(Procedure Linkage Table):用于延迟绑定函数调用,首次调用时通过动态链接器解析地址。
3. 如何生成PIC?
- 编译选项:
- **
-fPIC
**:生成与位置无关的代码,适用于共享库(如Linux的.so
)。- **
-fPIE
**:生成位置无关的可执行文件(PIE),支持ASLR。- 链接选项:动态链接时需指定库路径(如
-shared
生成共享库)。
4. 优缺点
- 优点:
- 支持动态加载和内存随机化。
- 节省内存(多进程共享代码段)。
- 缺点:
- 轻微性能损耗(间接寻址和GOT/PLT跳转)。
- 代码体积略大(需额外指令处理偏移)。
5. 平台差异
- x86-64:天然支持RIP相对寻址,PIC实现高效。
- x86:需通过
ebx
寄存器指向GOT基址(如-fPIC
生成的代码)。- ARM:使用PC相对加载指令(如
LDR PC, [PC, #offset]
)。
6. 示例:动态库编译
# 编译为共享库(使用-fPIC) gcc -fPIC -shared -o libdemo.so demo.c # 编译为PIE可执行文件(支持ASLR) gcc -fPIE -pie -o app app.c
7. 相关概念
- ASLR:随机化栈、堆、库的加载地址,依赖PIC/PIE增强安全性。
- 动态链接:运行时解析符号地址,依赖GOT/PLT机制。
GOT 表的存储
GOT 表(Global Offset Table,全局偏移表)本质上存储的是绝对地址,只不过这些绝对地址是放在数据段中的。正因为它是放在数据段,所以可以被动态链接器(如
ld.so
)在运行时维护和修改。不同进程在加载同一个共享库时会有不同的数据段,因此它们各自的 GOT 表也是不同的
。GOT 表的作用
- 运行时地址重定位:共享库在运行时会被加载到内存中的不同位置。GOT 表的作用是记录这些动态确定的地址。例如,当程序调用一个外部函数(如
printf
)时,GOT 表中会存储printf
函数在当前进程内存中的实际地址。- 延迟绑定支持:动态链接的延迟绑定机制允许函数地址在首次调用时才被解析。GOT 表中的条目初始可能指向一个特殊的动态链接器代码(如
_dl_runtime_resolve
),当第一次调用函数时,动态链接器解析该符号的实际地址,并将解析结果填入 GOT 表,后续调用则直接使用 GOT 表中的地址。与静态库的对比
- 静态库:在编译时,静态库的
代码会被直接嵌入到最终的可执行文件中
。因此,静态库中的全局变量和函数的地址在编译和链接时就已经确定了,不需要 GOT 表。- 动态库:动态库依赖 GOT 表来动态解析和存储外部符号的地址。
代码段中的指令通过相对地址访问 GOT 表中的条目,而 GOT 表中的条目存放了运行时动态库被加载到内存中的实际绝对地址。
总结
- GOT 表:存放在数据段,记录动态确定的全局变量和函数的绝对地址,由动态链接器在运行时维护。
- 静态库:代码和符号地址在编译时确定,不需要 GOT 表。
通过使用动态库和 GOT 表,不同的进程可以共享同一个程序代码段,同时能够动态解析和加载外部符号的地址,从而节省内存和磁盘空间。
问:
GOT其实存的也是绝对地址是吗,只不过是放在数据段,因此可以被链接器(ld.so)维护,不同进程有不同的数据段(GOT表),即便访问的是同一个动态库,而GOT表的地址是可知的,里面存放了运行时动态库被加载的位置,代码段中使用的变量其实是通过记录查询GOT表中的条目的相对地址,获取GOT存的绝对地址。
而静态库因为是直接把代码嵌入,所以在编译时就已经确定好了地址(即便是相对于分配的起始地址)是吗,而动态库是在运行时分配的起始地址还要内部偏移(找到用于存储GOT)之后再读取动态库的绝对地址
被链接的动态库在程序运行过程中,可能会发生内存位置的改变
deepseek:
你的理解非常准确!以下是更清晰的总结和补充:
1. GOT(全局偏移表)的本质
- 存储内容:GOT中存放的是绝对地址,但这些地址在运行时动态确定(由动态链接器
ld.so
填充)。- 位置与隔离性:
- GOT位于数据段(可读写区域),每个进程有独立的GOT副本,即使多个进程加载同一个动态库,它们的GOT内容可能不同(例如库加载基址不同)。
- 代码段通过相对地址访问GOT条目(如x86-64的
RIP
相对寻址),再通过GOT中的绝对地址找到实际数据或函数。- 示例:
; 代码段中的指令(相对寻址访问GOT) mov rax, [rip + got_offset] ; 获取GOT条目中的绝对地址 mov rbx, [rax] ; 通过绝对地址访问实际数据
2. 静态库的地址确定性
- 编译与链接方式:
- 静态库(如
.a
文件)在编译时生成位置相关代码(除非显式启用-fPIC
),其代码和数据的地址在链接阶段确定(嵌入到最终的可执行文件中)。- 链接时,静态库代码的地址被解析为相对于可执行文件基址的绝对地址(若可执行文件是非PIE的),或基于相对偏移(若可执行文件是PIE)。
- 内存分配特性:
- 静态库代码被直接嵌入可执行文件,加载到内存时地址固定(除非可执行文件是PIE且系统启用ASLR)。
- 不同进程使用同一静态库时,会各自拥有独立的代码副本(无法共享内存)。
3. 动态库 vs 静态库的关键区别
特性 动态库(PIC) 静态库 地址绑定时机 运行时(通过GOT动态解析) 链接时(地址直接嵌入可执行文件) 内存共享 多个进程共享同一代码段 每个进程有独立副本 地址无关性 必须使用PIC(通过GOT/PLT) 默认位置相关(除非显式启用PIC/PIE) ASLR支持 天然支持(代码段可随机加载) 仅当可执行文件为PIE时支持 性能 轻微间接寻址开销 无间接寻址开销
4. 静态库与PIE的兼容性
- PIE(位置无关可执行文件):
- 若可执行文件编译为PIE(使用
-fPIE -pie
),则其基址在运行时随机化(类似动态库)。- 此时,静态库代码也需编译为PIC(
-fPIC
),否则无法适应基址随机化。- 非PIE可执行文件:
- 静态库代码通常为位置相关,链接时地址固定(如基址
0x400000
),无法支持ASLR。
5. 动态库的灵活性与代价
- 灵活性来源:
- 代码段通过相对寻址跳转(如
call printf@PLT
)。- 数据段通过GOT间接寻址(动态解析绝对地址)。
- 代价:
- 每次访问全局变量或外部函数需额外内存访问(GOT查询)。
- PLT的延迟绑定机制在首次调用函数时有解析开销。
6. 示例场景
动态库(libdemo.so)加载
># 编译为动态库(强制使用PIC) >gcc -fPIC -shared -o libdemo.so demo.c ># 使用动态库的可执行文件(支持ASLR) >gcc -o app app.c -L. -ldemo
- 运行时:
libdemo.so
的代码段加载到随机基址(如0x7f8a3d200000
)。- 进程的GOT中存储
libdemo.so
中符号的实际地址(如全局变量var
的地址为0x7f8a3d201000
)。静态库(libdemo.a)嵌入
># 编译为静态库(默认非PIC) >gcc -c demo.c -o demo.o >ar rcs libdemo.a demo.o ># 链接到可执行文件(非PIE,地址固定) >gcc -o app app.c -L. -ldemo
- 运行时:
- 静态库代码嵌入可执行文件,加载到固定基址(如
0x400000
)。- 所有符号地址在链接时确定(如变量
var
的地址为0x601044
)。
7. 总结
- GOT是动态库实现位置无关的核心机制,存储运行时确定的绝对地址,通过数据段隔离实现多进程兼容。
- 静态库的地址在链接时固定(除非显式使用PIC+PIE),牺牲了内存共享和ASLR支持,但换取了直接寻址的性能优势。
- 动态库的PIC与静态库的地址确定性体现了灵活性与效率之间的权衡。
# 编译动态库
gcc -shared -fPIC mylib.c -o libmylib.so
# 方案一:链接动态库
gcc main.c -L.-lmylib -o main
# 运行前设置动态库路径
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main
# 方案二:链接动态库,指定动态库加载路径
gcc -Wl,-rpath=./ main.c -o main -L./ -lmylib
./main
动态库的制作过程
- 静态链接阶段
符号解析
: 动态库的符号(变量,函数)被标记为“未在可执行文件中定义”- 写
.dynamic段
,记录动态库依赖信息.dynamic
段是ELF(Executable and Linkable Format)
文件格式中的一种特殊段,用于存储动态链接和动态加载所需的信息。它是动态链接器(dynamic linker)在程序运行时解析动态库依赖关系、符号绑定和重定位的关键数据结构。
生成重定向条目
- 动态加载阶段(见下图)
- 具体怎么排查?
当动态库缺少依赖符号时,可能会导致程序运行时出现错误,例如
undefined symbol
或unresolved symbol
。以下是排查和解决动态库缺少依赖符号问题的常见方法:1. 使用
ldd
检查动态库依赖
ldd
是一个用于检查程序或动态库依赖关系的工具。运行以下命令检查动态库的依赖关系:
bash复制
ldd /path/to/your/library.so
- 如果有未找到的依赖,会显示
not found
,例如:复制
linux-vdso.so.1 (0x00007ffc9d2db000) libmissinglib.so => not found
解决方法:
确保缺失的库已安装。
如果库已安装,检查库的路径是否在系统的库搜索路径中(如
/etc/ld.so.conf
或LD_LIBRARY_PATH
)。2. 使用
nm
检查符号
nm
命令可以列出目标文件(如动态库)中的符号表,检查是否包含所需的符号。运行以下命令:
bash复制
nm -C /path/to/your/library.so | grep <symbol_name>
-C
参数表示对 C++ 符号进行 demangling(解码)。如果没有输出,说明动态库中不包含该符号。
示例:
bash复制
nm -C /path/to/libmath.so | grep add
如果没有找到
add
符号,说明动态库中未定义该符号。3. 使用
objdump
检查符号
objdump
是一个用于反汇编目标文件的工具,也可以用来检查符号。运行以下命令:
bash复制
objdump -T /path/to/your/library.so | grep <symbol_name>
-T
参数表示显示动态符号表。示例:
bash复制
objdump -T /path/to/libmath.so | grep add
如果没有找到
add
符号,说明动态库中未定义该符号。4. 使用
strings
检查符号
strings
命令可以提取二进制文件中的可打印字符串,包括符号名称。运行以下命令:
bash复制
strings /path/to/your/library.so | grep <symbol_name>
- 示例:
bash复制
strings /path/to/libmath.so | grep add
如果没有找到
add
符号,说明动态库中未定义该符号。5. 使用 Dependency Walker(Windows)
- Dependency Walker 是一个用于分析 Windows 动态链接库(DLL)依赖关系的工具。
- 下载并运行 Dependency Walker,打开目标 DLL 文件,查看其依赖的模块和符号。
- 如果有未找到的模块或符号,Dependency Walker 会用红色高亮显示。
6. 使用
dumpbin
(Windows)
dumpbin
是 Visual Studio 提供的一个工具,用于查看 DLL 的依赖关系和导出符号。运行以下命令:
cmd复制
dumpbin /exports /path/to/your/library.dll
- 检查输出中是否包含所需的符号。
7. 检查编译和链接选项
- 确保在编译和链接动态库时,正确包含了所有依赖的符号。
- 如果使用了 C++,确保在编译和链接时使用了相同的编译器和标准库版本。
8. 检查库路径和环境变量
确保动态库的路径已添加到系统的库搜索路径中:
在 Linux 中,可以设置
LD_LIBRARY_PATH
环境变量:bash复制
export LD_LIBRARY_PATH=/path/to/your/library:$LD_LIBRARY_PATH
在 Windows 中,可以设置
PATH
环境变量。9. 重新编译和安装库
- 如果上述方法都无法解决问题,可以尝试重新编译和安装相关库或程序,并重新构建运行环境。
2. 僵尸进程和孤儿进程
僵尸进程(Zombie Process):
进程已经终止,但其父讲程末对其进行回收(调用wait
函数数)。僵尸进程占用系统的进程表项,但不再消耗其他资
源。操作系统会等待其父进程来获取它的终止状态信息,清除僵尸进程。孤儿进程(Orphan Process):
父讲程提前终止,子进程继续运行,成为孤儿进程。操作系统会将孤儿进程托管给init
进程(Linux系统中的PID为的进程),由init
进程来收养并清理这些孤儿进程。
演示僵尸进程
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
// 父进程和子进程会同时从 fork() 调用之后的代码开始执行,只是它们的进程 ID 和一些其他属性不同。
// 子进程是父进程的一个拷贝,包括代码和数据,但它们是两个独立的进程。
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
std::cout << "Child process: PID = " << getpid() << std::endl;
exit(0); // 子进程正常退出
} else {
// 父进程
std::cout << "Parent process: PID = " << getpid() << ", Child PID = " << pid << std::endl;
// 父进程不调用 wait() 函数来回收子进程的退出状态
std::cout << "Parent process is not waiting for child to terminate" << std::endl;
// 父进程继续运行
while (true) {
// 为了展示父进程的运行
std::cout << "Parent process is still running..." << std::endl;
sleep(1);
}
}
return 0;
}
3. 虚析构函数和虚函数表
每个带有虚函数的类都会有一个虚函数表
,其中存储了指向虚函数的指针。当派生类继承基类并覆盖虚函数时,派生类的虚函数表会替换基类的虚函数指针为派生类的实现。
(非析构)虚函数需要同名才能覆盖派生类对应的虚函数表中的指针,而只要基类定义的是一个虚析构函数,派生类在继承的时候,都会将派生类的虚函数表中的指针覆盖为~Derived()
,
#include <iostream>
using namespace std;
class Base
{
public:
int *data;
Base()
{
cout << "Base()" << endl;
data = new int[10]{0};
}
virtual ~Base()
{
delete[] data;
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
int *derived_data;
Derived()
{
derived_data = new int[10]{0};
cout << "Derived()" << endl;
}
~Derived()
{
delete[] derived_data;
cout << "~Derived()" << endl;
}
};
int main()
{ /* 基类指针删除派生类对象时,如果基类没有虚析构函数,会导致派生类的析构函数没有被调用。 */
Base *d = new Derived();
delete d;
}
/* 输出 */
Base()
Derived()
~Derived() // 因为虚函数表动态绑定到派生类析构函数
~Base() // 析构函数链式调用