csapp链接
链接是什么呢?
链接(Linking) 是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译、加载、运行时,在早期,链接是被手动执行的,在现代系统中,链接是由一个叫做链接器的程序自动执行的。
-
预处理时:处理的过程
- 以#开头的预编译指令;删除以“#define”并展开所定义的宏;处理所有的条件预编译指令,如#if,#define;插入头文件到#include处,可使用递归的方式进行处理;删除所有的注释,添加行号和文件名标识符;保留所有的#pragma编译指令。
- 经过预处理后,得到的是与处理文件(hello.i),它还是一个可读的文本文件,但是不包含任何宏定义。
-
编译(compile time)时:将源代码翻译成机器代码。编译的过程就是将得到的预处理文件进行词法、语法、语义分析、生成汇编代码文件,经过编译后的代码文件还是可读的文件,但是CPU无法理解执行它。
-
汇编时:
- 汇编文件代码由汇编指令构成;汇编器(as)用来将汇编语言程序转换成机器指令序列。
- 汇编指令和机器指令都属于机器指令,所构成的程序称为机器及代码。
- 汇编的结果是一个可重定位目标文件,包含的结果是不可读的二进制代码。
-
链接:将多个可重定位的目标文件合并以生成可执行目标文件。
- 汇编的语言:用助记符表示操作码,用符号表示位置,用助记符表示寄存器。
- 链接的操作步骤:确定符号引用关系(符号解析);合并相关.o文件;确定每个符号的地址(优点:模块化,分成多个源程序文件;效率高,可分开编译);在指令中填入新地址。
在gcc 中的执行方法:
1.预处理:hello.c变成helloc.i(命令gcc -E或cpp)
2.编译:hello.i变成hello.s(命令gcc -0g -S)
3.汇编:hello.s变成hello.o(gcc或者as)
4.链接:hello.o+所需静态库——hello(gcc或ld)
链接的本质是什么呢?
从图中可以看出连接的本质就是合并相同的节,在合并之前还要进行符号解析
符号解析
- 符号分为强符号和弱符号
- 强符号:函数名和已初始化的全局变量名是强符号
- 弱符号:未初始化的全局变量名是弱符号,外部符号引用函数声明
- 在Linux链接器中使用下面的规则来处理多重定义的符号名:
- 规则1:不允许有多个同名的强符号;
- 规则2:如果有一个强符号和多个弱符号同名,那么选择强符号;
- 规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
- 多重定义符号的处理规则:强符号只能被定义一次,否则链接错误。
知道了这些知识后,那我们来具体看看在Linux中是怎么样的吧
//mismatch-main.c
long int x; /* Weak symbol */
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("%ld\n", x);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
//文件mismatch-variable
/* Global strong symbol */
double x = 3.14;
- 1
- 2
第一段代码中定义了一个长整型的弱符号x,并且没有赋予初值,在第二段代码mismatch-variable中定义了一个长整型的强符号x,并赋值为3.14。
在Linux平台下输入gcc -Wall -Og -o mismatch mismatch-main.c mismatch-variable.c
可得到一个重定位目标文件mismatch
这时我们再输入./mismatch可得到一个结果为4614253070214989087
那为什么是这个结果呢?
是因为强符号x=3.14,而浮点数在计算机中的存储、运算、表示等是按照IEEE754标准。
在命令gcc -Wall -Og -o mismatch mismatch-main.c mismatch-variable.c中
- -Wall是表示允许发出gcc提供的所有有用的报警信息
- -Og是表示启用全局优化
- -o是表示设定输出文件名,不加此选项会默认可执行文件名为a.out
- 此外,这条命令也可以将.o文件链接到可执行目标文件。
//global.h
extern int g;
int f();
- 1
- 2
//global-c1.c
#include "global.h"
int f() {
return g+1;
}
- 1
- 2
- 3
- 4
//global-c2.c
#include <stdio.h>
#include <stdlib.h>
#include "global.h"
int g = 0;
int main(int argc, char *argv[]) {
if (argc >= 2) {
g = atoi(argv[1]);
}
printf("g = %d. f() = %d\n", g, f());
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在Linux下输入gcc -Wall -Og -o global global.h global-c1.c global-c2.c后再输入./global可得到如下结果
在这里是定义了一个int型的全局变量g,并且赋予了初值0,所以执行出来的结果是没有问题的。
与静态库连接
所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(static library),它可以用来做链接器的输入。
库函数模块是许多的函数如printf,scanf,sqrt等函数都无需自己再去写,只要从共享的库函数中调用。
在Linux系统中,静态库以一种存档(archive)的特殊文件格式存放在磁盘中。存档文件时一组连接起来的可重定位目标文件的集合,有一个头不用来描述每个成员目标文件的大小和位置。存档文件名由后缀 .a标识,这样可以增强链接器功能,时期工呢过通过查找一个或者多个库文件中的定义符号来解析符号。
要创建这些函数,我们需要使用AR工具
ar (归档程序)能将制定的.o文件打包生成静态库文件。
接下来看一个例子
/* addvec.c */
/* $begin addvec */
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];
}
/* $end addvec */
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
/* multvec.c */
/* $begin multvec */
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];
}
/* $end multvec */
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
将这两段代码打包到libvector库函数中
通过使用
- gcc -c addvec.c multvec.c
- ar rcs libvector.a addvec.o multvec.o
/* main2.c */
/* $begin main2 */
#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;
}
/* $end main2 */
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
在main2.c 代码段中包含了头文件vector.h,定义了libvextor.a中例程的函数原型
接下来输入
- gcc -c main2.c
- gcc -static -o prog2c main2.o ./libvector.a
-
./prog2c
可得到以下结果:
链接的过程如下:
重定位
链接器完成符号解析后就可以进行重定位了。重定位由两部分组成:
- 重定位节和符号定义,链接器将所有相同类型的节合并为同一个类型的新的聚合类。
- 重定位节中的符号引用,在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。执行这一步,链接器主要依赖于可重定位目标模块中称为重定位条目(relocation entry)的数据结构。
可重定位目标文件
- .text:已编译程序的机器代码
- .rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表
- .data:已初始化的全局和静态C变量,局部C变量保存在栈中
- .bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量
- .symtab:符号表,存放在程序中定义和引用的函数和全局变量的信息
接下来我们看一下代码中的符号
#include <stdio.h>
int time;
int foo(int a) {
int b = a + 1;
return b;
}
int main(int argc, char *argv[])
{
printf("%d\n", foo(5));
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
在Linux环境中敲readelf -s symbols.o
可以得到
- 可以知道的是foo,main为全局变量,并且他们都为函数,所以为强符号。
foo是占4个字节,因为它是int型 - printf是没有类型的,它是全局变量,UND应该表示的是未定义的
- time 也是占4个字节,是int型的变量,它也是全局变量,然后它是COM,这个是表示它是未初始化的全局变量。
- 同时代码中还有很多的局部变量。
objdump -dx main.o
- -d表示将代码段反编译
- -x显示所有的可用头信息,包括符号表、重定位入口,使用此选项可以将文件内容做标注,方便我们查看理解
这个是可重定位文件而不是可执行目标文件,所以它的起始地址为0。在节部分,大部分节名都有对应,其中File off指明了每个节在ELF中的偏移地址,不是实际地址。Size指明了每个节所占大小,例如.data节的地址为0x00000054加上长度0x00000008就为0x0000005c,即.bss节的地址。
objdump -dx -j .data main.o - -j name显示指定名称为nane的section的信息,
这里在main.o的反汇编里仅显示。.data节信息
运用该指令我们只会看到.data节中的信息,最后反汇编.data节里存放的全局变量array,由于它已被初始化,且机器为小端模式,因此,00000000 <array>: 0: 01 00 00 00 02 00 00 00
我真的太垃圾了,这个作业拖了一个月才完成。之前刚学的时候是真的不会,也不知道老师给的代码要怎么样在Linux下去运行,这两天看了很多的别的同学写的日志,自己经过琢磨,实践,终于完成了。写完这些东西发现对知识又有了进一步的了解,毕竟这两天都在和它打交道。总之,自己还是太差劲了,还需要很努力很努力!
该文章中的许多信息来源于《深入理解计算机系统》,前两张图片来源于南京大学袁春风老师课件,有一张图片来源于老师所给的PPT,部分内容参考于https://blog.csdn.net/ziyonghong/article/details/101560077
https://blog.csdn.net/angranxueer/article/details/102236976
加一点东西
在hello.c文件中
#include <stdio.h>
int main()
{
printf("Hello World!");
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 用readelf -S hello.o查看
- 用readelf -h hello.o查看
- 用readelf -s hello.o查看
</div>
<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-b6c3c6d139.css" rel="stylesheet">
</div>
推荐阅读
-
php关键词替换的类(避免重复轮换,保留与还原原始链接)
-
浅析PHP关键词替换的类(避免重复替换,保留与还原原始链接)_php实例
-
php将url地址转化为完整的a标签链接代码_PHP教程
-
ubuntu 12.10 默许安装php5-fpm无监听9000端口,nginx无法链接php5-fpm修正
-
CodeIgniter超链接传递参数,该如何处理
-
为什么链接图片的地址后边 出现一个@字符 ?传到七牛的key是:/public/place/theme/1444717373
-
一个链接的问题大家帮忙解析下!
-
为什么链接可以直接打开 放到链接里就不行了_html/css_WEB-ITnose
-
注册表中存储数据库链接字符串的方法
-
drupal修改显示菜单链接地址解决办法