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

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

  • 内存管理
    • 直接使用物理内存地址
    • 虚拟内存-分段
    • 虚拟内存-分页
    • 分页和分段的主要区别
    • 段页式
  • 代码生成过程
    • 预处理
    • 编译
      • 词法分析
      • 语法分析
      • 语义分析
      • 源代码优化
      • 代码生成
      • 目标代码优化
    • 汇编
    • 链接
  • 目标文件
    • 格式
  • 静态链接
    • 静态链接bash
    • 生成静态库
    • 分配空间和地址
    • 符号解析与重定位
  • 动态链接
    • 与静态链接对比
    • 生成共享库
    • 动态链接过程
      • 相关技术
        • 新增存储段
        • 地址无关代码(PIC,posion-independent code)
        • 延迟绑定
      • 1.动态链接器自举
      • 2.装载共享对象
      • 3.地址重定位和初始化
      • 4.控制权转交
  • 函数调用
    • 调用过程压栈
    • 返回值传递
  • linux 进程堆管理
    • 两种堆空间的分配方式
      • brk()
      • mmap()
    • 堆分配算法
      • 空闲链表
      • 位图
      • 对象池
    • molloc底层调用
  • 系统调用
    • 概念
    • 系统调用原理
      • 中断

内存管理

直接使用物理内存地址

程序员的自我修养—链接、装载与库 笔记_第1张图片

缺点:

  1. 地址空间不隔离
  2. 内存使用率低
  3. 程序运行地址不确定

虚拟内存-分段

以程序为单位,对内存进行映射,如将A的0-1G地址对应内存中某个1G的内存,可以做到隔离和地址确定,但内存使用率低。
如果内存不足,换出的是整个文件。根据程序局部性原理,一个程序运行时,某个时间段只用到了一小部分数据,大部分数据都没用到,需要更小的粒度分割和映射

虚拟内存-分页

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

分页和分段的主要区别

相同点:
采用离散分配方式,通过地址映射机构实现地址变换
不同点:

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

段页式

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

代码生成过程

预处理

gcc -E hello.c -o hello.i

程序员的自我修养—链接、装载与库 笔记_第2张图片
在这里插入图片描述

编译

gcc -S hello.i -o hello.s

源代码:
在这里插入图片描述

词法分析

源程序送入扫描器,运用有限状态机简单进行词法分析,输出一系列token,例如:
程序员的自我修养—链接、装载与库 笔记_第3张图片
生成的词一般分为:关键字,标识符,字面量(数字 字符串等),特殊符号

语法分析

对扫描器产生的记号进行语法分析,通过上下文无关语法,生成以表达式为节点的语法树,
程序员的自我修养—链接、装载与库 笔记_第4张图片
上下文相关语法:
是乔姆斯基(Chomsky, N.)引进的.设G=(V,T,P,s)为一个短语结构文法,若限定式中的所有产生式a->b都满足下列条件:b的长度不小于a之长度.则称G为上下文相关文法.由上下文相关文法产生的语言称为上下文相关语言.上下文相关语言都是递归的,但反之不然.
上下文无关语法:
上下文无关文法(英语:context-free grammar,缩写为CFG),在计算机科学中,若一个形式文法G = (N, Σ, P, S) 的产生式规则都取如下的形式:V->w,则谓之。其中 V∈N ,w∈(N∪Σ)* 。上下文无关文法取名为“上下文无关”的原因就是因为字符 V 总可以被字串 w 自由替换,而无需考虑字符 V 出现的上下文。一个形式语言是上下文无关的,如果它是由上下文无关文法生成的(条目上下文无关语言)。

语义分析

语法分析仅仅完成了对表达式的语法层面的分析,不了解语句是否真的有意义,例如C语言两个指针相乘,合法但无意义。语义分析由语义分析器完成,编译器能完成的是静态语义,即在编译期间能确定的语义信息,相对于的动态语义只有在运行期才能确定的语义。
程序员的自我修养—链接、装载与库 笔记_第5张图片
经过语义分析,整个语法树的表达式被标识了类型。
程序员的自我修养—链接、装载与库 笔记_第6张图片

源代码优化

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

代码生成

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

目标代码优化

对目标机器代码进行优化,比如选择合适的寻址方式,比如使用移位来代替乘除法,删除多余的指令,等等。最终的结果是汇编代码,但此时的一些变量的地址仍未确定,需要等等链接确定。

汇编

gcc -c hello.s -o hello.o

程序员的自我修养—链接、装载与库 笔记_第7张图片

链接

例如A中引用了B的fun函数,在编译A时并不知道fun的地址,需要将fun地址在链接的时候填入。(如何知道这是外部的地址?通过头文件。具体的代码在什么地方?在B文件生成的库文件里。)
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。链接是由叫链接器(linker)的程序自动执行的。

目标文件

格式

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

程序员的自我修养—链接、装载与库 笔记_第8张图片程序员的自我修养—链接、装载与库 笔记_第9张图片
ELFheader。描述整个文件基本属性,如ELF文件版本,目标机器型号,程序入口地址等。
程序员的自我修养—链接、装载与库 笔记_第10张图片

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

symtab 符号表 链接过程即不同文件间函数和变量地址的引用,对于不同的符合,将其存储至符号表便于链接。
.init与.finit 程序初始化与终结代码段,可以用于C++的析构和初始化等

程序员的自我修养—链接、装载与库 笔记_第12张图片

静态链接

静态链接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

分配空间和地址

在这里插入图片描述
会输出全局符号表,这里的映射是与虚拟内存地址的映射,但此时还是没加载到内存的,不考虑生成的磁盘上的可执行文件的内部存储

符号解析与重定位

在这里插入图片描述
依据ELF文件的重定位表和第一步输出的全局符号表对需要重定位的数据或代码进行查找和重定位(偏移量),通过绝对地址修正或相对地址修正进行地址改写。

动态链接

与静态链接对比

  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选项可以得到使用地址无关码的共享对象

对于内部数据/函数的访问,直接通过相对寻址方式在运行前即可确定
对于外部数据/函数,往往意味着对全局变量的访问,对于在全局对象中定义的变量来说,这些符号的地址与模块装载地址有关,elf使用GOT(全局偏移表)来对这些变量进行间接引用需要在装载时确定。称之为全局偏移表(Global offest Table)。
程序员的自我修养—链接、装载与库 笔记_第13张图片
模块在编译时,可以确定内部变量相对于当前指令的偏移,即编译时可以确定GOT的偏移。在加载时,动态链接器会重定位GOT中的每个条目,使他们指向正确的地址

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

延迟绑定

即使使用了地址无关代码,动态链接仍存在问题:

  1. 对于全局/静态/模块间的数据/函数都需要复杂的GOT定位,耗时。
  2. 运行前链接的过程中,会寻找所有的共享对象和函数,耗时。

为了解决第二个问题,提出了延迟绑定,使用PLT(Procedure Linkage Table,过程链接表)实现,基本思想是当函数第一次被用到时才进行绑定(符号查找,重定位等),没有用就不绑定。实现思路是在GOT前再加一层间接跳转,调用函数不通过GOT直接跳,而是通过PLT,PLT记录了每个函数对应的地址。
PLT简单实现,假设调用 bar() 函数,在PLT中存在与其对应的bar@plt。
bar@plt初始时存储的是下一条指令的地址,所以初始时会进行一次绑定,之后直接跳转到调用函数的地址。

bar@plt:
jmp *(bar@GOT)         		//如果是第一次链接,该语句的效果只是跳转到下一句指令。否则,将会跳转到 bar()函数对应的位置
push n				//压栈 n,n 是 bar 这个符号在重定位表 .rel.plt 中的下标
push moduleID           	// 压栈当前模块的模块ID,上述例子中的 liba.so
jump _dl_runtime_resolve()   	//跳转到动态链接器中的地址绑定处理函数

在这里插入图片描述
ELF将GOT拆为了两个表叫做“.got”,“.got.plt”。其中 .got 用来保存全局变量的引用地址,.got.plt 用来保存函数引用的地址,也就是说,所有对于外部函数的引用被分离到了 .got.plt 表。

plt表的前三项:

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

1.动态链接器自举

动态链接器是所有程序运行时的代码入口,动态链接器本身也是一个共享对象,但它不能依赖于其他对象。
程序员的自我修养—链接、装载与库 笔记_第14张图片

2.装载共享对象

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

3.地址重定位和初始化

装载共享对象完成后,链接器开始遍历可执行文件和各个共享对象的重定位表,将GOT/PLT的内容进行修正,之后依据地址无关代码和延迟绑定进行重定位。

4.控制权转交

在这里插入图片描述

函数调用

调用过程压栈

通过两个寄存器来实现:sbq
程序员的自我修养—链接、装载与库 笔记_第15张图片
程序员的自我修养—链接、装载与库 笔记_第16张图片
程序员的自我修养—链接、装载与库 笔记_第17张图片
抽象:
程序员的自我修养—链接、装载与库 笔记_第18张图片
程序员的自我修养—链接、装载与库 笔记_第19张图片

返回值传递

eax寄存器存储返回值。但eax本身只有四字节,若大于四字节,则在调用函数前的函数栈内申请temp中间内存,在调用函数内部将得到的结果拷贝到temp中,之后返回后将temp的内存拷贝到返回的结果中。需要两次拷贝。
程序员的自我修养—链接、装载与库 笔记_第20张图片

linux 进程堆管理

两种堆空间的分配方式

brk()

程序员的自我修养—链接、装载与库 笔记_第21张图片

mmap()

初始 mmap 系统调用可以将用户空间的虚拟内存地址与文件进行映射(绑定),对映射后的虚拟内存地址进行读写操作就如同对文件进行读写操作一样。不映射到文件则可以用作堆空间。
在这里插入图片描述
在这里插入图片描述

堆分配算法

空闲链表

程序员的自我修养—链接、装载与库 笔记_第22张图片

位图

程序员的自我修养—链接、装载与库 笔记_第23张图片
程序员的自我修养—链接、装载与库 笔记_第24张图片

对象池

程序员的自我修养—链接、装载与库 笔记_第25张图片

molloc底层调用

程序员的自我修养—链接、装载与库 笔记_第26张图片

程序员的自我修养—链接、装载与库 笔记_第27张图片

系统调用

概念

为了让应用程序访问操作系统的资源或借助操作系统完成相应行为,操作系统为应用程序提供一些接口供其使用。
在这里插入图片描述

系统调用原理

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

中断

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

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

由于中断号宝贵,所有多个系统调用的接口都是使用同一个80中断号。如何区分不同的系统调用?通过EAX寄存器,EAX寄存器中断调用前可以传递系统调用号,调用结束后可以传递返回结果。
程序员的自我修养—链接、装载与库 笔记_第28张图片

部分系统调用号:
在这里插入图片描述
程序员的自我修养—链接、装载与库 笔记_第29张图片

你可能感兴趣的:(c++,编译原理,操作系统)