1.一个实例
#include <iostream>
int main()
{
printf("main\n");
return 0;
}
执行:g++ -std=c++11 main.cpp
,我们得到a.out
。
执行:objdump -dx a.out > 1.txt
,查看1.txt。
截取以下信息:
我们上述简单的实例可执行程序引用了来自C库的符号printf
。
这里的C库是动态库,程序执行时如何通过callq找到printf
符号定义位置属于动态库引用重定位范畴。
2.一个扩展的实例
// main.cpp
#include <iostream>
extern "C" int fun(int i, int j);
int main()
{
printf("main\n");
printf("1+2=%d\n", fun(1,2));
return 0;
}
// func.cpp
extern "C" int fun(int i, int j)
{
return i+j;
}
执行:g++ -std=c++11 func.cpp -c
得到func.o。
执行:g++ -std=c++11 main.cpp -c
得到main.o
。
执行:objdump -dx main.o > 1.txt
,查看1.txt。
截取以下信息:
上述main.o中引用了符号fun。但1.txt中并不包含符号fun的定义。
我们继续执行:g++ -std=c++11 main.cpp fun.cpp
执行:objdum -dx a.out > 11.txt
,查漏11.txt。
截取以下信息:
可以看到此时有两处改变:
(1).引用符号fun处,由0xe8 00 00 00 00变为0xe8 6a 00 00 00。
(2).fun符号的定义被拷贝到可执行文件a.out内代码段内了。
上述两处改变正是静态链接所作的工作。
(1).将符号引用对应到符号定义。
(2).将符号定义拷贝到可执行文件内。
3.静态库
3.1.实例解析
3.1.1.构建静态库
// t1.cpp
int addcnt = 0;
void addvec(int *x, int *y, int *z, int n)
{
int i;
addcnt++;
for (i = 0; i < n; i++)
z[i] = x[i] + y[i];
}
// t2.cpp
int mulcnt = 0;
void multvec(int *x, int *y, int *z, int n)
{
int i;
mulcnt++;
for (i = 0; i < n; i++)
z[i] = x[i] * y[i];
}
// t.h
#ifndef _T_H
#define _T_H
extern int addcnt;
void multvec(int *x, int *y, int *z, int n);
void addvec(int *x, int *y, int *z, int n);
#endif
上述结构中,我们基于t1.cpp,t2.cpp构建一个静态库。
构建静态库的makefile为:
main: t1 t2 static
t1:
g++ -std=c++11 t1.cpp -c
t2:
g++ -std=c++11 t2.cpp -c
static:
ar rcs libt.a t1.o t2.o
clean:
rm *.o libt.a
3.1.2.使用静态库
我们只需借助静态库文件libt.a,静态库提供的头文件t.h。即可引用静态库中导出的符号。
静态库头文件用于为库的使用者提供导出符号的声明。静态库自身则包含导出符号的定义。
以上述实例为例,我们在main.cpp使用了此静态库中定义的全局符号。
#include <stdio.h>
#include "t.h"
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
addvec(x, y, z, 2);
printf("z=[%d %d]\n", z[0], z[1]);
printf("addcnt_%d\n", addcnt);
return 0;
}
利用静态库,源文件构建可执行程序的makefile为:
main:
g++ main.cpp -std=c++11 -I./include -L./static -lt
clean:
rm a.out
3.2.静态链接解析
3.2.1.何时发生静态链接
上述实例中,我们通过makefile执行:g++ main.cpp -std=c++11 -I./include -L./static -lt
将在完成main.cpp的预处理,编译,汇编后,进入链接过程。链接过程对main.cpp引用的来自动态库的符号,来自静态库的符号的处理策略是不同的。对引用的来自静态库的符号的处理,称为静态链接处理。对引用的来自动态库的符号的处理,称为动态链接处理。
我们这里以链接过程main.cpp如何处理引用的来自静态库libt.a的符号来说明静态链接。
(1).通过符号解析将符号引用关联到符号定义
a.链接器维护一个可重定位目标文件的集合E;一个未解析的符号集合U;一个在前面输入文件中已定义的符号集合D。初始时,E、U和D均为空。
b.对编译命令中出现的可重定位目标文件和存档文件进行迭代处理。
b.1.若当前迭代到的是可重定位目标文件,称其为 f 。
把 f 添加到 E,修改U和D来反映 f 中 的符号定义和引用,并继续下一个输入文件。
b.2.若当前迭代到的是存档文件,称其为 F 。
对存档文件中所有的成员执行迭代,设当前迭代到的成员为 m 。
若 m ,定义了一个符号来解析 U 中的一个引用,那么就将 m 加到 E 中,修改 U 和 D 来反映 m 中的符号定义和引用。
为了保证,存档内部迭代时,前面一个成员定义了符号A,但没定义 U 中符号。后面一个成员定义了 U 中符号且引用了A场景下正确性。
上述迭代过程将持续进行,知道对存档中未加入到 E 的成员执行一轮迭代后,U 和 D 无任何变化,再结束。
结束迭代时,存档文件中未加入到 E 的成员,不会出现在最终汇集成的可执行程序文件中。
c. b阶段迭代结束后。
若 U 是非空的,就输出一 个错误并终止。
若 U 是空的,则合并和重定位 E 中的目标文件,构建输出的可执行文件。E 中每个文件的全部内容均将出现在可执行文件中。
上述处理过程要求,编译时,引用静态库符号的模块必须出现在静态库模块的前面。
(2). 将过程(1)中 E 中所有文件汇集得到单独的可执行程序文件。
(3). 对可执行程序文件中每个符号引用执行重定位。
我们针对上述实例,分析main中针对addvec引用通过静态链接执行重定位的过程。
a.查看main.cpp的反汇编,重定位信息
a.1.执行:g++ -std=c++11 main.cpp -I./include -c
a.2.执行:objdump -dx main.o
,截取输出中引用addvec部分的汇编
a.3.执行:objdum -sx main.o
,截取输出中重定位节中addvec引用的重定位项
顺便查看输出中符号表中关于addvec符号的信息:
因为addvec并非main.cpp中定义的符号。所以,并不清楚此符号的位置。
b.针对执行make得到的a.out查看其数据信息
b.1.针对a.out执行:objdump -sx a.out
,截取输出中符号表中addvec符号的信息
此时我们已经知道了addvec的具体位置信息。这正是链接器执行静态链接中做的工作之一。
有了引用符号的位置信息,再结合引用符号的重定位信息,我们就可以找到符号引用的位置,并将对应位置的内容进行重新设置。以便通过设置后的内容可以找到符号定义位置。
针对a.out执行:objdump -dx a.out
,截取引用addvec的汇编部分
为何说:将引用位置从0x 00 00 00 00修改为0x 3b 00 00 00就是对的呢?
因为0xe8表示的机器指令用于重新设置PC。将PC设置为:新PC = PC + 0xe8后四字节。
这样新PC = 0x400575(在执行到e8指令时,PC位于下一指令起始处)+ 0x3b = 0x4005b5。
而0x4005b5正是a.out中符号addvec定义所在位置。
3.2.2.静态链接中符号名多次定义问题
(1). 符号的强弱
我们正常定义个函数或变量,其对应的符号均是强符号。
若我们希望定义一个弱符号,需要通过编译器修饰符__attribute__((weak))
来达到目的。
(2). 链接阶段若收集到多个同名强符号的定义会报错
#include <stdio.h>
#include "t.h"
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
void addvec(int *x, int *y, int *z, int n)
{
printf("addvec_in_main\n");
}
int main()
{
addvec(x, y, z, 2);
printf("z=[%d %d]\n", z[0], z[1]);
printf("addcnt_%d\n", addcnt);
return 0;
}
若我们在上述实例的main.cpp中新增一个addvec的定义,则执行make时会报错。
(3).链接阶段收集到多个同名符号,但只有一个是强符号,其余为弱符号时,可以正常收集。对符号的引用会执行符号的强定义。
#include <stdio.h>
#include "t.h"
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
__attribute__((weak)) void addvec(int *x, int *y, int *z, int n)
{
printf("addvec_in_main\n");
}
int main()
{
addvec(x, y, z, 2);
printf("z=[%d %d]\n", z[0], z[1]);
printf("addcnt_%d\n", addcnt);
return 0;
}
按上述修改main.cpp执行make正常编译。运行a.out将执行静态库中addvec的强定义。
(4).链接阶段收集到多个同名符号,全部为弱符号时,可以正常收集。对符号的引用会关联到其中一个弱符号定义。