Logo Xiabing Blog

程序员的自我修养

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

Sep 8, 2023 - 1 minute read

简介

线程

  • 线程可以访问进程内存里的所有数据,但也拥有自己的私有存储空间

  • 当线程数小于等于处理器数量时,线程的并发是真正的并发

Linux的多线程

  • Windows对进程和线程实现如教科书一样标准,其内核有明确的线程和进程概念
  • 对于Linux,线程并不是一个通用的概念。Linux内核中并不存在真正意义的线程概念,其将所有的执行实体称为任务(Task),每一个任务都类似于一个单线程的进程,但是不同的任务之间可以选择共享内存空间,共享同一个内存空间的多个任务构成了一个进程。
  • 用户线程不一定一等一对应同等数量的内核线程

静态链接

编译过程

  • 预编译:主要处理源代码中的预编译命令,删除注释(生成.i或者.ii文件)。
  • 编译:把预处理完的文件进行词法分析、语法分析等并生成相应的汇编代码文件(生成.s文件)。
    • 扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化等
  • 汇编:将汇编代码转成机器可以执行的指令(生成.o文件)。
  • 链接:ld命令(生成.out文件)。
    • 地址和空间分配、符号决议、重定位等

目标文件

  • 目标文件就是没有链接的文件。(Windows下为.obj, Linux下为.o)
  • Windows下他们为PE-COFF文件格式,Linux下为ELF文件格式
  • 局部变量存放在进程的堆栈中,而不存放在目标文件中

ELF文件类型:可重定位文件、可执行文件、共享目标文件、核心转储文件

动态链接库与静态链接库

  • 动态链接库:Windows下为.dll,Linux下为.so
  • 静态链接库:Windows下为.lib,Linux下为.a

目标文件的样子

  • 代码段:程序源代码编译后放在代码段(.code .text)
  • 数据段:已初始化的全局变量和局部静态数据放在数据段(.data)
  • .bss段:未初始化的全局变量和局部静态数据放在.bss段,.bss段只是未两者预留位置而已,并没有内容,所以它在文件中不占据空间
  • 只读数据段(.rodata):一般是程序里的只读变量(const)
  • 注释信息段(.comment):保存编译器和系统版本信息
  • 堆栈提示段(.note.GNU-stack)
  • 等等

c++的函数重载利用了符号修饰。

弱符号和强符号

  • 编译器默认函数和初始化了的全局变量未强符号,未初始化的全局变量为弱符号

静态链接

ELF文件定义了两种特殊段:

  • .init 构成了进程的初始化代码,在main函数调用之前执行
  • .fini main函数正常退出时,执行

API与ABI:

  • API指应用程序接口,ABI指二进制层面接口
  • API相同不代表ABI相同

可执行文件的装载与进程

  • 程序的虚拟地址空间的大小由计算机的硬件平台决定
  • 指针大小的位数与虚拟空间的位数相同

动态装载方法

  • 覆盖装入(Overlay):程序员需要手工将模块按照他们之间的调用依赖关系组织成树状结构
  • 页映射(Paging)

动态链接

  • 进程A和B同时使用同一个动态库,这个动态库被加载时数据段会有自己独立的副本
  • 动态链接器本身是静态链接的

GOT表和PLT表

  • GOT:ELF文件中用于定位全局变量和函数的表
  • PLT:ELF文件中用于延迟绑定的表

动态链接比静态链接慢的原因

  • 对于全局和静态数据访问需要复杂的GOT定位,让后间接寻址;对于模块间的调用也要先GOT定位,再跳转
  • 程序开始执行时,动态连接器都要进行一次链接

延迟绑定:对于很少用到的模块,再函数第一次使用到时才进行绑定

动态链接的步骤

  • 动态链接器自举
  • 装载共享对象
  • 重定位和初始化

Linux共享库的组织

  • 动态链接器会在/lib、/usr/lib、 /etc/ld.so.conf配置文件指定的目录中查找共享库

共享库文件名

  • libname.so.x.y.z:最前面为前缀"lib",中间是库的名字和后缀,最后面跟着的是三个数字组成的版本号
  • 主版本号表示重大升级,次版本号表示增量升级,发布版本号表示错误的修正、性能的改进等

共享库构造和析构函数

  • 只要在函数声明时加上“ attribute((constructor)) attribute((destructor))” 属性,这种函数就会在main()函数执行完毕前(后)执行(或者时程序调用exit()前(后)执行)

内存

  • 栈上的数据在函数返回的时候会被释放掉
  • 我们程序所使用的内存地址叫做虚拟内存地址,实际存在硬件里面的空间地址叫物理内存地址
  • Linux下的进程堆管理提供了两种堆空间的分配方式
    • brk():可以设置进程数据段的结束地址,即可以扩大和缩小数据段
    • mmap():可以向操作系统申请一段虚拟地址空间,并且可以将其映射到某个文件;当不映射时,称这块空间为匿名空间,匿名空间就可以作为堆空间
  • 不能重复释放两次堆空间的内存
  • malloc申请的内存,在进程结束后不会存在
  • malloc申请的虚拟地址空间是一定连续的,但是物理地址不一定连续,可能是若干个不连续的物理页组成的

声名狼藉的C++返回对象

  • C++返回一个对象的时候会调用两次拷贝构造函数—一次拷贝到栈上的临时对象里,另一次将临时对象拷贝到存储返回值的对象里

  • C++提出了返回值优化:将两次合并,直接将对构造在传出时使用的临时对象上

    MyClass fn()
    {
    	return MyClass();
    }
    

堆分配算法

  • 空闲链表:将空闲块按照链表的方式连接起来
  • 位图
  • 对象池

运行库

  • CRT:C运行库
  • glibc和MSVC都是运行库,是不同操作系统之间的抽象层,他们将不同的操作系统API抽象成相同的库函数
  • 环境变量存储的是系统的公用信息

入口函数

  • 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等
  • 入口函数完成后,调用main函数
  • main函数执行完毕后,返回到入口函数,进行清理工作,包括全局变量析构、堆销毁等,然后进行系统调用结束进程
  • glibc程序入口为_start
  • MSVC的入口函数为mainCRTStartup

运行库与IO

  • 每个进程都有一个私有的“打开文件表”,这个表是一个指针数组,每一个元素指向内核的打开文件对象,fd就是这个表的下标

系统调用与API

  • 系统调用是应用程序与操作系统内核之间的接口
  • 操作系统通过中断从用户态切换到内核态