程序员

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

2017-08-19  本文已影响192人  花与少年_
重点思考两个问题

一段源代码是怎么变成最后可执行的程序的
一个进程,在内存中是什么样的

一.预备知识
  1. 为了协调CPU、内存和高速的图形设备 -> 北桥 -> 有相对低速的设备连在北桥上 -> 南桥,专门处理低速设备
    CPU频率被4GHz天花板限制,增加CPU数量 -> 对称多处理器 -> 成本高,多处理机之间共享昂贵缓存,只多个核 ->多核处理器

  2. 将用于管理计算机本身的软件成为系统软件


    计算机软件体系结构
  1. 将计算机上有限的物理内存分配给多个程序使用,但问题是地址空间不隔离、内存使用效率低、程序运行地址不确定。
    加中间层的方法可以避免问题,把程序给出的地址看作是虚拟地址,通过映射,将虚拟地址转换为实际物理地址。
    内存 -> 物理地址
    分段将程序所需的内存空间大小的虚拟地址映射到某个物理地址,程序A、B被映射到两块不同物理空间区域且无重叠,解决了地址空间不隔离、程序运行地址不确定的问题
    根据局部性原理,程序在运行时,在某个时间段内,只是频繁用到一小部分数据,分页将地址空间人为地等分成固定大小的页共程序使用,提高内存使用效率

  2. 进程:所有应用程序以进程的方式运行在比操作系统权限更低的级别,有独立的地址空间,进程之间地址空间相互隔离,CPU分配资源的最小单位
    线程:cpu执行任务的最小单元,线程ID + 当前指令指针PC + 寄存器集合 + 堆栈
    一个进程由一个或多个线程组成,每个线程都运行在进程的上下文中,各个线程之间共享程序的内存空间(代码段、数据段、堆)及一些进程级资源(打开文件和信号)

线程间私有及共享数据

线程状态:运行 就绪 等待
线程调度:

  1. 线程安全
    同步:在一个线程访问数据未结束时,其他线程不得对同一个数据进行访问,将数据的访问原子化
    同步最常见方法:锁,线程访问数据或资源前先获取锁,并在访问结束之后释放锁,锁被占用时,获取锁的线程等待,直到锁重新可用

编译器优化时,可能为了效率而交换毫不相干的两条相邻指令的执行顺序,使用volatile关键字阻止过度优化
1.阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回
2.阻止编译器调整操作volatile变量的指令顺序

二.编译和链接

编译 + 链接 = 构建

三.目标文件
目标文件内存布局

.text/.data 它们在文件中和虚拟地址中分配空间
.bss仅分配虚拟地址空间

每个可重定位目标模块m都有一个符号表,包含m所定义和引用的符号的信息,共有三种不同符号:
由m定义并能被其他模块引用的全局符号:非静态的C函数和全局变量
由其他模块定义并被模块m引用的全局符号:其他模块中定义的非静态C函数和全局变量
只被模块m定义和引用的局部符号:带static属性的C函数和全局变量,
这些符号在模块m中全局可见,但其他模块不可见
其他非本地静态变量由栈管理,链接器对此类不感兴趣
利用 static 属性隐藏变量和函数名字

段表:数组中每个元素都是结构体,包括段名、类型、加载地址、相对于文件头的偏移量、段大小、链接信息
重定位表:需要重定位的信息
函数、变量需要独特的符号名,防止类似的符号名冲突,C++采用命名空间的方法解决符号冲突,Objective-C 采用加前缀方式。
函数签名:包含了一个函数的信息,包括函数名、参数类型、所属类、名称空间及其他信息
符号分为强符号,和弱符号。强符号不可名称重复,弱符号(未初始化的全局变量)可以符号名相同。
对符号名的引用分为强引用和弱引用,强引用表示如果找不到符号定义会报错,弱引用不报错,默认为0或某个特殊值。
目标文件里面还有可能保存调试信息
可以进行设置断点、监视变量变化、单步行进等调试是因为编译器将源代码的行、函数和变量类型、结构体的定义、字符串保存在目标文件里
GCC编译时加上 -g 参数,编译器就会加上调试信息

四.静态链接

链接:将几个输入目标文件加工后合并成一个输出文件,这个文件可被加载到内存并执行。
两步链接:空间与地址分配 符号解析与链接时重定位
一种语言的开发环境会附带语言库,语言库是对操作系统API的包装、常用函数
静态库可看做一组目标文件的集合,参与编译
链接控制脚本控制链接器的运行,将目标文件和库文件转化为可执行文件

五.可执行文件的装载与进程

程序:静态 预先编译好的指令和数据集合的文件
进程:动态 程序运行时的过程
CPU位数决定了虚拟地址空间的大小 —> 硬件寻址空间大小页映射函数将虚拟空间的各个页映射至相应的物理空间
程序执行时所需要的指令和数据必须在内存中才能够正常运行,又根据局部性原理,可将程序最常用的部分驻留在内存中,不常用的存放在磁盘里,需要时,动态装入
动态装载的方法:

进程的建立:
创建一个独立的虚拟地址空间

读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系

将CPU的指令寄存器设置成可执行文件的入口地址,启动运行

进程结束后,将相关资源(进程地址空间、物理内存、打开文件、网络链接)都被操作系统关闭或收回
页错误:当执行到某个地址的指令时,发现页为空(未被装入)
段地址对齐
进程启动前,将系统环境变量和程序运行参数保存到进程的虚拟空间栈中
程序库部分会把堆栈里的初始化信息中的参数信息传递给main函数的argc、argv参数argc命令行参数数量、argv命令行参数字符串指针数组

六.动态链接

静态链接把所有程序模块都链接成一个单独的可执行文件,可能会带来这些问题:

动态链接:把程序按照模块拆分为各个相对独立的部分,运行时才将他们链接在一起,在运行和加载时,可以被加载到任意的内存地址,并和一个在内存中的程序链接起来。
共享对象会被多个程序调用,导致其在虚拟地址空间中的位置难以确定,所以共享对象需要在装载时重定位
但装载时重定位会导致无法在多个进程间共享,采用地址无关代码
将共享对象模块中的地址引用划分为模块内部引用模块外部引用指令引用数据访问

延迟绑定 Lazy Binding 当函数第一次被用到的时候才重定位,提供程序运行速度
动态链接器是一个特殊共享对象,不依赖与任何动态共享文件,且自己的重定位工作由自己完成
自举:不用到任何全局和静态变量,自己完成重定位工作

静态链接库与动态链接库对比
七.内存

内核空间(内核使用,应用程序无法访问) + 用户空间
用户空间:

堆分配算法

Debug模式中,将未初始化区域都初始化为0xCC,有助于判断一个变量是否没有初始化,0xCCCC被当做文本就是烫,0xCDCD是屯
HotPatch 可替换函数,实现Hook,允许用户在某些时刻截获特定函数的调用

八.运行库

入口函数:运行库的一部分,一个程序的初始化和结束部分,准备好了main函数执行所需要的环境,并且负责调用main函数,这样在main函数中才能:申请内存、使用系统调用、触发异常、访问I/O

程序运行步骤:
1.操作系统创建进程后,把控制权交给程序入口
2.入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等
3.入口函数完成初始化后,调用main函数进行程序主体部分
4.main函数执行完毕后,返回入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O,然后系统调用结束进程

环境变量:存在于系统中的一些公用数据,如系统搜索路径,当前OS版本
I/O 指代了程序与外界的交互,包括文件、管道、网络、命令行、信号等FILE
运行库:启动与退出、标准函数、I/O、堆、语言实现、调试

九.系统调用

系统调用:为了让应用程序 (运行库) 有能力访问系统资源,让程序借助操作系统做一些必须由操作系统支持的行为,操作系统提供的接口
现代CPU可在多种截然不同的特权级别下执行指令,分为用户模式、内核模式
接口的调用通过中断实现从用户模式到内核模式的切换
上下文:操作系统保持跟踪进程运行所需的所有状态信息,包括 PC 和寄存器文件的当前值,以及主存的内容。
在任何时刻,单处理器系统都只能执行一个进程的代码,当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递到新进程。

课后问题及答案
  1. 源代码是怎么变成可执行文件的,每一步的作用是什么?
    (预编译,词法分析,语法分析,语义分析,中间语言生成目标代码生成,汇编,链接)
    预编译:展开宏,删除注释,标注行号
    词法分析:将代码解析成一个个记号
    语法分析:生成语法树
    语义分析:将语法树上的类型不符的插入相应结点,做隐式转换
    中间代码生成汇编:生成汇编语言
    链接:将源代码模块独立地编译,然后将其组装起来,将目标文件链接形成可执行文件​

  2. 应用层、API、运行库、系统调用、操作系统内核之间的关系是什么?
    应用层通过API调用运行库的接口,运行库通过系统调用调用操作系统内核​

  3. 虚拟内存空间是什么,为什么要有虚拟内存空间?
    虚拟内存空间使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。​

  4. 静态链接和动态链接分别表示什么,大概是怎么实现的?
    静态链接:把所有程序模块都链接成一个单独的可执行文件
    动态链接:把程序按照模块拆分为各个相对独立的部分,运行时才将他们链接在一起​

  5. 可执行文件的结构如何?分为哪些段?
    文件头、代码段、数据段、bss段(未初始化的全局变量)

  6. 进程的内存格局是怎样的?
    堆、栈、全局/静态区,代码区,常量区

  7. 堆和栈的区别,函数调用和栈的关系

  8. 进程和线程的区别

  9. 异步和同步,串行,并发,并行的区别

  1. 多并发任务,仅多线程能加快速度么?
    (不能,会变慢,有线程切换的开销)

  2. 多个线程之间可以共享那些数据?
    全局变量、堆上的数据、函数里的静态变量、程序代码打开的文件

  3. 进程之间如何通信管道?
    在父子进程中单向的流动有名管道:可在无亲缘关系的进程中通信信号量:控制多个进程对共享资源的访问
    消息队列:由消息的链表,存放在内核中并由消息队列标示
    共享内存:映射一段能被其他进程所访问的内存
    套接字:进程间通信机制

  4. 介绍几种锁,他们的用途和区别?
    //todo
    我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源, 应运而生。

上一篇下一篇

猜你喜欢

热点阅读