辅助工具
gcc/g++编译c/c++源代码
-E 预编译,生成后缀为 .i 的预编译文件。处理#开头预处理指令,#difine、#include、#if等
-S 生成汇编代码,生成后缀为 .s 的汇编源文件。
-c 只编译不连接,生成后缀为 .o 的目标文件。
-o 确定输出文件的名称
-g 产生符号调试工具(GNU的 gdb)所必要的符号信息
-static
-shared 动态链接生成共享对象so文件
-fPIC 不使用该参数装载时重定位,使用该参数调用时重定位,-shared一般都需要带-fPIC
-Wl 将紧跟其后的参数传给连接器ld
-Wl,-Bsymbolic 强制采用本地的全局变量定义
-Wl,-Bstatic 指示跟在后面的-lxxx选项链接的都是静态库(若不指定,优先找动态链接库)
-Wl,-Bdynamic 指示跟在后面的-lxxx选项链接的都是动态库
-std=c++11 启用c++11标准
-L 指令动态链接目录
-I 指定include目录
-rdynamic 指示连接器把所有符号(而不仅仅只是程序已使用到的外部符号,但不包括静态符号,比如被static修饰的函数)都添加到动态符表
-w 的意思是关闭编译时的警告
-Wall 选项意思是编译后显示所有警告。
-W 是只显示编译器认为会出现错误的警告
-fmessage-length=0 编译过程中输出信息会根据控制台的宽度自动换行
objdum 命令
Linux下的反汇编目标文件或者可执行文件的命令
objdump -d 1.o #反汇编代码段
objdump -r 1.o #重定位表,链接时代码段中需要重定位的变量的位置
objdump -t 1.o #符号表,所有静态使用和导出的符号
objdump -T 1.so #动态符号表
objdump -h 1.so #显示所有段头
readelf命令
用于查看ELF格式的文件信息,常见的文件如在Linux上的可执行文件,动态库(*.so)或者静态库(*.a) 等包含ELF格式的文件
readelf -r 1.o #重定位表,链接时代码段中需要重定位的变量的位置
readelf -s 1.o #符号表,所有静态使用和导出的符号
readelf -x .got 1.so #以16进制显示指定段的内容(其中.got可以是段名,也可以是下标)
readelf -d a.out #打印.dynamic段信息,可以查看依赖的动态链接库
nm命令
列举出该目标中定义的符合要求的符号
ldd命令
列出一个程序所需要得动态链接库(so),“=>”左边的表示该程序需要连接的共享库so名称,右边表示按运行时共享库搜索路径找到的so具体路径。
静态链接
根据重定位表、符号表重定位代码段未定义符号
//文件1.c
#include<stdio.h>extern int ss;
int add(int a,int b);
int main()
{
int a = 1;
int c = add(a,ss);
printf("%d\n",c);
return 0;
}
//文件2.c
int ss = 2;
int add(int a,int b)
{
return a+b;
}
1. 编译
gcc -c 1.c
gcc -c 2.c
2. 结合汇编代码、重定位表、符号表看出1.o文件中还有3个符号未定位到具体代码
汇编代码
重定位表
符号表
3.静态链接生成可执行文件
gcc 1.o 2.o
4. 生成的可执行文件a.out重定位表应该是可空的,否则链接时会报错“未定义的引用”
5.可执行文件a.out的符号表对应的3个符号已经都有定义了,printf还是标记UND,是运行时加载动态符号表,动态链接时再讲
6.对比静态链接前后的汇编代码
动态链接
.got
GOT(Global Offset Table)全局变量引用的地址。
- .got.plt
全局函数引用地址表,所有对外部函数引用的地址全部在.got.plt
- .plt
PLT(Procedure Linkage Table)程序链接表。要么在 .got.plt
节中拿到地址,并跳转。要么当 .got.plt
没有所需地址的时,触发「链接器」去找到所需地址
linux动态链接使用了PLT延长绑定技术,函数调用前进行函数链接。
//文件1.cpp
int add(int a,int b)
{
return a+b;
}
//文件2.cpp
#include<stdio.h>
int add(int a,int b);
int main()
{
int c = add(1,2);
printf("%d\n",c);
return 0;
}
1.编译、链接以上代码
gcc -shared -fPIC -o 1.so 1.cpp #编译动态链接库1.so
gcc 2.cpp 1.so #编译可执行文件生成a.out
2.反汇编a.out、1.so代码,分析动态链接延迟绑定(PLT)过程
readelf -x .got.plt a.out #a.out显示.got.plt段
objdump -R a.out #a.out显示动态重定位表
objdump -d a.out #a.out显示代码段汇编代码
objdump -T 1.so #1.so显示动态符号表
objdump -d 1.so #1.so显示代码段汇编代码
全局符号介入问题解决
有多个动态链接库都定义了同名函数,则可能出现全局符号介入的问题。此时,依据链接器的规则,只会保留第一个链接的函数,而忽略后面链接进来的函数。链接器的装载顺序为广度优先算法。
//文件1.cpp
#include<stdio.h>
void print_self()
{
printf("1.cpp\n");
}
//文件2.cpp
#include<stdio.h>
void print_self()
{
printf("2.cpp\n");
}
void test()
{
print_self(); //希望调用2.cpp
}
//文件3.cpp
int print_self();
int test();
int main()
{
print_self(); //希望调用1.cpp
test(); //希望调用2.cpp
return 0;
}
1.编译执行效果如下,函数并没有按我们希望的方式去调用。
2.执行
objdump -d a.out
objdump -d 2.so
查看汇编代码,如上图,main调用test,test调用print_self都是动态链接延迟绑定(plt),参考前面动态链接章节我们知道,一旦绑定了函数,后面的函数就会被忽略,1.so放在前面,print_self就全部执行1.cpp中的,2.so放前面,print_self就全部执行2.cpp中的。
3.如何解决该问题,基本思路就是让2.cpp中test调用print_self不要走动态链接
方案一:使用链接参数 -Wl,-Bsymbolic 优先使用本地符号,如下图:
执行objdump -d 2.so ,如下图,2.cpp汇编代码test调用print_self已经是绝对地址调用了。
若链接时2.so放前面会怎么样,看下图,main中调用print_self函数调到了2.cpp中,原因是动态链接先找到2.cpp中的print_self函数:
执行ldd a.out可查看动态链接的符号查找顺序,查找算法为广度优先算法,如下图:
通过-Wl,-Bsymbolic链接参数的方案还算比较完美。
方案二:2.cpp中print_self定义为static
文件2.cpp
#include<stdio.h>
static void print_self() //定义为static
{
printf("2.cpp\n");
}
void test()
{
print_self(); //希望调用1.cpp
}
编译执行,问题解决,如下图:
执行 objdump -d 2.so,如下图,2.cpp汇编代码test调用print_self已经是绝对地址调用了。
该方案的优点是屏蔽了2.cpp中的print_self,链接顺序不影响执行结果,缺点是必须改代码,且其它模块也使用不了2.cpp中的print_self函数了。
共享库路径
系统共享库路径
/lib:系统运行最关键和基础的共享库,主要是/bin /sbin用到的库
/usr/lib:一般不是系统运行本身需要的共享库,而是开发时用到的系统库
/usr/local/lib:第三方软件库,运行程序一般在/usr/local/bin
环境变量PATH、 LIBRARY_PATH、 LD_LIBRARY_PATH的区别
PATH:可执行文件路径
LIBRARY_PATH:程序编译期间查找动态链接库时指定查找共享库的路径
LD_LIBRARY_PATH:程序加载运行期间查找动态链接库时指定除了系统默认路径之外的其他路径
编译链接时共享库搜索
1.链接时指定了具体路径,则按指定路径找共享库,如 g++ 1.cpp ./1/2.so,运行时也会在这个目录下找so,这种方式兼容性不好,一般不用
2.链接时未指定具体路径,如g++ 1.cpp -l2 或g++ 1.cpp 2.so,则链接时搜索顺序为:
(1)编译参数-L指定的目录,如g++ 1.cpp -l2 -L ./1/
(2)环境变量LIBRARY_PATH指定的目录(“export LIBRARY_PATH=:” 表示当前目录)
(3)系统目录/lib、/usr/lib和/etc/ld.so.conf里指定的路径
运行时共享库搜索
1.链接时指定了具体路径,即readelf -d a.out中NEEDED项依赖的是带路径的so,则按照这个指定的路径去找so。
2.环境变量LD_LIBRARY_PATH指定的目录(“export LD_LIBRARY_PATH=:” 表示当前目录)
3.系统目录/lib、/usr/lib和/etc/ld.so.conf里指定的路径
备注:ldd展示的是按以上规则搜索到的so目录,即可以认为ldd找到的文件就是运行时加载的so文件
LD_PRELOAD环境变量
LD_PRELOAD,用于动态库的加载,动态库加载的优先级最高,一般情况下,其加载顺序为LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib,命令格式如:export LD_PRELOAD="./1.so ./2.so",
一旦设置了LD_PRELOAD,任何程序无论是否依赖该so,启动前都会先加载该so,所以该环境变量一般只用于测试场景。
场景一:不同so包含相同的函数,如下图,1.so放前面就调用1.cpp的函数,2.so放前面就调用2.cpp的函数。
//文件1.cpp
#include<stdio.h>
void print_self()
{
printf("1.cpp\n");
}
//文件2.cpp
#include<stdio.h>
void print_self()
{
printf("2.cpp\n");
}
//文件3.cpp
int print_self();
int main()
{
print_self();
return 0;
}
场景2:相同的so,在不同的目录下存在多份,通过LD_PRELOAD控制运行时加载顺序。
//文件1.cpp
#include<stdio.h>
void print_self()
{
printf("1.so\n");
}
//文件2.cpp
#include<stdio.h>
void print_self()
{
printf("1.so.new\n");
}
//文件3.cpp
int print_self();
int main()
{
print_self();
return 0;
}
LD_DEBUG环境变量
export LD_DEBUG=files # 显示库的依赖性和加载的顺序
export LD_DEBUG=bindings # 显示符号绑定过程
export LD_DEBUG=libs # 显示加载器查找库时使用的路径的顺序
export LD_DEBUG=versions # 版本依赖性
export LD_DEBUG=help # LD_DEBUG的帮助信息
export LD_DEBUG=reloc #显示重定位过程
export LD_DEBUG=symbols #显示符号表查找过程
通过export LD_DEBUG=libs查看so搜索过程
1.编译时指定so具体路径,运行时直接按该路径查找,如下图:
2.编译时不指定so具体路径,先找LD_LIBRARY_PATH,再找系统目录,如下图:
通过export LD_DEBUG=symbols查看符号查找过程