《程序员的自我修养》读书笔记
对称多处理器(SMP Symmetrical Multi-Processing)每个CPU在系统中所处的地位和发挥的功能是一样的。
“计算机科学领域的任何问题都可以通过添加一个间接的中间层来解决”
分页的基本方法是将地址空间人为地分为固定大小的页,每一页的大小由硬件决定。这些叫做虚拟空间
虚拟空间的有些页被映射到同一个物理页,这样就可以实现内存共享
通过页映射将虚拟空间和物理空间对应起来,保护也是页映射的目的:虚拟存储的实现需要依赖硬件的支持,MMU(Memory Management Unit)进行映射
一个标准的线程(Thread)是由线程ID,当前指令指针(PC),寄存器集合和堆栈组成的。
线程私有的:局部变量,函数的参数,TLS数据(线程局部存储,Thread Local Storage),寄存器数据
线程之间共享:全局变量,堆上的数据,函数里的静态变量,程序代码(任何线程都有权力读取并执行任何代码),打开的文件(A线程打开的文件可以由B线程来读写)
windows和linux的区别,window有以上明显的线程和进程的区别,linux只有一个task的概念,类似于一个单线程的进程,具有内存空间,执行实体,文件资源等。(不同的task可以共享内存空间,这样多个task就变成了多个线程组成的进程)
fork可以复制当前task,由此有“写时复制(Copy on Write)”,
函数可重入一般发生在多线程同时访问这个函数,或者内部有递归调用,函数调用自身
过度优化(包括编译器的优化)可能会影响线程安全,编译器在内存和寄存器之间的读写,因为锁是针对内存中的数据的,所以可以使用 volatile来阻止过度优化,volatile可以做到:1,阻止编译器为了优化将一个变量缓存到寄存器而不写回,2,阻止编译器调整操作volatile变量的指令顺序
还有就是 Singleton的double-check
因为CPU的乱序执行能力对保持多线程安全异常困难,阻止CPU换序是必需的,调用CPU提供的barrier指令,
GCC编译程序时候,预处理,编译,汇编,链接 四个步骤
1,预编译的过程:将头文件或者源文件预编译成另一个格式,.h文件到.i文件,.hpp文件到.ii文件,如下面的指令: gcc -E hello,c -o hello.i -E表示预编译
这个阶段处理的主要是处理以 # 开始的预编译指令,如 #include #define,展开所有的宏定义,删除#defne ,删除所有注释,添加行号和文件名标识,保留所有的#pragma编译器指令
2,编译的过程:词法分析,语法分析及优化等。
可以分为6步:扫描,语法分析,语义分析,源代码优化,代码生成,目标代码优化
如指令:gcc -S hello.i -o hello.s
gcc这个命令只是这些后台程序的包装,它会根据不同的参数要求去调用预编译程序cc1, 汇编器as,链接器ld。
3,汇编的过程:将汇编代码转为机器可以执行的指令,每一个汇编语句对应一个机器指令,一一翻译,输出目标文件(Object File)
如指令:gcc -c hello.s -o hello.o
4,链接的过程
扫描,词法分析,扫描器,使用有限状态机的算法,
语法分析:语法分析器(Grammar Parser),产生语法树,使用上下文无关语法的分析手段。
语义分析,语义分析器,只能分析静态语义,只能完成对表达式的语法层面的分析
中间语言生成,源码级优化器,优化语句,直接将这个语法树转换成中间代码,中间代码使得编译器可以被分为前端和后端,编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换为目标机器代码。
链接器:一条条指令不一定是连续的,存在跳转的目标地址,重新计算各个目标的地址过程被叫做重定位。这种程序的经常修改导致跳转目标地址变化在程序拥有多个模块的时候更为严重。
一个程序被切分为多个模块后,模块之间组合的问题可以归结为模块之间通信问题,两种方式:函数调用和变量访问。这种开发方式让“链接”在整个过程种非常重要
链接的内容是将各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。
链接的过程主要有:地址和空间分配(Address and Storage Allocation),符号决议(Symbol Resolution)和重定位(Relocation)这些步骤。
模块拼接 -- 静态链接和动态链接
每个模块单独编译,跨模块调用的指令的目标地址会暂时搁置,等待最后链接的时候由链接器去将这些指令的目标地址修改。所以,使用链接器,可以直接引用其他模块的函数或者全局变量而不需知道地址。
目标文件
目前PC平台的可执行文件格式(Executable)主要是windows下的PE(Portable Executable)和linux的ELF(Executable Linkable Format),都是COFF(Common File Format)的变种。
动态链接库和静态链接库都是按照可执行文件格式来存储,在windows按照PE-COFF格式存储,在linux下按照ELF格式存储,
目标文件就是源码编译后但未进行链接的那些中间件。Unix最早的可执行文件是.out格式,设计简单,难以支持共享库, 后来的COFF的主要贡献是在目标文件引入“段”的机制
目标文件将信息按照不同的属性,以“段”的形式存储,代码段,数据段。文件头还有一个段表。未初始化的全局变量和局部静态变量都放在.bbs段里面,默认值都为0。.bbs段只是为未初始化的全局变量和局部静态变量预留位置而已,并没有内容,在文件种不占据空间。
总体来说,程序源代码被编译以后主要分为两种段:程序指令和程序数据,代码段属于程序指令,而数据段和.bbs段属于程序数据。
数据和指令分开段的好处:1,当程序被装载后,数据和指令分别映射两个虚拟空间,数据区域是读写,指令区域只读。2,对CPU强大的缓存Cache体系,指令和数据的分开缓存能提高缓存命中,3,当系统运行着多个该程序的副本时候,只需要在内存保存一份指令段。同样对于只读数据也可以这么做。
Exp: gcc -c test.c 得到 test.o 文件,可使用binutils的objdump查看内部的结构,
ELF文件的文件头结构的Magic Number,是标识符,操作系统在加载可执行文件的时候会确认魔数是否正确。
段的标记位段的标识符表示该段在进程虚拟地址空间中的属性。
链接的本质-符号
链接的过程是将多个不同的目标文件之间互相粘到一起。在链接中目标文件的相互拼合是目标文件之间对地址的引用。每个函数或者变量都有自己独特的名字,将函数或变量统称为符号(Symbol),函数名或变量名是符号名(Symbol Name)
每一个目标文件都有一个符号表,记录了所用到的符号,每个定义的符号有一个符号值,对于函数和变量来说符号值是它们的地址。
ELF文件中的符号表往往是文件中的一个段,".symtab",一个Elf32_Sym的结构体,
特殊符号:使用ld作为链接器来链接可执行文件时。链接器会在将程序最终链接成可执行文件的时候将其解析成正确的值,
随着模块增多,名称空间(Namespace)来解决模块间的符号冲突问题
为了支持C++的类,继承,重载,虚机制等特性,引入 符号修饰(Name Decoration)和符号改编(Name Mangling)
对于c/c++语言,强符号:编译器默认函数和初始化了的全局变量; 弱符号: 未初始化的全局变量。
调试信息:目标文件里面可能保存的是调试信息,如在函数中设置断点,可以监视变量变化,可以单步行进。
静态链接
空间与地址分配:链接器如何将各个段,各个目标文件,合并到输出文件
1,按序叠加:最简单的方案,各个目标文件依次合并,每个段都需要一定的地址和空间对齐,容易造成内存空间大量内部碎片
2,相似段合并:将相同性质的段合并到一起,.text段,.data段分开,并且相似段合并,输出的可执行文件的空间,.bss段在目标文件和可执行文件中并不占用文件的内存,但在装载时占用地址空间。
两步链接(Two-pass Linking)方法,整个链接过程分为两步,一,空间与地址分配,扫描所有的输入目标文件,链接器将能够获得所有输入目标文件的段长度,并且将它合并,计算出输出文件中各个段合并后的长度位置,并建立映射关系,二,符号解析与重定位,读取输入文件中段的数据,重定位信息,进行符号解析与重定位。
在完成空间和地址的分配后,链接器进入符号解析和重定位步骤,静态链接的核心内容。
重定位表:该结构专门用来保存重定位相关的信息,重定位入口的偏移就是这个重定位表
符号解析:当链接时报错符号未定义,最常见的是链接时缺少某个库,或者输入目标文件路径不正确或符号的声明与定义不一样。
重定义的过程中,每一个重定位的入口对应一个符号的引用。
指令修正方式:不同的处理器指令对地址的格式和方式不 一样,转移跳转指令(jmp指令),子程序调用指令(call指令),数据传送指令(mov指令),寻址方式千差万别。
差别有:1,近址寻址或远址寻址,2,绝对寻址和相对寻址,3,寻址长度为8,16,32或64位,
COMMON块 (common block) 当不同的目标文件需要的common块空间大小不一致时,以最大的为准。
C++相关:
重复代码消除,c++的模板,虚函数表,外部内联函数可能在不同编译单元生成相同的代码,可能会产生空间浪费,地址易出错,指令运行效率低的问题。有效的做法是把每个模板的实例代码单独放在一个段里,
函数级别链接:这个作用是让所有的函数像模板函数一样,单独保存到一个段里面,当链接器需要用到函数时候,将其合并到输出文件中,将没用到的函数抛弃,很大程序上减小了输出文件的长度,减少空间浪费。
C++ABI(Application Binary Interface):如果要使两个编译器编译出来的目标文件能够相互链接,那么这个目标文件必须满足:采用相同的目标文件格式,拥有同样的符号修饰标准,变量的内存分布方式相同,函数的调用方式相同 。将 符号修饰标准,变量内存布局,函数调用方式等与可执行代码二进制兼容性相关的内容成为ABI
API(Application Programming Interface)和ABI之间的区别:API是源码层面的接口,ABI二进制层面的接口,
静态库l链接:一个静态库可以看作是一组目标文件的集合,即多个目标文件经过压缩打包后形成的一个文件。通常在静态运行库一个目标文件只含有一个函数,是因为链接器在链接静态库的时候是以目标文件为单位的,如果将很多函数都放入一个目标文件,那么很多没用的函数也会链接过来,由于运行库有成千个函数,每个函数独立放在一个目标文件可以尽量减少空间浪费。
链接过程控制:一般使用链接器提供的默认链接规则对目标文件进行链接。操作系统内核本质上也是一个程序,就是PE文件,windows/system32/ntoskrnl.exe 文件就是。
链接控制脚本:链接器一般能提供多种控制整个链接过程的方法,以用来产生用户所需要的文件。有三种方法:1,使用命令行来指定参数,2,将链接指令存放在目标文件,编译器经常会通过该方法,3,使用链接控制脚本,最为灵活强大
使用ld链接脚本:脚本的语句组成:命令语句,赋值语句。
BFD库,GNU项目,目标是希望通过一种统一的接口来处理不同的目标文件格式
windows的二进制文件格式PE/COFF
windows引入PE(Protable Executable)的可执行格式,由COFF发展而来,PE也允许程序员将变量或者函数放到自定义的段。
系统装载PE时,根据数据目录 来快速找到导入表,导出表,资源,重定位表。
装载过程:基本过程是将程序从外部存储中读取到内存中的某个位置
每个程序被运行起来后都有着自己的独立的虚拟地址空间,32位系统为4G,(0 - 2^32),64系统为(0 - 2^64) ,可以通过C指针所占空间来计算虚拟地址空间的大小。
PAE(Physical Address Extension)Intel新的物理地址扩展方式,修改页映射的方式,使得新的映射方式可以访问更多的物理内存。在windows下是 AWE(Address Windowing Extensions)在linux是通过mmap() 系统调用来实现
动态装载:覆盖装入(Overlay)和页映射(Paging) ,原则上都使用了程序的局部性原理,思想是用到了啥就装入,不用则存放到存储。
覆盖装入:(早期的程序员需要编写一个小的辅助代码来管理哪些模块驻留内存的,覆盖管理器(Overlap Manager)),树状的调用路径,禁止跨树间调用,典型的利用时间换空间的方法
页映射:虚拟存储机制的一部分,数据和指令按照页的单位划分,特定算法(FIFO, LUR)进行装载管理,这个所谓的装载管理器就是现代操作系统
进程的建立:(最关键的特征是其拥有独立的虚拟地址空间)
1,创建一个独立的虚拟地址空间 (创建一个虚拟空间实际上只是分配一个页目录)
2,读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系,当程序执行发生页错误时,操作系统将从物理内存中分配一个物理页,将该缺页从磁盘读取到内存中
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件又被叫做 映像文件
虚拟内存:内存管理技术,将多个分隔的物理内存碎片和部分外部磁盘存储映射到一个连续内存中,对地址空间的重定义
3,将cpu的指令寄存器设置成可执行文件的入口地址,启动运行。通过设置cpu的指令寄存器将控制权交给进程
进程虚拟空间分布: 单位是Segment的,存在合并段,尽量将相同权限属性的段分配在同一空间下,
进程的虚拟空间中的表现是以VMA的形式存在的,一个进程中有4种VMA区域,代码VMA,权限只读可执行,有映像文件:数据VMA,权限可读写可执行,有映像文件,堆VMA,可读写可执行,无映像文件,匿名,可向上扩展;栈VMA,可读写不可执行,无映像文件,匿名,可向下扩展
堆的最大申请范围: 收到操作系统,程序栈数量大小,程序本身大小等影响,还可能每次的不一,有着随机地址空间分布的技术,来防止恶意攻击。
静态链接的缺点:浪费内存和磁盘空间,模块更新困难。在静态链接的情况下,如果有两个可执行文件用到了A模块,则在磁盘和内存中有两个副本。同时影响程序的发布,一旦有更新,这个程序都需要更新。
动态链接的做法:形成两个独立的文件,不对目标文件进行连接,等到程序运行时才进行连接,同时多个程序可以共同使用一个内存中的动态库。好处:节省内存,减少物理页面的换入换出,增加CPU缓存命中,模块独立,耦合度小。
动态链接,共享的对象的最终装载地址在编译时是不确定的,在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给共享对象。
动态链接时的重定位叫做 装载时重定位(Load Time Reload),又叫基址重置
MSVC编译器提供了 __declspec(dllimport)编译器扩展来表示一个符号是模块内部还是模块外部
产生地址无关代码:模块内部的函数或者跳转,数据读取,都是相对寻址;模块间的数据访问和函数调用跳转,依赖全局偏移表GOT(一个指向这些变量的指针数组 或者 目标函数的地址),TEXTREL表示代码段重定位表地址,产生的文件是地址无关可执行文件(PIE,Position-Independent Executable) 共享模块的数据段部分在每个进程都有独立的副本。
延迟绑定:当函数第一次被用到时候才进行绑定(符号查找,重定位),ELF使用PLT(Procedure Linkage Table)的方法来实现,通过PLT项的结构来实现间接跳转。
动态链接情况下:
1,可执行文件的装载和静态链接基本一样,读取文件头,检查合法性,各个段的映射,
2,操作系统会启动一个动态链接器(Dynamic Linker) ,实质是共享对象
3,将控制权交给动态链接器的入口地址,
ELF有动态符号表(Dynamic Symbol Table),表示模块之间的符号导入导出的关系
动态链接重定位表:共享对象需要重定位的主要原因是导入符号的存在。
动态链接时候进程堆栈初始化信息
在加载多个模块时候,如果一个共享对象里面的全局符号被另一个共享对象的同名全局符号覆盖: 共享对象全局符号介入(Global Symbol Interpose)
显式运行时链接(Explicit Run-time Linking),运行时加载,让程序自己在运行时控制加载指定的模块,并且在不需要该模块的时将其卸载。也可以实现对某个脚本解释模块的重新装载
动态库的装载可以通过动态链接器提供的API:如打开动态库(dlopen),查找符号(dlsym),错误处理(dlerror)和关闭动态库(dlclose),
Linux的共享库,普通的ELF共享对象,可以被各个程序之间共享。共享库的更新可以分为两类:兼容更新(只添加内容 不修改原有接口),不兼容更新(改变原有接口) 接口(Application Binary Interface)
如果要开发一个接口为C++的共享库(使用C的接口在linux会更加简单),需要注意:1,不要在接口类中使用虚函数,2,不要改变类中任何成员变量的位置类型,3,不要删除非内嵌的publish或protected成员,等等
SO-NAME软链接:指向系统中最新版的共享库,目的是使得所有依赖某个共享库的模块,在编译链接运行时都使用共享库的SO-NAME,而不是详细的版本号。
当共享库的符号有了版本集合后,链接器在程序的最终输出文件中会记录当前用到的版本符号集合。链接器会计算出函数调用实际所用到的最高版本的符号,然后将符号记录到可执行文件内
Linux有着FHS(File Hierarchy Standard)标准,规定了一个系统中的系统文件如何存放,包括各个目录结构,组织和作用。
动态链接器装载或查找共享对象的顺序:1,由环境变量LD_LIBRARY_PATH指定的路径,2,由路径缓存文件/etc/ld.so.cache指定的路径,3,默认共享库目录,先usr/lib,后/lib
共享库的安装:默认情况下,链接器在产生可执行文件时,只会将那些链接时被其他共享模块引用到的符号放到动态符号表,这样可以减少动态符号表的大小
创建:最简单的办法是将共享库复制到某个标准的共享库目录
GCC提供了一种共享库的构造函数,在函数声明时加上 __attribute__((constructor)) 属性,指定该函数为共享库构造函数,拥有这种属性的函数会在共享库加载时候被执行,即在程序的main之前执行;或者在dlopen打开共享库,在dlopen返回之前
如果加上 __attribute__((destructor)) 则在main执行完毕后执行 ,使用dlclose卸载时在dlclose返回之前执行。构造函数可以设置优先级数字,数值越小优先级越大
Windows的动态链接
DLL(动态链接库Dynamic Link Library),更强调模块化,
PE里面有两个很常用的概念是基地址(Base Address) 和相对地址(RVA, Relative Virtual Address)(相对基地址的偏移)
DLL共享数据段:可以实现进程间通信,常见做法是将一些需要进程间共享的变量分离出来,放到另外一个数据段中。DLL有两个数据段,一个进程间共享一个私有
ELF默认导出所有的全局符号,在DLL中需要显式告诉编译器,或者使用 __declspec(dllexport)表示该符号是从本dll导出的符号,__declspec(dllimport)表示从别的dll导入的符号,也可以使用.def 文件来声明导入导出符号
Math.lib 并不包含 Math.c 的代码和数据,用来描述 Mah.dll 的导出符号,包含了符号和一些胶水代码,以便程序和dll粘在一起。
DLL也支持运行时链接,Windows提供了一些API:LoadLibrary 用来装载dll,GetProcAddress,FreeLibrary用来卸载某个已加载的模块
导出表结构包含了:导出地址表(存放的是各个导出函数的RVA),符号名表(保存的是导出函数的名字),名字序号对应表(历史出现,查找函数)和其他
导出重定向(Export Forwarding)a.dll的A函数重定向为b.dll的B函数,实现机制:将地址数组中的函数RVA指向的位置位于导出表中。
Windows PE采用的是装载时重定位,DLL所有涉及到绝对地址的引用都进行重定位。
一个dll中的每一个导出的函数都有一个对应的序号。
DLL的绑定实现:editbin对被绑定的程序的导入符号进行遍历查找,找到后将符号的运行时目标地址写入被绑定程序的导入表内。对程序进行绑定时,对每个导入的DLL,链接器把DLL的时间戳和校验和保存到被绑定的PE文件的导入表中。
C++和动态链接:为了兼容性问题,MS开始了组件对象模型(COM)工作。
解决DLL HELL的方法:1. 静态链接,2. 防止DLL覆盖,3. 避免DLL冲突,4. .NET下程序集有两种类型应用程序程序集和库程序集,一个程序集包含一个或者多个文件,需要一个清单文件来描述,Manifest文件,描述了程序集的名字,版本号以及程序集的各种资源,同时也描述了该程序集的运行所依赖的资源,包含DLL以及其他资源文件。
有了Manifest这种机制后,动态链接的c/c++程序在运行时必须在系统中有与他在Manifest指定的完全相同的DLL。
程序的环境由三个部分组成:内存,运行库和系统调用,(内核可算是运行环境一部分,在此忽略)
栈:从高地址位开始,向下生长,栈顶esp指针,栈保留了一个函数调用所需要的维护信息,叫做堆栈帧或者活动记录,包含了:a,函数的返回地址和参数,b,临时变量,c,保存的上下文
堆栈帧使用ebp和esp两个寄存器划定范围,esp始终指向栈的顶部,
Hook技术,允许用户在某些时刻截获特定函数的调用
调用惯例(Calling Convention)
函数参数的传递顺序和方式,常见的是栈传递,函数调用方将参数压入栈中
栈的维护方式,
名字修饰(Name-mangling)策略
函数返回值传递
如 int f1() { return 1; }
void f2() { int n = f1(); }
1,首先在f2函数栈上开辟空间,并将这块空间一部分作为传递返回值的临时对象 temp
2,把temp对象的地址作为隐藏参数传递给 f1 ,f1函数将数据拷贝给temp,并将temp对象的地址用eax传出
3,在f1返回之后,f2函数将eax指向的temp内容拷贝给n
如果返回值类型的尺寸太大,C语言在函数返回时使用一个临时的栈上内存区域作为中转,结果返回值对象会被拷贝两次。(所以最大不返回大对象)
返回值优化(Return Value Optimization,RVO) ,在返回对象和拷贝构造函数时候将两步骤合并,直接将对象构造在传出时使用的临时对象上,减少一次复制过程
堆:一块巨大内存空间,常常占据整个虚拟空间的绝大部分。运行库通过一个堆的分配算法来管理堆空间,运行库通过向操作系统批量申请了块内存,然后分发给程序使用
Linux下的进程堆管理: 提供了两种堆空间的分配方式,两个系统调用
1,int brk(void *end_data_Segment) 作用是设置进程数据段的结束地址,可以扩大缩小数据段
2,void *mmap(void *start, size_t length, int port, int flags, int fd, off_t offset) 作用是向操作系统申请一段虚拟地址空间,因为mmap申请匿名空间时系统会为其在内存或交换空间中预留地址,申请的空间大小不能超过空闲内存+交换空间的总和。
Windows的进程堆管理
Windows的每一个线程都有着自己的独立的栈,线程栈的大小由创建线程时候的CreateThread的参数指定。Windows的内存申请api是VirtualAlloc,要求空间大小必须为页的整数倍,与mmap相似。也提供了堆管理器来管理堆空间,
堆管理算法:空闲链表,位图(bitmap),对象池
运行库
在main开始执行的时候,堆栈初始化完成了,系统IO也初始化了
main返回之后,记录main函数的返回值,调用atexit注册的函数,然后结束进程
一个典型的程序运行步骤:
1,操作系统在创建进程后,将控制权交给程序的入口,入口往往是运行库中的某个入口函数
2,入口函数对运行库和程序运行进行初始化,堆栈,IO,线程,全局变量的初始化
3,入口函数完成初始化之后,调用main,正式开始。
4,main执行完毕后返回到入口函数,入口函数进行清理工作,堆栈,IO的关闭销毁,然后进行系统调用结束进程
入口函数
glibc入口函数: _start -> __libc_start_main -> exit -> _exit 简化的流程。
环境变量是存在于系统中的一些公用数据,任何程序都可以访问。
MSVC CRT入口函数: 默认的入口函数名为 mainCRTStartup ,
mainCRTStartup的总体流程是:1,初始化和OS版本有关的全局变量,2,初始化堆,3,初始化IO,4,获取命令行参数和环境变量,5,初始化C库的一些数据,6,调用main并记录返回值,7,检查错误并将main的返回值返回。
MSVC的入口函数使用了alloca,特点是能够动态地在栈上分配内存,在函数退出时如同局部变量一样自动释放
IO:操作系统的IO是计算机和人,和外接设备之间的交互;程序的IO是任何操作系统理解为“文件”的事物。具有输入输出概念的实体(设备,磁盘文件,命令行)都是文件
在操作系统层面,文件操作,在linux是文件描述符(File Descriptor),在Windows是句柄(Handle),
MSVC的IO初始化主要为如下:1,建立打开文件表,2,如果能够继承自父进程,那么从父进程获取继承的句柄,3,初始化标准输入输出
C语言的运行库,C运行库(CRT)(C Runtime Library)
一个CRT大致包含了如下功能:1,启动与退出:如入口函数及其相关函数,2,标准函数,3,IO,4,堆,5,语言实现,6,调试,实现调试功能的代码
glibc的启动文件,crt1.o 包含的程序入口函数 _start, 调用 _libc_start_main 初始化 libc并调用main函数进入真正的程序主体。
C++ CRT MSVC在编译的时候,会根据不同的模式和版本(如单线程/多线程,发布版/调试版),链接不同的运行库。
线程的访问权限:很自由。
线程私有:线程的局部存储(Thread Local Storage,TLS),栈,寄存器(PC寄存器)(执行流的基本数据),局部变量,函数的参数
线程之间共享:全局变量,堆上的数据,函数里面的静态变量,程序代码,打开的文件
多线程相关的运行库,需要考虑到的方面:1,提供支持多线程的接口,如创建线程,退出线程等,2,保证c运行库在多线程安全。 在线程不安全的函数内部会自动进行加锁。
线程局部存储(TLS)机制,GCC的是关键字 __thread , MSVC是 __declspec(thread) 变成线程私有变量 。 实现:编译器会将该变量放在PE文件的.tls段,当系统启动一个新的线程时从进程的堆中分配一个空间来存放这个变量。
入口函数 _start -> __libc_start_main -> __libc_csu_init -> _init -> __do_global_ctors_aux 编译器会编译单元产生的目标文件的 .ctors 放置一个数组,存放全局构造函数指针。每一个元素指向一个目标文件的全局构造函数 。(负责初始化所有的复杂全局/静态对象)
fread,缓冲, flush一个缓冲,对写缓冲来说是将缓冲内的数据全部写入实际的文件中并清空缓冲,保证文件处于最新的状态
C语言支持行缓冲(输入输出每次遇到一个换行符就自动flush缓冲),全缓冲(满的时候才去flush缓冲)
系统调用
操作系统都会提供一套接口,以供应用程序使用,接口往往通过中断来实现。Linux使用0x80号中断作为系统调用的入口,Windows使用0x2E号中断来作为系统调用入口 (中断号有限)
Linux系统调用 都可以在程序直接使用,c语言定义在 unistd.h 中,
运行库将不同的操作系统的系统调用包装为统一固定的接口,使得同样的代码,在不同的操作系统下可以直接编译,并产生一致的效果
中断是一个硬件或者软件出发的请求,要求CPU暂停当前的工作转手去处理更重要的事情。两个属性:中断号(从0开始),中断处理程序(Interrupt Service Routine,ISR)。内核中有中断向量表,包含了第n号中断的中断处理程序的指针,根据中断号找到处理程序并调用。中断号有限,系统调用都有一个系统调用号(在系统调用表的位置)。
系统调用的过程:1,触发中断,2,切换堆栈(用户栈切换为内核栈)(a 保存当前的ESP SS的值,b 将ESP SS的值设置为内核栈的相应值),3,中断处理程序
windows API(Application Programming Interface)Windows系统提供给应用程序的接口。windows 的最底层接口API不是公开,而是在系统调用上建立了一个API层。以DLL导出函数的形式暴露给应用程序开发。这些windows API dll导出的函数头文件,导出库相关文件一起称为 SDK
使用windows api的原因:系统调用非常依赖于硬件结构(寄存器数量,参数传递,中断号),中间封装了DLL层后,系统调用层面可以自由改动,做到向后兼容
子系统(Subsystem 环境子系统)是Windows架设在API和应用程序之间的另一个中间层;用来为各种不同平台的应用程序创建与他们兼容的运行环境