CSAPP笔记04链接(中)

CSAPP笔记04链接(中)

0x00 符号和符号表

​ 每个可重定位目标模块都有一个符号表,它包含m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:

​ ①由模块m定义并能被其他模块引用的全局符号。对应非静态的c函数和全局变量

​ ②由其他模块定义并被模块m引用的全局符号。对应于在其他模块定义的非静态c函数和全局变量

​ ③只被模块m定义和引用的局部符号,他们对应于带static属性的c函数和全局变量。这些符号在模块中任何位置都能被引用,但是不能被其他模块引用。

image-20200810152451996

extern int a1;
static int b1=0;
static int b2=1;
int c1=0;
int c2=1;
static int func1(){
    static int dd1=0;
    static int dd2=1;
    dd1*=2;
    dd2*=2;
    return dd1+dd2;
}
int main(){
    int ee1=0;
    int ee2=1;
    b1*=2;
    b2*=2;
    a1*=2;
    ee1*=2;
    ee2*=2;
    func1();
    return a1+b1+b2+c1+c2+ee1+ee2;
}

​ 通过上面的readelf读取符号表可以看出,main函数中的本地变量ee1 ee2不存在符号表里,这些局部变量在运行时由栈管理。但是带有static的静态局部变量则是和静态全局变量一样,放在.data或者.bss里(取决于是否初始化和初始化是否为0),并且存在符号表.symtab里

​ 同时可以看出,静态局部变量在符号表里的名字和变量名不同,这正是链接器能够区分不同函数内相同名字的静态局部变量的关键!

​ (需要注意的是,如果定义一个变量,不论何种类型,如果没有使用,大概率会被编译器优化掉,因此无法在符号表里找到)。

符号表

​ 编译器先将c语言代码生成.s汇编文件,里面有各种函数和变量的标号,并记录了属性。而后汇编器再根据这个.s文件里的符号,生成符号表,记录在.o文件里的.symtab节里。

	.file	"main.c"
	.text
	.type	func1, @function
func1:
.LFB0:
	.cfi_startproc
	movl	dd1.1917(%rip), %eax
	addl	%eax, %eax
	movl	%eax, dd1.1917(%rip)
	movl	dd2.1918(%rip), %ecx
	leal	(%rcx,%rcx), %edx
	movl	%edx, dd2.1918(%rip)
	addl	%edx, %eax
	ret
	.cfi_endproc
.LFE0:
	.size	func1, .-func1
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	endbr64
	sall	b1(%rip)
	sall	b2(%rip)
	sall	a1(%rip)
	movl	$0, %eax
	call	func1
	movl	b1(%rip), %eax
	addl	a1(%rip), %eax
	addl	b2(%rip), %eax
	addl	c1(%rip), %eax
	addl	c2(%rip), %eax
	addl	$2, %eax
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.data
	.align 4
	.type	dd2.1918, @object
	.size	dd2.1918, 4
dd2.1918:
	.long	1
	.local	dd1.1917
	.comm	dd1.1917,4,4
	.globl	c2
	.align 4
	.type	c2, @object
	.size	c2, 4
c2:
	.long	1
	.globl	c1
	.bss
	.align 4
	.type	c1, @object
	.size	c1, 4
c1:
	.zero	4
	.data
	.align 4
	.type	b2, @object
	.size	b2, 4
b2:
	.long	1
	.local	b1
	.comm	b1,4,4
	.ident	"GCC: (Ubuntu 9.3.0-10ubuntu2) 9.3.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	 1f - 0f
	.long	 4f - 1f
	.long	 5

​ 以上是上面c语言代码的汇编文件,可以看到里面包含了各种符号,既包括函数又包括全局变量和静态变量。

​ .symtab节中包含了ELF符号表,这个表的每一项的结构如下:

image-20200810154415420

​ name: 记录了符号的名字在字符串表.strtab里的偏移(strtab里记录了各种符号的名字,这个name保存的是相对于strtab节的偏移)。

​ value: 记录了符号的值在对应节里的偏移(比如全局变量初始化非0,那么值纪录在.data节,value里记录的就是它的值在value节里的偏移地址,如果目标文件是可执行文件,那么记录的是绝对地址,因为已经重定位完成了)。

​ size: 记录符号对应对象的大小(数据大小或者函数所占的字节数)。

​ type: 常见三种值,object代表变量,function代表函数,section代表节(ELF节)

​ binding: 只有两个值,global和local,对于全局变量,或者函数都是global,而对应于静态变量(全局和局部都是)和节(section,也就是前面的.bss .data .text也作为符号保存在.symtab中)都是local。

​ section/ndx: 每个符号都被分配到某个节(也就是每个符号是在不同的节里定义的)由section字段表示,该字段使用节头部表里的索引

image-20200810155526038

​ (比如全局变量初始化为0,那么定义在.bss节,因此其符号表里的ndx对应4,再比如.text节本身也在符号表里,那么其ndx自然是.text对应的1啦,具体参见前面的符号表截图)。

​ 需要注意的是 section/ndx字段除了填索引外,还有三个字符串常量可以填,分别是:ABS,表示该符号不该被重定位;UNDEF,代表未定义,一般是引用外部变量和或者函数对应的符号;COMMON,未初始化的全局变量

注意:COMMON和4(对应.bss节)的区别:本来,所有未初始化的全局变量和静态变量,或者初始化为0的全局变量和静态变量都在.bss节,但是现在规定,虽然未初始化的全局变量在.bss节,但是其符号的ndx字段就得填COMMON而不是4

​ 可以使用readelf命令来观察目标文件的elf结构。

0x01 符号解析

​ 符号解析是指,链接器所有的重定向文件里每个符号的引用,(也就是对标号的使用),都找到重定向文件里符号表里的某个确定的符号关联起来,以便后面重定位。

​ 对于符号的引用分为三大类 :

​ ①不在符号表里定义的符号,即非静态的局部变量,在编译时就完成符号解析,不允许有符号名冲突。

​ ②局部符号:即静态变量,所有的静态变量在符号表里binding字段都是local,也就是局部符号,由于不同函数中同变量名的静态局部变量的符号会不同,因此局部静态变量不会发生解析符号冲突,而全局静态变量的变量名即是符号名,如果冲突,编译阶段即报错。

​ ③全局符号,即函数和全局变量对应的符号名。

全局符号解析

①找不到引用对应的全局符号

​ 在编译阶段,如果编译器遇到一个没有在当前模块(也就是当前目标文件)中没有定义的符号(变量或者函数名,也就是在符号表里的NDX/SECTION字段为UNDEF,也就是c语言里用extern 定义的,如果在符号表里都没有,编译阶段就直接报错了)时,就假设该符号定义在其他的模块中定义的,生成一个链接器符号表条目,等待链接器处理。如果链接器发现所有的输入模块的符号表里都找不到这个被引用的符号,就报错并终止

​ 变量引用找不到对应符号:image-20200810164047797

image-20200810164058784

​ (上图是写了extern,但是其他模块中并没有定义,而下图是连extern都没写就直接用。)

​ 函数引用找不到对应符号:

image-20200810164341243

image-20200810164404425

​ (上图是在模块中写了引用的函数的原型,而下图连函数原型都没写)

②找到多个同名全局符号

​ 链接时,如果多个输入目标模块(即目标文件)有多个重名的全局符号的情况:编译时,编译器会在给出每个全局符号的强弱(strong or weak)。强的是函数和已经初始化的全局变量(已经初始化的全局变量,如果为0,在.bss不为0在.data),汇编器将其在符号表里的NDX设置为3(.data节),而弱的是未初始化的全局变量将被放在COMMON。

​ 根据强弱符号的定义,同名全局符号遵循以下三个规则:

规则一:不允许有多个同名的强符号。

规则二:如果有一个强符号和多个弱符号,则选择强符号。

规则三:如果有多个同名弱符号,则任意选择一个。

强符号冲突:

//1.c
int x=123;
int main(){
    return 2;
}
//2.c
double x=123;
//gcc -Og 1.c 2.c -o prog.out
//error!

x在两个目标模块中都初始化了,都是强符号,因此链接时报错

一个强符号和若干弱符号:(无warning,无error)

//1.c
int x=123
int main(){
    f();
    return 0;
}
//2.c
int x;
void f(){
    x=100;
}
//gcc -Og 1.c 2.c -o prog.out
//运行后main中的x将被改成100,因为所有对x的引用都被定位到了1.c中的x

由此导致的很隐秘的错误:

//1.c
#include <stdio.h>
int y=100;
int x=100;
void foo();
int main(){
    foo();
    printf("%d\n",x);
    return 0;
}
//2.c
double x;
void foo(){
    x=1.2;
    return ;
}
//gcc -Og 1.c 2.c -o prog.out
//运行后x输出235235235一个很大的数字,原因是2.c里的x定位到了1.c里的
//地址,但是在编译和汇编时2.c里将他当double了,最终还是会将1.2对应的double二进制数写入到x的空间。

与静态库链接

​ 前面的例子都是假定链接器读取一组可重定位的目标文件,把它们链接起来,输出一个可执行文件。实际上,所有的编译系统都提供一种机制,将所有相关的目标模块(也就是目标文件)打包成一个单独的文件,称为静态库(static library)。静态库也用作链接器的输入,当链接器构造可执行文件时,它只复制静态库里被应用程序引用的目标模块

​ 静态库来源于对c语言标准函数的提供。在使用静态库之外,有这些方法,能够向c语言开发者提供标准函数调用:

​ ①编译器生成调用的标准函数的代码。如果编写者调用了c语言的标准函数,那么编译器负责识别,并将对应函数的代码自动添加到机器码里。

​ 缺点:将让编译器的实现非常复杂,并且要修改标准函数就要修改编译器

​ ②将所有标准函数都写在一个目标模块里。这样的话,用户只需将字节的目标模块和库模块链接在一起即可。

​ 缺点:对磁盘和内存的浪费极大,因为这样不管程序功能大小目的如何,每个程序有一份巨大的库模块在内存和最终可执行文件中。并且更要命的是,库模块的编写者无论对库进行多么小的修改,都要重新编译整个库。

​ ③为每个标准函数都创建一个独立的可重定位文件,然后把它们存放在约定的目录里。

​ 确定,链接起来十分繁琐,因为每个被调用的函数都是一个输入模块,如果发生了嵌套引用,那么要输入的命令十分长且容易出错遗漏。

image-20200811085433719

​ 静态库解决了上述方法的缺点,因为同类型的函数被编译为独立的目标模块,然后再封装成一个单独的静态库文件。链接时可以在命令行里指定静态库文件以使用其中的函数,并且只复制其中使用的函数的目标模块,这样就减少了在内存和磁盘的浪费,也简化了命令。

​ 例如,我们使用的printf函数被编译成了独立的目标模块,并被封装在了libc.a里,linux上使用whereis / find命令查找该文件。(实际上编译器总是自动把libc.a传送给链接器的,因而可以不用我们手动填写,我们直接gcc main.c -o prog 即可)

image-20200811090008408

​ linux系统中,静态库以一种被称为存档(archive .a后缀)的特殊格式保存在磁盘,.a文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。

如何在linux上创建并使用静态库.a

​ 我们定义以下两个待调用函数,分别编译成两个.o的可重定位模块:

//addvec.c
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];
    }
}
//multvec.c
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 //一次将两个文件编译成.o可重定向文件
ar rcs libvec.aaddvec.o multvec.o//将两个函数的可重定位模块合成静态链接库(archive)

image-20200811092811715

​ 并编写库对应的.h文件,方便c语言编写者使用(.h文件里都是对应库的函数原型、宏定义、结构定义)

//libvec.h
void addvec(int*x,int*y,int*z,int n);
void multvec(int*x,int*y,int*z,int n);

​ 编写如下main函数来调用库里面的函数:

//main.c
#include <stdio.h>
#include "libvec.h"//用""而不是<>表明在同一个目录
int x[]={1,2};
int y[]={3,4};
int z[]={0,0};
int main(){
    addvec(x,y,z,2);
    printf("z:[%d %d]",z[0],z[1]);
    return 0;
}

​ 使用以下命令编译链接main.c:

image-20200811094051523

​ 最终成功调用静态链接库,注意其中的-static命令 表示使用静态链接库。

​ 当链接器运行时,判定main.o引用了addvec.o定义的addvec符号,所以就复制addvec.o到可执行文件,因为没有引用由multvec.o定义的符号,因而不会复制这个模块。同时由于使用了libc.a里的printf.o里的printf符号,因此也会复制printf.o模块,因为libc.a是自动加入的,因此我们没有在命令里指定。

链接器如何使用静态库来解析引用

​ 在符号解析阶段,链接器按照命令行里输入文件从左到右的顺序来扫描可重定位目标文件和存档文件(.a静态库)。

​ (这一部分较为复杂,内容繁多,在此先略过,只需要知道静态库应该放在输入文件的最后即可,以后有时间再补上。)