目录
6.2.2规则2:如果有一个强符号和多个弱符号同名,那么选择强符号。
6.2.3规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
1.基础知识
1.1什么是链接?
将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存中并执行
1.2链接执行的时间?
1.执行于编译时,在源代码被翻译成机器代码时
2.执行于加载时,程序被加载器加载到内存并执行时
3.执行于运行时,由应用程序来执行
1.3学习链接用来解决什么问题?
1.构造大型程序
解决缺少模块、缺少库,或者不兼容的库版本引起的链接器错误。
2.避免编程错误
3.辅助理解语言的作用域规则的实现
4.辅助理解其他重要系统的概念
如:加载、运行程序、虚拟内存,分页,内存映射
5.利用共享库
共享库和动态链接
软件在运行时使用共享库来升级压缩包装的二进制程序
许多we服务器都依赖于共享库的动态链接来提供动态内容
1.4基本流程
运行环境——Linux X86-64
两个源文件
int sum(int *a,int n);
int array[2]={1,2};
int main()
{
int val=sum(array,2);
return val;
}
int sum(int *a,int n)
{
int i,s=0;
for(i=0;i<n;i++)
{
s+=a[i];
}
return s;
}
手动复现上述过程
1.cpp main.c main.i
将源程序main.c翻译为一个ASCII码的中间文件main.i
2.cc1 main.i -Og -o main.s
cc1编译器将main.i翻译成一个ASCII汇编语言文件main.s
刚开始的时候会出现找不到cc1报错
在网上查询发现可能是路径问题,如下是在一个相关的讨论帖里面找到的
请问GCC里面的cc1命令怎么运行 - C/C++-Chinaunix
先使用命令gcc -v main.c sum.c 2>&1 |grep cc1
确定cc1所在的位置,此处为 /usr/lib/gcc/x86_64-linux-gnu/9/cc1
然后使用命令/usr/lib/gcc/x86_64-linux-gnu/9/cc1 main.i -Og -o main.s
就可以得到main.s文件
查看main.s内容
3.as -o main.o main.s
汇编器as 将main.s翻译成一个可 重定位目标文件main.o
4.重复以上步骤生成sum.o
5.ld -o prog main.o sum.o
链接程序ld将main.o和sum.o以及一些必要的系统文件组合起来创建一个可执行布标文件prog
存在的问题:
具体命令如下:
手动调用链接器来构造可执行程序时,还需要以上的五种 .o文件
或者gpt的回答方法是
使用 gcc 命令进行链接可能会更加简单和方便,因为它会自动处理这些依赖关系。例如:
gcc -static -o prog1 main.o sum.o
这将使用 gcc 命令以静态方式链接程序,并且会自动处理所有必需的库链接。
- ./prog
执行程序,shell调用操作系统中的加载器函数,将可执行文件prog中的代码和数据复制到内存,然后将控制转移到这个程序的开头
也可以通过gcc -v参数来生成可执行文件,并查看中间过程
gcc -v -Og -o prog main.c sum.c
2.静态链接
链接器主要完成两个任务
2.1符号解析
关键词:定义和引用
每个符号对应一个函数、全局变量、静态变量
符号解析的目的是 将每个符号引用和每个符号定义关联
2.2重定位
编译器和汇编器生成地址从0开始的代码和数据节
链接器通过把每个符号定义和一个内存位置关联起来,实现重定位
然后修改所有对这些符号的引用,使他们指向内存位置。
3.目标文件
有三种形式
1.可重定位目标文件——经过编译和汇编的.o文件
2.可执行目标文件——经过链接后的文件
3.共享目标文件——一种 特殊类型的可重定位目标文件可以在加载或运行时被动态的加载进内存并链接
4.可重定位目标文件
【CSAPP-深入理解计算机系统】7-2. 可重定位目标文件_哔哩哔哩_bilibili
4.1基本组成
4.2节区的基本内容
.text:已编译程序的机器代码
.rodata:只读数据,比如说printf语句中的格式串,和Switch语句中的跳转表
.data:已初始化的全局和静态便利那个
.bss:未初始化的全局和静态变量,以及初始化为0的全局或静态变量
bss块在文件中不占据实际的空间,只是一个占位符,可以理解为是一个提升效率的节区,来区分初始化和未初始化变量,在目标文件中,未初始化的变量不需要占据任何实际的磁盘空间,运行时,在内存中分配这些变量,初始值为0.
better save space
.symtab:符号表,存放程序中定义和引用的函数和全局变量的信息
4.3如何生成可重定位文件?
#include<stdio.h>
int count =10;//赋初值的全局变量
int value; //未赋初值的全局变量
void func(int sum)
{
printf("sum is :%d\n",sum);
}
int main()
{
static int a=1;//初始化的静态变量
static int b=0;//初始化但值为0的静态变量
int x=1;//局部变量
func(a+b+x);
return 0;
}
对main.c进行编译和汇编而不链接
(-c Compile and assemble, but do not link.)
gcc -c main.c
得到了可重定位目标文件main.o
.
查看文件包含了多少字节
查看elf header中的详细内容
readelf -h main.o
前16个字节所代表的基本含义
查看section table
readelf -S main.o
查看目标文件的内容
objdump -s -d main.o
-s 选项告诉 objdump 显示目标文件的所有部分的内容(即所有节的内容)。
-d 选项告诉 objdump 显示目标文件的反汇编内容。
首先查看数据部分.data节
之前定义了初始化了的全局变量count值为10,静态变量a值为1
并且数据存放方式为小端存储
如上可以看到初始化了的全局变量和静态变量是在data节中的
同时也可以看到.rodata节中保存了printf中需要打印的字符串
5.符号和符号表
链接过程的本质就是将不同的目标文件粘合在一起,而符号就可以看做是其中的粘合剂
每个可重定位目标模块m都有一个符号表,包含m定义和引用的符号信息,
5.1符号的分类
在链接器的上下文中,存在三种不同的符号
1.由m定义并能被其他模块引用的全局符号,对应非静态的C函数和全局变量
2.由其他模块定义被m引用的全局符号,成为外部符号,对应在其他模块中定义的非静态的 C函数和全局变量
3.只被m定义和引用的局部符号,对应与带static属性的C函数和全局变量。
尽可能用static属性来保护变量和函数是很好的编程习惯
5.2符号表
符号表是由汇编器构造的,在.symtab节中,其结构体如下
其中各个字段的具体含义为:
name:字符串表中的字节偏移,指向符号的以null结尾的字符串名字
value:距定义目标的节的起始位置的偏移 ,即节中的相对偏移,在可执行目标文件中, 该值是一个绝对运行时的地址
size:目标的大小,单位是字节
type:数据对象(OBJECT)/函数(FUNC)
binding:表示符号是本地的还是全局的
Ndx:是一个到节头部表中的索引,表示当前符号在哪个节中
存在3个特殊的伪节,在节头部表中不存在相应的条目
1.ABS代表不该被重定位的符号
2.UNDEF代表未定义的符号,即在本目标模块中引用但却在其他地方定义的符号
3.COMMOM表示还未被分配位置的未初始化的数据目标
注:以上三个伪节只在可重定位目标文件中存在,在可执行目标文件中是没有的
而COMMON和.bss仍存在细微的区别
查看main.o中符号表的内容
readelf -s main.o
局部变量
可以看到,原来的变量名a,b在此处位a.2320,b.2321,变量名自动加后缀
这一操作称为名称修饰,防止静态变量名称冲突
6.符号解析
链接器解析符号引用的实质
将引用与可重定位目标文件的符号表中的确定的符号定义相关联
6.1缺少符号定义
void foo();
int main()
{
foo();
return 0;
}
在main.c中只有对foo()的声明,没有具体定义
对mian.c可以正常的编译和汇编
查看符号表
汇编器生成了对应的符号表条目
对main.o进行链接
gcc -Wall -Og -o main main.c
Wall参数指将所有的警告输出
以上警告信息中提示找不到foo符号定义
6.2链接器对于多重定义的全局符号解析的处理
在编译时,编译器向汇编器输出每个全局符号,分为强/弱两类,汇编器要把这个类别信息隐含的编码在可重定位目标文件的符号表里。
强符号:函数和已初始化的全局变量
弱符号:未初始化的全局变量
处理规则:
规则1:不允许有多个同名的强符号。
规则2:如果有一个强符号和多个弱符号同名,那么选择强符号。
规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
6.2.1规则一:不允许有多个同名的强符号。
int main()
{return 0;}
int main()
{return 0;}
对以上两个文件进行编译
此处强符号main被多次定义
int x=15213;
int main()
{return 0;}
int x=15212;
void f()
{}
对以上两个文件进行编译
此处强符号x被多次定义
6.2.2规则2:如果有一个强符号和多个弱符号同名,那么选择强符号。
#include<stdio.h>
void f();
int x=15213;
int main()
{
f();
printf("x=%d\n",x);
return 0;
}
int x;
void f()
{
x=15212;
}
对以上两个文件进行编译,并没有错误和警告提示
然后运行foobar3
此处在bar3.c中的是未被初始化的 全局变量,是弱符号,在foo3.c中是初始化值为15213的全局变量,是强符号,所以编译链接时,x的值取15213
最后输出的结果是15212的原因是,调用f()函数,对全局变量的值进行了更改,但是链接器通常不会表明它检测到多个x的定义,这样的代码容易造成一些不易察觉的运行时错误
6.2.3规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
#include<stdio.h>
void f();
int x;
int main()
{
x=15213;
f();
printf("x=%d\n",x);
return 0;
}
int x;
void f()
{
x=15212;
}
编译链接并运行,结果为15212
最后输出的结果是15212的原因是,调用f()函数,对全局变量的值进行了更改,但是链接器通常不会表明它检测到多个x的定义,这样的代码容易造成一些不易察觉的运行时错误
6.2.4同名全局变量的类型不同导致的错误
#include<stdio.h>
void f();
int y=15212;
int x=15213;
int main()
{
f();
printf("x=0x%x y=0x%x\n",x,y);
return 0;
}
double x;
void f()
{
x=-0.0;
}
产生警告提示信息,但并未产生错误信息
运行
产生如上结果的原因
查看重定位表——readelf -s foobar5
变量x的地址是0x0000000000004010
变量y的地址是0x0000000000004014
查看目标文件的内容
objdump -s foobar5
而double类型的8字节的-0.0对应的16进制为8000000000000000
最后负零的双精度浮点数覆盖内存中x和y的位置
怎么避免此类的错误呢?
用像GCC-fno-common标志这样的选项调用链接器,这个选项会告诉链接器,在遇到多重定义的全局符号时,触发一个错误
7.静态库链接
编译系统提供一种机制,将所有相关的目标模块打包成一个单独的文件,成为静态库
在Linux系统中,静态库以一种被称为archive的特殊文件格式存放在磁盘中
存档文件是一组可重定位目标文件的集合,存档文件后缀为.a
7.1与静态库链接
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];
}
}
int multcnt=0;
void multvec(int *x,int *y,int *z,int n)
{
int i;
multcnt++;
for(i=0;i<n;i++)
{
z[i]=x[i]*y[i];
}
}
创建静态库
gcc -c addvec.c multvec.c
ar rcs libvector.a addvec.o multvec.o
libvector.a就是新生成的库
创建vector.h头文件
#ifndef VECTOR_H
#define VECTOR_H
void addvec(int *x, int *y, int *z, int n);
void multvec(int *x, int *y, int *z, int n);
#endif // VECTOR_H
创建main函数来调用这个库,其中头文件vector.h定义了libvector.a例程中的函数原型
#include<stdio.h>
#include"vector.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]);
return 0;
}
首先编译main.c 得到main.o
gcc -c main.c
然后链接mian.o和libvector.a
gcc -static -o prog2c main.o ./libvector.a
-static参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,可以加载到内存执行,在加载时无需更进一步的链接
运行可执行程序
7.2链接器使用静态库来解析引用
链接器 从左到右按照砸编译器驱动程序命令行上出现的顺序来扫描可重定位文件和存档文件
一定要注意输入文件的顺序!!!
三个集合
E:可重定位的目标文件集合,最后会被合并起来形成可执行文件
U:未解析的符号集合,引用但未定义
D:已定义的符号集合
以上述链接过程为例: gcc -static -o prog2c main.o ./libvector.a (默认调用libc.a)
首先判断输入文件的类型,是可重定位目标文件还是静态库文件
首先处理main.o文件
然后处理libnector.a,在静态库文件中寻找未定义的符号
如:addvec,将addvec.o添加到集合E,并在U中删除
同时将addvec.o中包含的其他符号按照类别添加到三个集合中,此处为addcnt
待扫描过所有的输入文件之后
如果U是空的,就将E中的文件合并成可执行文件
如果U是非空的,链接器输出错误并终止