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

文章目录

  • 程序员的自我修养 ——— 读书笔记
    • 第一章 温故而知新
      • 1. 基本概念
      • 2. 线程基础
      • 3. Linux的多线程
      • 4. 线程安全
    • 第二章 静态链接
      • 1. 编译过程中隐藏的细节
        • 1.1 预编译
        • 1.2 编译
        • 1.3 汇编
        • 1.4 链接
      • 2. 编译器做了什么
      • 3. 链接器
    • 第三章 目标文件
      • 1. 目标文件的格式
      • 2. 目标文件中的内容
      • 3. ELF文件结构
      • 4. 链接的接口——符号
      • 5. 调试信息
    • 第六章 可执行文件的装载与进程
      • 1. 进程虚拟地址空间
          • 1.1 覆盖装入
    • 第七章 动态链接
      • 1. 为什么要动态链接
    • 第十章 内存
      • 1. 程序的内存布局
      • 2. 栈与调用惯例
        • 2.1 栈
        • 2.2 调用惯例
      • 3. 堆与内存管理
    • 第十一章 运行库
      • 1. 入口函数和程序初始化

程序员的自我修养 ——— 读书笔记

第一章 温故而知新

1. 基本概念

  1. 计算机系统分为:硬件部分和软件部分。

  2. 硬件部分主要是中央处理器CPU、内存和I/O控制芯片等。

  3. 为了协调CPU和内存和高速的图形设备,设计了北桥(Northbridge, PCI Bridge),以便于它们之间高速交换数据。

  4. 专门处理低速设备则设计了南桥(Southbrigde, ISA Bridge),如磁盘、USB、键盘、鼠标等设备。《程序员的自我修养——链接、装载与库》——选读笔记_第1张图片

  5. CPU的频率受制造工艺的限制,目前CPU的频率处于4GHz。因此从CPU的个数来提高运算速度,因此诞生了对称多处理器(SMP)。由于多个CPU的使用会增大成本,因此有了多核处理器,其与SMP的差异在于缓存共享等方面存在细微的差异。

  6. 系统软件:用于管理计算机本身的软件称为系统软件可以分成2部分,一是平台性的,比如:操作系统内核、驱动程序、运行库和大量的系统工具;二是用于程序开发的,如:编译器、链接器、汇编器等开发工具和开发库。

  7. 计算机软件体系结构可以理解为按层设计,最底下为硬件层,操作系统,应用程序编程接口,应用程序。每层之间的通信则是通过接口连接。《程序员的自我修养——链接、装载与库》——选读笔记_第2张图片

  8. 操作系统的两大主要功能:提供抽象的接口,管理硬件资源。

  9. 硬盘的基本存储单位为扇区(sector),每个扇区为512字节。硬盘存储大小的计算:盘片 * 磁道 * 扇区 * 512 byte。比如一个硬盘有2个盘片,每个盘面分65536磁道,每个磁道分1024个扇区,每个扇区512字节,那么该硬盘大小为128G。

  10. 地址空间有两种:一种是物理地址:是存在计算机中,实实在在存在的,在计算机唯一的。另一种是虚拟地址:是虚拟的,实际不存在,每个进程都是有自己独立的虚拟空间,使每个进程只能访问自己的地址空间,这样就达到了进程之间的隔离。

  11. CPU发出的是虚拟地址,也就是我们程序看到的是虚拟地址,经过内存管理单元(MMU,集成在CPU内部)就会变成物理地址。

  12. 分段:就是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。

  13. 分页:把地址空间人为地分成固定大小的页,每一页的大小有硬件决定或者硬件支持多种大小的页,有操作系统选择决定页的大小。

2. 线程基础

  1. 线程:可以称作为轻量级进程,是程序执行流的最小单元。
  2. 线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。
  3. 通常情况,一个进程由一个或者多个线程组成,各个线程之间共享程序的内存空间(如:代码段、数据的段、堆等)及一些进程级的资源(如打开文件和信号)。
  4. 多线程可以互不干扰地并发执行,并共享进程的全局变量和堆的数据。
    《程序员的自我修养——链接、装载与库》——选读笔记_第3张图片
  5. 使用多线程的原因:(1)多线程执行可以有效利用等待的时间。(2)程序逻辑本身就要求并发操作。(3)相对于多进程应用,多线程在数据共享方面效率要高很多。
  6. 线程访问非常自由,可以访问进程内存中的所有数据,甚至可以访问其他线程的堆栈(需要提前知道其他线程的堆栈地址,这种是很少见的情况)。
    《程序员的自我修养——链接、装载与库》——选读笔记_第4张图片
  7. 线程拥有自己的私有存储空间:(1)栈;(2)线程局部存储(TLS)(比如栈中的局部变量);(3)寄存器,寄存器是执行流的基本数据。
  8. 线程总是“并发”进行的。当线程数量小于等于处理器数量时,线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此之间互不相干。当线程数量大于处理器数量,线程的并发受到一些阻碍,因为此时至少有一个处理器运行多个线程。
  9. 在单处理器对应多个线程的情况下,并发是一种模拟出来的状态。操作系统会让这些多个线程程序轮流执行,所以这些线程看上去是在同时进行,实际每次只执行了一小段时间。
  10. 不断在处理器上切换不同的线程的行为称为线程调度
  11. 线程通常至少拥有3种状态:(1)运行(2)就绪,线程可以立刻运行,但CPU已经被占用了。(3)等待,无法执行。《程序员的自我修养——链接、装载与库》——选读笔记_第5张图片
  12. 线程的优先级改变的三种方式:(1)用户指定优先级。(2)根据进入等待状态的频繁程度提升或者降低优先级。(3)长时间得不到执行而被提升优先级。

3. Linux的多线程

  1. Linux内核中并不存在真正意义上的线程概念,Linux将所有的执行实体(无论是线程还是进程)称为任务,每一个任务类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。
  2. Linux上多个任务如果是共享了同一个内存空间,则这些任务构成一个进程,每个任务就是进程里的线程。

4. 线程安全

  1. 多线程程序处于一个多变的环境中,可访问的全局变量和堆数据随时可能被其他线程改变。
  2. 多个线程访问一个共享数据,可能造成严重的恶劣后果。比如:
    (1)线程1:i = 1; ++i;
    (2)线程2:–i;
    由于两个线程共同访问i了,i的可能结果有:0,1,2。原因是"++" "–"操作在编译成汇编语言时,会生成多条指令,在多线程执行时,可能执行一半后被调度系统打断,去执行其他代码,导致i被改写。
  3. 同步:指一个线程在访问数据未结束的时候,其他线程不得对同一个数据进行访问。
  4. 同步的最常见的方法是使用。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取锁,并在结束之后释放锁,在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
  5. 二元信号量:是最简单的一种锁,它只有两种状态:占用与非占用。它适合只能被唯一一个线程独占访问的资源。
  6. 当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,此后其他得所有试图获取该二元信号量得线程将会等待,直到该锁被释放。
  7. 对于允许多个线程并发访问的资源,多元信号量简称信号量
  8. 互斥量和二元信号量的区别
    (1)相同点:资源仅同时允许被一个线程访问。
    (2)不同点:二元信号量在整个系统可以被任意线程获取并释放,也就是说同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。互斥量则是要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其他线程去释放互斥量则是无效的。
  9. 临界区:是比互斥量更加严格的同步手段。把临界区的锁的获取称为进入临界区,把锁的释放称为离开临界区
  10. 临界区和互斥量与信号量的区别:互斥量和信号量在系统的任何进程都是可见的。即一个进程传建了互斥量和信号量,另一个进程试图去获取该锁是合法的。临界区的作用范围仅限于本进程,其他进程无法获取该锁。除了这点,临界区具有互斥量的性质。
  11. ** 读写锁**:读写锁有两种获取方式:共享的或独占的。读写锁的行为如下:
    《程序员的自我修养——链接、装载与库》——选读笔记_第6张图片
  12. 条件变量:作为一种同步手段,作用类似一个栅栏。使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。
  13. volatile关键字可以阻止过度优化,它起到两点作用:
    (1)阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。
    (2)阻止编译器调整操作volatile变量的指令顺序。——该点即使volatile可以做到,但无法阻止CPU动态调度换序。
  14. 代码解读:
int *pStr;
pStr = new int; //包含了三个步骤:1.分配内存;2.在内存的位置调用构造函数;3.将内存的地址赋值给pStr;
  1. 线程的三种模型:用户现场和内核线程存在3种模型.
    (1)一对一模型:用户使用的线程就唯一对应一个内核使用的线程。(反过来就不一定,因为内核线程在用户态不一定有对应的线程存在)。缺点:操作系统存在限制内核线程数量,该模式就会限制用户线程数量。在内核线程调度时,存在上下文的切换开销较大,导致用户现程执行效率低下。
    (2)多对一模型:多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行。线程切换速度比一对一切换快,线程数量不受限。缺点:如果一个线程阻塞,那么所有线程都会阻塞。
    (3)多对多模型:结合以上两者的优缺点。

第二章 静态链接

1. 编译过程中隐藏的细节

  1. 构建:编译和链接合并在一起的过程。
  2. 源代码到可执行文件经历4个过程:预处理、编译、汇编、链接。《程序员的自我修养——链接、装载与库》——选读笔记_第7张图片

1.1 预编译

  1. 预编译过程主要处理源代码文件中以#开始的预编译指令。
  2. 预编译的对象是源文件(.c/.cpp/.h/.hpp等),预编译后的产物是.i格式文件。
  3. 预编译的处理规则:
    (1)将所有的“#defin”删除,并且展开所有的宏定义;
    (2)处理所有条件编译预指令,如:“#if"、”#ifdef"、“#else"、”#endif"等。
    (3)处理“#include"预编译指令,将包含的文件拆入到该预编译指令的位置。
    (4)删除所有注释”//"和“/* */;
    (5)添加行号和文件标识名。
    (6)保留所有的#pragma编译器指令,因为编译器需要他们。
  4. 由于编译后的产物.i没有任何宏名,因此可以通过该文件判断宏定义和头文件是否包含正确。
  5. 命令:gcc -E hello.c -o hello.i

1.2 编译

  1. 编译过程是将预处理输出的.i文件进行一系列词法、语法、语义分析及优化后生产相应的回报代码文件(.s格式)。
  2. 命令:gcc -S hello.c -o hello.s

1.3 汇编

  1. 汇编器将汇编代码转成机器可执行的指令,每一条汇编语句几乎对用一条机器指令。
  2. 命令:as hello.s -o hello.o (as是汇编器)。

1.4 链接

  1. 将所有的目标文件(.o文件)链接成可执行文件(.out文件)。
  2. 命令:ld -static hello.o 等o文件。

2. 编译器做了什么

  1. 汇编器就是将高级语言翻译成机器语言的一个工具。
  2. 编译器的编译过程有6步:扫面、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
  3. 静态语义:指在编译期可以确定的语义。如:声明和类型的匹配、类型的转换等。
  4. 动态语义:指在运行期才能确定的语义。如:0作为除数是一种在运行期语义错误。
  5. 编译器可以分为前端和后端,前端负责产生与机器无关的中间代码,后端则是将中间代码转成目标机器代码。因此可以针对不同的平台使用同一个编译器前端和针对不同机器平台的数个后端。
  6. 编译器后端主要包括代码生成器和目标代码优化器。

3. 链接器

  1. 重定位:指重新计算各个目标地址的过程。
  2. 链接:将各个模块组装起来的过程就是链接。
  3. 链接过程包括地址和空间分配、符号决议和重定位等步骤。

第三章 目标文件

1. 目标文件的格式

  1. 编译器编译源代码后但未进行链接的中间文件是目标文件。(windows是.obj,Linux是.o)
  2. 可执行文件格式:都是COFF(Common File format)格式的变种。
    (1)Windows对应PE(Portable Executable)
    (2) Linux对应ELF文件(Executable Linkable Format)

2. 目标文件中的内容

  1. 目标文件中包含编译后的机器指令代码、数据,还包括链接时所需要的信息,如:符号表、调试信息、字符串等。这些信息的存储形式是按照“节”或者“段”的形式。
  2. 程序源代码编译后的机器指令经常被放在代码段里,代码段常见名字有“.code"或".text"。
  3. 全局变量和局部静态变量数据放在数据段。.data段。
  4. 未初始化的全局变量和局部静态变量放在.bss段中,其默认值是0。.bss段只是为未初始化的全局变量和静态变量预留位置而已,并没有内容,只是方便记录这些变量的大小。.bss段和.data段的区别是:data段的数据初始化是来源于文件中的初始化,而.bss段的数据是直接初始化为0。
  5. ELF文件内的信息:
    (1)文件头,描述整个文件的属性,包括文件是否可执行、是静态还是动态链接、目标硬件或者目标操作系统等。
    (2)文件头还包括一个段表,段表是描述文件中各个段的数组,是描述各个段在文件中的便宜位置和段的属性等。
  6. 将数据和代码分成数据段和代码段的好处:
    (1)当程序被装载后,数据和指令会分别映射到两个虚存区域。数据区域对进程来说是可读写的,而指令区是只读的,因此可以将两个虚存区的权限分别设置为可读写和只读,防止程序的指令被无意或有意的改写。
    (2)指令和数据被分开存放对CPU的缓存(Cache)命中率提高有好处。
    (3)当存在运行多个该程序的副本时,他们的指令都是一样的,多以内存中只需要保存一份改程序的指令部分,有利于内存共享。
  7. 实例来分析:
/**************************************
demo.c
gcc -c demo.c 
**************************************/
#include

你可能感兴趣的:(C++,C语言相关,c++)