CSAPP读书笔记3


前言

感觉一些硬件知识读着还是吃力,想着先把基本的数据结构学会,再去讨论算法
计划是这个学期把CSAPP读完必须看的章节,假期把数据结构和算法过一下,然后再二刷,黑皮书是真的硬核。。


一、编译器驱动

我们已经编辑好了main.c和sum.c

#include<stdio.h>

int array[2] = {1,2};
int main()

{

    int val = sum(array,2);
    return val;
}

sum.c的函数源码

int sum(int* a,int n)
{
    int i,s = 0;

    for(i = 0;i < n; i++)
    {
        s += a[i];
    }
    return s;

}

在这里插入图片描述
在这里插入图片描述

利用编译命令gcc -Og -o prog main.c sum.c调用驱动程序
cpp做为预处理器,将main.c翻译成一个ASCII码的main.i的文件,这是main.i的一小部分文件内容
用命令gcc -E main.c -o main.o可以单独进行预处理
在这里插入图片描述
在这里插入图片描述

ccl作为C编辑器,将main.i翻译成一个ascii汇编文件main.s
用命令gcc -S main.i -o main.s 或者gcc -S main.c -o main.s都行
之前提到过,就是一堆汇编
as作为运行编辑器,将main.i变成可重定位文件main.o,用objdump可以打开
最后通过链接器ld,将之前处理的main.o,sum.o以及其他文件组合起来,形成一个可执行文件progld main.o sum.o -o prog
输入./prog可以直接运行程序

二、目标文件

可执行文件

就是我们链接产生的文件

可重定位目标文件

有很多个节组成,比如.text .rodata .data .bss印象之前有说过,这里总结一下之前没有提到过的section
.symtab:就是之前提到的符号表,存放程序引用的函数和全局变量的信息,不包含局部变量
.rel.text调用外部函数时,需要进行修改,也就是和其他文件组合时才被修改,调用本地函数不用修改
.rel.data:被引用后或定义的所有全局变量的重定位信息
.debug:调试符号表,定义局部变量和类型定义
.line:原始C

三、静态链接

上面的ld就是链接器,输入是一组可重定位的目标文件以及命令行参数,输出就是可执行文件
输入的就是各种不同的代码,数据section表示
而链接器需要做两件事:
1.符号解析。文件的引用符号,每一个符号需要对应局部变量,全局变量,参数。实际上就是我们的变量,定义的数据等
2.重定位。编译器和汇编器生成一些地址从0开始的代码和数据,链接器通过之前的符号定义与一个虚拟内存位置连接,从而重新定位这些编译器和汇编器生成的节。使得他们指向内存的位置。实际上就是把地址给到,比如一个汇编jmp L0,这个L0是另外一个代码片段,CPU开始遇到L0不知道是哪里,所以开始的时候需要进行符号解析,解析过后知道了这个L0是我们已经定义过的位置,在进行分页后就定位到了虚拟地址

符号表

每一个可重定位文件的目标都有一个符号表,它包含这个目标定义或者引用的符号信息,有三种分类
(1)由这个目标定义的,并且其他可重定位文件也可以引用的,对应于非静态函数和全局变量,全局符号类型
(2)由其他的可被重定位目标定义,并且被引用的,外部符号类型
(3)被模块定义和引用的局部符号,带static属性,只能在定义的目标中引用,不能被其他的引用
而符号表就是汇编器构造的

common伪节里存储的是一个外部模块定义的,但是没有放初始值的全局变量,只有可重定位有伪节
比如有以下几个文件
这是main.c

void swap();

int buf[2] = {1,2};
int main()
{
    swap();
    return 0;
}

这是swap.c

extern int buf[];

int* bufp0 = &buf[0];
int* bufp1;
void swap()
{
    int temp;
    bufp1 = &buf[1];
    temp = *bufp0;
    *bufp0 = *bufp1;
    *bufp1 = temp;
}

对于swqp.c来说,bufp1,bufp0,swap都是全局类型的符号,bufp0,bufp1,swap都是在swap.o中定义的,buf在main.o中定义,其中buf,bufp1,bufp0,swap都在symtab条目,buf和bufp0在data段,bufp1在common段,swap在.text段

符号解析规则

分为强符号和弱符号
强符号:函数和已经初始化的全局变量是强符号,未初始化的全局变量是弱符号
规则:不能有同名强符号,如果一个强符号和多个弱符号同名,那么选择强符号。如果有多个弱符号同名,从这些弱符号选一个

静态库链接

静态库就是定义了一些函数,这样main就可以直接调用,比如printf和scanf,就是标准的I/O函数
如果缺失了,可以用命令来编译gcc main.c /usr/lib/libc.o,比如在main里调用printf,就会从libc.a里面调用一个已经定义了printf函数的printf.o来执行,libc.a是我们的默认库,一般放在最后,这里gcc命令上是有顺序的
这里注意一点,如果一个libx.a中引用一个被liby.a定义的目标,那这个libx.a要放到liby.a之前
比如:

gcc main.o libx.a liby.a
gcc main.o libx.a liby.a libx.a

第一行这样写的原因就是main中引用了libx.a定义的目标,且liby.a中也定义了libx.a引用的目标
第二行就是main.o引用了libx.a中定义的,libx.a中引用了liby.a中定义的,且liby.a也引用了libx.a定义的,libx.a也引用了main.o定义的,也就是闭环了。
符号解析按照U\E\D来分类,其中如果在main中没找到,放进U等待下一步匹配,E就是匹配后需要进行链接的.o文件,D就是已经解析后的符号,最后符号解析结束后就会返回E

重定位

将E中相同节合并,比如数据和数据合并数据节,代码合并代码节,后对D中定义的符号进行重定位,比如为函数确定地址,进而确定每条指令的地址,然后对引用符号进行重定位,比如add B和jmp L0,不知道B和L0是哪,在汇编器读的时候不知道这是什么,所以就先把B和L0分别放在rel.data和rel.text里面去
下面就是重定位条目
typedef struct
{
int OFFSET的意义:要修改的位置在.text节的偏移量
TYPE:重定位类型
VALUE:重定位符号的名称
ADDEND:其实就是修饰的一个量,如果是PC引用就可以让地址指向EIP(call的地址加四)
}
将所有相同类型的节合并为一个节,然后链接器把运行时的内存地址赋值给新的聚合节,这样,程序中每条指令和全局变量都有唯一进行的内存地址了,相当于把多个目标文件的.text和.data聚合到可执行文件的.text和.data
这是一开始的main.o和sum.o组成的prog的objdump的反汇编
在这里插入图片描述
一开始,这个call前面是e8 00 00 00 00,这个05就是后面通过重定位算法得到的,而在运行时,如果是PC寻址,就将PC压入栈,而这个入栈的PC值就是614(call的下一条),614加上上面的5的值给到PC,所以下一步就是619,也就是sum的地址了,也就是当前EIP地址+X = 地址,这就是重定位PC相对引用
上面的lea那一行,对array进行操作一开始也是没有3d060a20的,就是00000000也是通过重定位算法,得到运行时的地址,把000000改成那个地址这是重定位的绝对引用

可执行文件

是用execve系统调用来调用加载器加载,将磁盘上的数据拷贝到虚拟空间中,先执行entry_start,最终执行main‘函数,执行execve中如果找不到execve中的filename就返回-1了,成功执行最终控制权给main
execve是当前进程上下文中加载并运行一个新程序。加载时在shell命令输入./hello,调用fork函数,创建一个shell和父进程完全相同,调用execve
可执行文件具有.data和.text段,这些是多个可重定位文件结合到一起的段,因为已经进行过重定位,所以在可执行文件中,没有.rel段,其中·.init段,.text段.rodata段都是只读内存段,而.data和.bss都是读写数据段,在寄存器空间中,如图所示
在这里插入图片描述
在加载可执行文件时,代码段总是从0x400000开始,后面就是数据段,运行时堆在数据段后,调用malloc函数往上增长,虽然链接器会使用ASLR,但是相对地址还是不变的
在这里插入图片描述
这图上面写错了嗷,应该是只读代码段

四、动态链接

共享库

如果使用静态链接,每次比如调用printf以及scanf函数的时候,这些函数复制到每个进程中的文本段,会造成很大的浪费,共享库在运行加载时,可以加载到任意的内存地址,并且可以和一个在内存中的程序链接起来。这个过程就是动态链接,由动态链接器完成,共享库也叫做共享目标,在linux系统中通常用.so后缀表示,在win下就是.dll
运行的时候可以被加载,文件只有一个备份,进行动态链接
其实,整个流程就是链接阶段,将.so进行链接,运行的时候把.so和execve函数给到动态链接器执行,也就是动态完成链接过程,此时,没有代码和数据被复制到可执行文件中,而是链接器复制一些重定位和符号表信息。比如prog遇到需要libc.so的时候,加载一下libc.so,然后链接的时候进行符号解析和重定位就行了
如果加载器遇到动态链接信息时,会加载运行链接器,然后动态链接器通过执行重定位完成链接,重定位libc.so的文本和数据,重定位libvector.so到另一个内存段,重定位可执行文件对libc.so和libvector.so引用
用addvec.c和multvec.c创建一个叫libvector.so的库:gcc -shared -fpic -o libvector.so addvec.c multvec.c
把这个库和main.c链接:gcc -o prog21 main.c ./libvector.so
有个新的解释,就是可以先和静态库默认库进行不完全的链接,此链接为静态链接,因为加载不是.a是.so只是把符号表信息和重定位信息加载到可执行文件中。文件执行调用加载器,加载器调用动态链接器,然后和动态库和默认库进行链接,就进行重定位了,但不会放到磁盘上去,只有前面静态链接的可执行文件去磁盘,就执行第一条指令

位置无关代码

gcc 编译生成共享库有个选项 -fpic
这样生成的就是位置无关代码,也就是是位置不确定,还有就是这个共享库即使大小被修改,调用程序可以不管,下次动态链接的时候这个程序就调用的是一个新的共享库代码
通常在共享库种分配一个专用地址空间片,要求加载器总是在这个地址加载共享库,但是如果一个进程不使用这个库,这个空间就被浪费了
而现代系统使用PIC进行加载,这可以让他们加载到内存任何地址,上面的fpic参数就是使用PIC代码
,指令和数据间的相对距离是不变的,因此,指令和变量之间的距离是一个运行时常量,与绝对内存地址无关。
PIC代码共享库就可以被无限多个进程引用

总结

关于链接与库的知识

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值