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

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

内存管理

直接使用物理内存地址

缺点:

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

虚拟内存-分段

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

虚拟内存-分页

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

分页和分段的主要区别

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

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

段页式

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

代码生成过程

预处理

gcc -E hello.c -o hello.i


编译

gcc -S hello.i -o hello.s

源代码:

词法分析

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

生成的词一般分为:关键字,标识符,字面量(数字 字符串等),特殊符号

语法分析

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

上下文相关语法:
是乔姆斯基(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语言两个指针相乘,合法但无意义。语义分析由语义分析器完成,编译器能完成的是静态语义,即在编译期间能确定的语义信息,相对于的动态语义只有在运行期才能确定的语义。

经过语义分析,整个语法树的表达式被标识了类型。

源代码优化

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

代码生成

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

目标代码优化

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

汇编

gcc -c hello.s -o hello.o

链接

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

目标文件

格式

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


ELFheader。描述整个文件基本属性,如ELF文件版本,目标机器型号,程序入口地址等。

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

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

静态链接

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

模块在编译时,可以确定内部变量相对于当前指令的偏移,即编译时可以确定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.动态链接器自举

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

2.装载共享对象

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

3.地址重定位和初始化

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

4.控制权转交

函数调用

调用过程压栈

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



抽象:

返回值传递

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

linux 进程堆管理

两种堆空间的分配方式

brk()

mmap()

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

堆分配算法

空闲链表

位图


对象池

molloc底层调用

系统调用

概念

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

系统调用原理

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

中断

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

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

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

部分系统调用号:

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

  1. 【《程序员的自我修养---链接装载于库》读书笔记】可执行文件的装载与进程

    系列文章目录 [<程序员的自我修养-链接装载于库>读书笔记]初探ELF [<程序员的自我修养-链接装载于库>读书笔记]windows PE/COFF [<程序员的自我修养 ...

  2. 《程序员的自我修养-链接-装载与库》第三章 目标文件里有什么(1)

    目录 0.引言 1.目标文件的格式 1.1 目标文件的格式及ELF文件格式的文件的分类 1.2 目标文件与可执行文件格式的小历史 2.目标文件是什么样的 2.1 程序与目标文件简介 2.2 BSS历史 ...

  3. 【读书笔记】【程序员的自我修养 -- 链接、装载与库(三)】函数调用与栈(this指针、返回值传递临时对象构建栈、运行库与多线程、_main函数、系统调用与中断向量表、Win32、可变参数、大小端

    文章目录 前言 介绍 内存 内存布局 栈与调用惯例 堆与内存管理 运行库 入口函数和程序初始化 C/C++运行库 运行库与多线程 C++全局构造与析构 fread 实现 系统调用与API 系统调用介绍 ...

  4. 《程序员的自我修养—链接、装载与库》pdf书签,目录分享

    在网上下载到<程序员的自我修养-链接.装载与库>pdf版本,拜读之后受益匪浅,但是因为下载的pdf没有书签,所以想要查找某一章的内容不是很方便,于是自己制作了一下书签文件,将书签文件导入p ...

  5. 【读书笔记】【程序员的自我修养 -- 链接、装载与库(二)】进程虚拟地址空间、装载与动态链接、GOT、全局符号表、共享库的组织、DLL、C++与动态链接

    文章目录 前言 介绍 可执行文件的装载与进程 进程虚拟地址空间 装载方式 操作系统对可执行文件的装载 进程虚存空间分布 ELF文件的链接视图和执行视图 堆和栈 Linux 内核装载ELF & ...

  6. 《程序员的自我修养--链接、装载与库》笔记

    写在前面:本文是我在阅读<程序员的自我修养–链接.装载与库>一书时做的笔记,所谓好记性不如烂笔头嘛,其中主要摘抄记录了本人着重阅读的章节:除此之外还有小部分本人对书中内容的个人理解.以及文 ...

  7. 《程序员的自我修养--链接、装载与库》学习笔记(一)

    本系列文章是<程序员的自我修养–链接.装载与库>(电子工业出版社)一书的学习摘录笔记,本文是书中1.1至1.4部分. 文章目录 基础概念 硬件 软件 基础概念 #include <s ...

  8. 程序员的自我修养——链接、装载与库 笔记(一)

    程序员的自我修养   悄咪咪的说一句,这篇文章可能需要对计算机有过系统的学习,不然看着可能一脸懵.如果有疑问的话,当然,很可能是我太菜了,写的不好,欢迎大家评论区留言指教!此笔记只是刚刚开始,后续我会 ...

  9. 程序员的自我修养--链接、装载与库笔记:总结

    <程序员的自我修养----链接.装载与库>这本书是2009年出版的,书中有些内容的介绍可能已经过时,已不再适用于现在的C/C++开发,而且书中展示的结果均是在32位机上进行的操作,这里全部 ...

最新文章

  1. spring访问oracle数据库表,Spring访问oracle数据库配置步骤
  2. harbor pull 失败
  3. Flink 在唯品会的实践
  4. 数学是理工基础,如何才能令人信服?
  5. Qt undefined reference to,Error 255,找不到库函数的解决办法
  6. java 嵌套事务_Java事务以及嵌套事务
  7. keras padding_GAN整体思路以及使用Keras搭建DCGAN
  8. n步自举法:时序差分方法与蒙特卡洛方法的结合
  9. python用户输出怎么命名变量_python变量及用户交互,用户名格式化输出
  10. Java集合框架详解笔记及其代码
  11. 100以内的勾股数python_常见100以内勾股数
  12. 罗技craft键盘使用入门
  13. 信息收集-CDN绕过
  14. AWS 亚马逊云良好架构框架
  15. cpu第几代计算机,赛扬G系列有几代cpu分别是
  16. 关于async await 等任务队列插话
  17. 跨模态行人重识别:RGB-Infrared Cross-Modality Person Re-Identification(2017 ICCV)
  18. 图像的低频,中频,高频信息含义?
  19. 联想教育应用使用说明(7.6版本)——第0章 目录
  20. 登录路由器显示服务器拒绝访问,ie浏览器拒绝访问原因如何解决

热门文章

  1. 【渗透测试笔记】之【被动信息收集 】
  2. 矩阵A^TA(A'A)和AA^T(AA')的性质
  3. python模拟用户登录注册定义函数user_python模拟登录通达信
  4. python公司基本面_Python读年报12-获得公司财务基本面信息
  5. 【网络教程】群晖中如何安装甜糖,手把手教您Docker安装甜糖,如何解决甜糖安装使用中遇到的问题(已解决)
  6. C程序设计进阶week6(指针3)
  7. delphi JSON用法
  8. build path功能详解 在项目上右键》Build path》Config build path
  9. VC编程工具的灵活使用实验报告
  10. 【宇宙编码】厉害,生日那天:程序员小哥把天上的宇宙星云都送给女神了~