您的当前位置:首页正文

程序员的自我修养—链接、装载与库 笔记

2024-11-29 来源:个人技术集锦

内存管理

直接使用物理内存地址

缺点:

虚拟内存-分段

虚拟内存-分页

对程序的数据和代码段进行分割,常用的放到内存,不常用的扔在磁盘,需要的时候放入内存。

分页和分段的主要区别

  1. 页是信息的物理单位,分页是为了满足系统的需要;段是信息的逻辑单位,含有意义相对完整的信息,是为了满足用户的需要。
  2. 页的大小固定且由系统确定,由系统把逻辑地址分为页号和页内地址,由机器硬件实现;段的长度不固定,取决于用户程序,编译程序对源程序编译时根据信息的性质划分。
  3. 分段系统的一个突出优点是易于实现段的共享和保护,允许若干个进程共享一个或多个分段,,且对段的保护十分简单易行。分页系统中虽然也能实现程序和数据的共享,但远不如分段系统方便。

段页式

段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。

代码生成过程

预处理

gcc -E hello.c -o hello.i

编译

gcc -S hello.i -o hello.s

源代码:

词法分析

源程序送入扫描器,运用有限状态机简单进行词法分析,输出一系列token,例如:

语法分析

对扫描器产生的记号进行语法分析,通过上下文无关语法,生成以表达式为节点的语法树,

语义分析

语法分析仅仅完成了对表达式的语法层面的分析,不了解语句是否真的有意义,例如C语言两个指针相乘,合法但无意义。语义分析由语义分析器完成,编译器能完成的是静态语义,即在编译期间能确定的语义信息,相对于的动态语义只有在运行期才能确定的语义。

源代码优化

优化一些在编译时期可以确定的表达式,比如2+6就被优化成8减少表达式数量,输出的结果是中间代码

代码生成

中间代码转化为目标机器代码,依赖于目标机器的配置,如字长,寄存器,整数类型 浮点类型等,目标机器代码可以是汇编形式。

目标代码优化

汇编

gcc -c hello.s -o hello.o

链接

目标文件

格式

ELF文件 executable linkable file,链接前的.o,链接后的可执行文件以及静态库动态库均是ELF格式

.rel.text text段的重定位表 对于每个需要重定位的代码段和数据段,在重定位表进行存储。
.rel.data data段的重定位表
.text 代码段
.data 数据段 已初始化的全局变量和静态变量
.comment 注释信息,例如编译器版本信息等
.rodata 只读数据,const变量和字符串常量等
.bss 未初始化的全局变量和局部静态变量
section table 段表 除头之外最重要的结构,描述每个段的信息,如段名,段的长度,文件中的偏移,读写权限及段的其他属性。编译器,链接器和装载器都是依据段表访问段。
strtab/shstrtab 字符串表/段字符串表

静态链接

静态链接bash

ld a.o b.o -e main -o ab

生成静态库

生成静态库,需要先对源文件进行汇编操作 (使用参数 -c) 得到二进制格式的目标文件 (.o 格式), 然后在通过 ar 工具将目标文件打包就可以得到静态库文件了 (libxxx.a)。

使用 ar 工具创建静态库的时候需要三个参数:

参数c:创建一个库,不管库是否存在,都将创建。
参数s:创建目标文件索引,这在创建较大的库时能加快时间。
参数r:在库中插入模块 (替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。

# hello.o生成libmyhello.a静态库
ar -crs libmyhello.a hello.o

分配空间和地址

符号解析与重定位

动态链接

与静态链接对比

  1. 空间太浪费。每个库文件在运行时在磁盘和内存上都有多个副本。
  2. 对程序的更新,部署,发布带来许多麻烦。比如某个库更新了,需要重新链接发布给用户。

动态链接就是将链接的过程推迟到运行时在进行。使得每个库文件在内存和磁盘上都只有一个副本,共用同一个库内存的好处是减少内存的换入换出,增加cpu缓存命中率,程序可扩展性和兼容性强。性能略有下降,但这点性能换来空间节省和灵活性,可接受的。

生成共享库

# a.c文件生成 a.so动态库
gcc -fPIC -shared -o liba.so a.c

#使用动态库
gcc main.c -o main -L ./ -la
#其中-L指明动态链接库的路径,-l后是链接库的名称,省略lib

动态链接过程

相关技术

新增存储段
  1. .interp 保存一个字符串,字符串表示的是动态链接器的路径,一般是/lib/ld-linux.so.2
  2. .dynamic 动态链接最重要的结构,保存动态链接需要的基本信息,如依赖于哪些对象,动态链接表符合表的位置,动态链接重定位表的位置,共享对象初始化代码的地址。
  3. .dynsym 动态链接符号表,只保存与动态链接相关的符号,一般静态链接的符号表.symtab包含.dynsym的内容。
  4. .dynstr 动态链接符号字符串表,与静态的strtab对应,往往还有.hash字符哈希表来进行辅助
  5. .got与.got.plt .got 用来保存全局变量的引用地址;.got.plt 用来保存函数引用的地址
  6. .rel.dyn与.rel.plt 动态链接重定位表,分别相当于静态的,rel.data与rel.text,前者对数据引用修正,修正的位置在.got及数据段;后者对函数引用修正,修正的位置在.got.plt
地址无关代码(PIC,posion-independent code)
  1. 可以加载而无需重定位的代码称为地址无关码
  2. gcc中使用-fPIC选项可以得到使用地址无关码的共享对象

对于全局/静态的数据,由于不能事先确定是否引用了外部的库,所以也使用GOT处理

延迟绑定
  1. 对于全局/静态/模块间的数据/函数都需要复杂的GOT定位,耗时。
  2. 运行前链接的过程中,会寻找所有的共享对象和函数,耗时。
bar@plt:
jmp *(bar@GOT)         		//如果是第一次链接,该语句的效果只是跳转到下一句指令。否则,将会跳转到 bar()函数对应的位置
push n				//压栈 n,n 是 bar 这个符号在重定位表 .rel.plt 中的下标
push moduleID           	// 压栈当前模块的模块ID,上述例子中的 liba.so
jump _dl_runtime_resolve()   	//跳转到动态链接器中的地址绑定处理函数

plt表的前三项:

  1. .dynamic 段的地址,dynamic指出了依赖的共享对象
  2. 本模块的 ID
  3. _dl_runtime_resolve()的地址

1.动态链接器自举

动态链接器是所有程序运行时的代码入口,动态链接器本身也是一个共享对象,但它不能依赖于其他对象。

2.装载共享对象

从ELF文件头和dynamic中得到依赖的所有共享对象集合,找到相应的共享对象映射到进程空间,若共享对象有依赖就将依赖的也放入集合中,整个装载的过程是广度优先搜索的过程。当对象被装载后,符号表会合并到全局符号表,当所有的共享对象都装载后,符号表包含所有符合。

3.地址重定位和初始化

4.控制权转交

函数调用

调用过程压栈

通过两个寄存器来实现:sbq



抽象:

返回值传递

eax寄存器存储返回值。但eax本身只有四字节,若大于四字节,则在调用函数前的函数栈内申请temp中间内存,在调用函数内部将得到的结果拷贝到temp中,之后返回后将temp的内存拷贝到返回的结果中。需要两次拷贝。

linux 进程堆管理

两种堆空间的分配方式

brk()

mmap()

堆分配算法

空闲链表

位图


对象池

molloc底层调用

系统调用

概念

为了让应用程序访问操作系统的资源或借助操作系统完成相应行为,操作系统为应用程序提供一些接口供其使用。

系统调用原理

进程运行时,有两种不同的特权级别,内核态和用户态。用户态程序通过中断从用户态切换到内核态。

中断

什么是中断?中断是一个硬件或软件发出请求,要求CPU暂停当前的手头工作转手去处理更加重要的事情。
中断具有两个属性,中断号中断处理程序,不同中断具有不同的中断号,内核有一个中断向量表,包含了指向指定中断号的执行函数的指针,中断到来,中断向量表查找相应代码,执行中断代码,之后返回继续原先工作。

中断有两类:硬件中断和软件中断。硬件中断包括电源掉电,键盘被按下等。软件中断通常是一条带有中断号的指令,用户可以手动的触发。在windows下,系统调用的中断号是int 0x2e;linux下是int 0x80。

由于中断号宝贵,所有多个系统调用的接口都是使用同一个80中断号。如何区分不同的系统调用?通过EAX寄存器,EAX寄存器中断调用前可以传递系统调用号,调用结束后可以传递返回结果。

部分系统调用号:

显示全文