一.软硬件基本知识linux
1.在计算机多如牛毛的硬件部件中最重要的三个是:中央处理器(CPU)、内存和I/O芯片。下图为现代计算机的硬件结构框架程序员
PCI bridge被称为北桥,是为了让内存等设备可以跟上CPU的频率。ISA bridge为南桥,让低速设备能够链接到北桥上。编程
2.计算机软件的体系结构:windows
应用程序调用运行库提供的应用程序编程接口,而运行库调用操做系统提供的系统调用接口。数组
3.目前全部的操做系统对CPU的分配方式都是以抢占式进行的,每一个程序会以进程的方式运行,每一个进程有本身独立的地址,每一个进程根据本身优先级的高低都有机会获得CPU资源,但当CPU运行超过超过一段时间后,就会让给其它等待的进程。若是操做系统分配给每一个进程的时间都很短,那么CPU会频繁的切换任务,从而形成多任务同时运行的假象。数据结构
4.硬件驱动来搞定硬件操做的繁琐细节,一般由硬件厂商开发,一般应遵照操做系统的提供的接口跟框架。硬件驱动能够看作是操做系统的一部分,它与操做系统内核一块儿运行在特权级。多线程
5.硬盘基本知识:一个硬盘每每有多个盘片,一个盘片有两面,每面按照同心圆划分为多个磁道,每一个磁道划分为若干扇区,一个扇区通常为512字节。每一个扇区有一个逻辑编号并发
6.程序与内存:app
地址空间能够分为两种:虚拟地址空间和物理地址空间,物理地址空间是在计算机中实实在在存在的、惟一的内存地址,虚拟空间是指虚拟的、想象出的地址,每一个进程都有本身独立的虚拟地址空间,这是程序隔离的方法。框架
(1)最开始,人们采用分段的方法将一段程序所需大小的虚拟空间映射到物理空间,一个字节对应一个字节的严格映射,例如:
这样虽然隔离了程序,但内存使用效率过低。
(2)分页的方法后来被发明,其原理是将地址空间人为的划分为页(page,大小由硬件或操做系统决定,大部分为4k),下面举一个简单例子:
设1页的大小为1KB,设两个程序的虚拟空间地址有8KB,即8个虚拟页:
假设这是一个32位的电脑,即拥有2^32个物理寻址能力(4G),但假设目前只有6KB的内存(6个物理页可用),当咱们将进程里虚拟空间按也划分,经常使用的代码或数据页装到内存,不经常使用的装到磁盘(磁盘页中),须要时再取出来。上图中的process1的VP0/1/7倍映射到了物理页PP0/2/3,VP2/3却存储在了磁盘的DP0/1(磁盘页)中,而其余的注入VP4等可能尚未被用到过。
不一样的进程可用将本身的虚拟页映射到同一物理页,实现了内存的重用,当进程须要使用DP1的数据或代码时,操做系统会复负责将其从磁盘中调取出来到内存,并为其与VP3创建映射关系。
下面是虚拟存储的实现方式:
7.线程:基本组成包括线程ID、当前指令指针(PC)、寄存器集合和栈堆。
(1)从C/C++的角度来说数据在线程中是否私有的关系以下:
单个处理器多线程是一种模拟出来的状态,多个处理器中,当线程数量小于处理器个数时,是真正的并发运行,当线程数超过处理器个数时,会存在线程调度,这时线程也是一种抢占式的方式占有处理器一段时间(时间片)后释放、等待。
注:抢占的含义就在于运行完指定的时间后会强制释放CPU资源。
(2)线程的三种状态,及其切换:
(3)优先级,通常状况下,线程都是有优先级的,这个能够由开发者设定,同时操做系统也会为线程设定优先级,IO密集型线程的优先级大于CPU密集型的线程。IO密集型线程会频繁进入等待状态,不耗CPU。
另外长时间得不到执行的线程也会被提升优先级。
(4)线程模型:大多数操做系统都在内核中对线程进行了支持(内核线程),可是在用户开发的应用程序中的线程(用户态线程)并不必定对应一个内核线程。用户态线程与内核线程的对应关系有三种模型:
一对1、一对多、多对多。通常操做系统API建立的都是一对一线程,如windows的createTread();
2、编译与连接
1.gcc编译过程分解:
(1)预编译的过程包括:展开宏,展开#include引用的h文件(递归进行的),处理条件预编译指令:#if、#ifdef...,删除全部注释,添加行号和文件标识,保留全部#pragma指令。
(2)编译:一系列词法分析、语法分析、语义分析和优化后生成汇编代码文件。
(3)汇编:汇编器将汇编语言转化为机器能够执行的机器指令(汇编后即是目标代码,存在目标文件中)。
(4)连接:将独立编译的源代码模块组装起来,即将模块之间相互引用的地方处理好。
2.编译:扫描(词法分析)->语法分析->语义分析->源代码优化->代码生成->目标代码优化
3.连接:
不一样的模块(即不一样的文件)之间编译是相互独立的,当文件A调用了文件B中定义的全局变量n时,在编译A的时候,并不知道n的具体地址(前面说的虚拟地址),所以用0代替。当B编译完成后,知道了n的具体地址,连接开始进行时,在A中调用n的地方将n的地址替换回去,这个过程叫作重地位。
库就是一些目标文件的包,最基本的即是系统的运行时库,它是支持函数运行的基本函数集合,咱们本身写的程序每每被编译成目标文件后,都要与运行时库连接后运行。
三.目标文件
1.目标文件就是通过编译后,但未进行连接的那些中间文件(windows下的.obj和linux下的.o,又叫可重定位文件),它的格式和最终的可执行文件格式(windows下的exe和linux下的ELF可执行文件)采用同一种格式。同时动态连接库(windows下的.dll和linux下的.so)和静态连接库(windows下的.lib和linux下的.a)文件都按照可执行文件格式存储。静态连接库稍有不一样,它将多个目标文件捆绑在一块儿造成一个文件并加上一些索引。
2.目标文件组成:
源代码编译后代码存放在代码段(名字为.code或.text),数据存储在数据段(.data),还有只读数据段(.rodata)存放常量用的,以下面程序中的字符常量“%d\n”,这些段也是要按page对齐的,一个简单程序编译后结果如图:
目标文件(或可执行文件等)的file header描述了整个文件的属性:包括文件是否可执行、是静态链接、是动态连接?,入口地址(若是是可执行文件),目标硬件,目标操做系统等信息。还包括一个段表,用于描述文件中各个段的数组,描述了各个段的偏移位置以及属性。初始化的全局变量和局部静态变量存储在数据段,但未初始化的全局变量和局部静态变量存储在一个叫“.bss”的段里,因为未初始化的变量在程序中都默认为0,因此在.data里都存放一些0是没有必要的撒。在程序运行时这些变量是要占内存的,但在文件中,咱们只记录全部未初始化的全局变量和局部静态变量所需空间的总和,.bss段至关因而位置的预留,并无内容,在文件中不占据空间。bss是不占用.exe文件空间的,其内容由操做系统初始化(清零),好比int a[100],在可执行文件中没有记录100个0,而只是记录了a符号和a所用内存的大小,程序开始运行后,才会在内存中申请这么大的地方。bss段的大小存储在段表里。
将数据与指令(函数)分开存储,便于分开装载,便于同一程序多个副本同时运行时指令的共享,但数据的独立,以节省内存。
3.ELF可执行文件格式(linux下的可执行文件就是这个格式,windows下是PE,他们都是COFF的变种,因此很相似)。
(1)文件头(ELF header)包含:ELF魔数(确认文件类型,ELF的16进制型)、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型(可重定位文件,即编译后造成的目标文件(.o),可执行文件,共享目标文件)、硬件平台、硬件平台版本、入口地址、程序入口和长度等。
(2)段表(header section table),描述ELF各段的信息(段名、段的长度、在文件中的偏移、读写权限及其余属性),编译器、连接器和装载器都是依靠段表来定位和访问段的属性的。其结果以下图:
段表的字段包括(依次为:名字、类型、段的标志位(是否可写?可执行?须要分配空间?)、地址、偏移、大小、段的连接信息(sh_link/sh_info)。。。):
(3)重定位表:.rel.text,专门记录须要重定位的位置,若是.data中有数据须要重定位,重定位表为.rel.data
4.符号表(段名:.symtab):连接的关键,尤为是全局变量和函数的符号名
表示一个符号的结构体(Elf32_Sym):
5.强符号与弱符号
(1)对于C/C++来讲,编译器默认函数以及初始化了的全局函数为强符号,未初始化的全局变量为弱符号。位于不一样文件的两个强符号名字相同,连接时会报错。GCC中_attribute_((week))能够定义任何一个强符号为弱符号:
(6)强引用与弱引用
连接时,若是没有找到该符号,会报符号未定义错误,这就是一种强引用,相反,不会报错则为弱引用,对于未定义的弱引用,连接器默认它为0.
四.静态链接
1.静态连接就是将几个输入目标文件加工合并成一个输出目标文件,那么输出的目标文件中的空间是如何分配给几个输入目标文件的呢?
首先,须要解释一下这里的分配地址与空间,这里的地址与空间有两层含义:一.输出到可执行文件的中的空间;二.装载后的虚拟地址中的虚拟地址空间。对于包含实际数据的段如.text和.data等,他们在文件和虚拟空间中都要分配空间,所以他们在这二者中都存在。而对于.bbs这样的段来讲,分配的意义仅限于虚拟地址空间的分配,由于它在文件中并无内容。事实上,咱们这里说到的空间分配只关注于虚拟地址空间分配(连接时才分配),这个关系到连接器关于地址计算的步骤。
有2种分配方案:
(1)按序叠加,当输入的文件过可能是,会产生不少零散的段,因为段要页对齐的缘由,还会产生大量空间浪费。
(2)类似段合并,如今连接器基本都有采用这种策略,这种策略链接步骤分为两步:
第一步:空间与地址分配。获取全部文件全部段的属性和长度,获取全部符号表并统一辈子成全局符号表,计算合并后各个段的长度与位置,并创建映射关系。
第二步:符号解析与重定位(连接的关键)。使用上步的信息,获取文件中的数据与重定位信息,并进行符号解析与重定位、调整代码中的地址。
当一个目标文件声明或调用了其它文件里的变量或函数时,编译时赋予一些如0x00000000或0xFFFFFFFC来代替,连接的时候,将全部的目标文件合并,重定位便会将真正的地址去替换。重定位信息包含在重定位表(段)里。
2.C++语言的特有问题(关于连接器的)
(1)重复代码消除。C++因为支持模板、外部内联、虚函数表等特性,会产生大量的重复代码,如模板,他或许会在不一样的编译单元被实例化。重复代码会形成空间浪费、地址易出错、指令运行效率低等。
目前主流的作法是:每一个模板的实例都存在单独的段里(如:.temp.add<int>与temp.add<float>),连接的时候判断需不须要将相同代码合并。
目前一些连接器还支持函数级别的连接,在库文件很大,但只用其中一两个函数的时候很实用,其原理是丢弃了用不到的函数,能够有效减少可执行文件的大小,但减慢了编译与连接的速度。(也就是在exe中只加载lib里用到的函数)
3.ABI(application binary interface)指的是符号修饰标准、变量内存布局、函数调用方式等跟可执行代码二进制兼容性相关的内容。其影响因素包括:硬件、语言、编译器、连接器、操做系统等。
因为ABI差别的问题,让不一样编译器产生的结果很难连接到一块儿。 C++最为人诟病的即是这种兼容性问题。
4.连接过程控制的方法有:(1)命令行的方式(如linux下使用Id命令时加 -o,-e等)(2)编译指令存储在目标文件的特定段里(如windows采用的PE/COFF里的.drectve段)(3)使用连接控制脚本
五.windows COFF/PE
1.windows引入了PE格式的可执行文件(是COFF的一种扩展),其实与ELF同样也是来源与COFF,所以PE与ELF格式很是类似,在windows中目标文件默认为COFF,可执行文件为PE。PE文件在装载的时候是被直接映射到虚拟空间中运行,它是虚拟空间的映像,因此PE可执行文件也被称为映像文件。
同时,PE也是基于段的,一个PE文件至少要包含代码段:.code,同时在程序中也能够自定义段名,如:
#pragma data_seg(".FOO")
int global -0;
#pragma data_seg(".data")
表示将全局变量global放在.FOO里,而后再切换回.data.
如下为COFF格式:
跟前面讲的ELF文件同样,段表仍然记录的是各个段的信息,如:段名、物理地址、虚拟地址、原始数据大小、段在文件中的位置、标志位等等。
2.COFF中的大部分段都与ELF文件类似,惟有两个是独特的:
(1)连接指示信息(.drectiva)包含了编译器想传递给连接器的指令。好比说告诉连接器要使用哪一个库。
(2)调试信息。
3.PE文件结构
它与COFF相比,它的开头不是COFF的都文件而是DOS MZ可执行文件头和桩代码(很大部分是历史遗留问题,为了兼容DOS);而COFF原来的头文件被扩展为PE头文件:
六.可执行文件的装载与进程
1.进程的虚拟地址空间。32位CPU下,程序的虚拟空间不能超过4GB,由于32位CPU只能使用32位指针,其寻址范围为0-4GB。可是从硬件层面来讲,原先的32位地址线只能访问4GB物理内存,但Intel公司将地址线拓展为36位,并修改了页映射的方式能够访问更多物理内存(可达64G),这种地址扩展方式叫作PAE。
2.因为一般状况下程序所需内存大于物理内存,所以静态装载确定不合适,根据程序的局部性原理,咱们只须要将经常使用的部分装入内存便可,即动态装载。有两种动态装载的方法:
(1)覆盖载入,在虚拟内存没发明前普遍使用,现已被淘汰。这种方式内,分割程序的工做是程序员完成的,晕...
(2)页映射,内存被划分为页,程序的地址空间也被划分为页。
3.进程建立的过程:
(1)建立独立的虚拟地址空间。并非真正的建立一块实在的空间,而是建立映射函数所需的相应的数据结构,好比页目录。
(2)读取可执行文件头,创建虚拟空间与可执行文件的映射关系。其关系如图所示:
很明显,这种映射关系只是保存在操做系统内部的一个数据结构。
须要注意的是这里只是可执行文件和虚拟页之间的映射关系,虚拟页与物理页之间的映射会在页错误时发生。
(3).将CPU指令寄存器设置成可执行文件入口,启动运行。
4.页错误
上面的步骤执行完后,并无任何程序装载到内存,只是创建了虚拟地址空间与可执行文件的映射,并指定了程序的入口,当CPU开始执行这个入口指令的时候,发现是一个空白页,这被认为是一个页错误,发生页错误后,CPU将控制权交给操做系统,操做系统会查找装载时创建的那个数据结构,找到应该被加载进来的程序虚拟地址空间,并分配物理页面,创建虚拟页与物理页的映射,进而将程序指令加载进来。随着进程的执行,会有页错误不断发生。
5.进程虚拟存储的分布
(1)因为映射都是以页为单位的,所以为了不空间地址被过多浪费,能够将相同权限的段合并成一个段进行映射。段的权限主要有三种:可读可执行(如:.text等)、可读可写段(如.data,.BBS等)、只读(只读数据段等)。合并的段被称为segment,他们在一块儿映射以后,在虚拟空间中只有一个地址呦。因此根据section(段)和segment划分可执行文件,能够被称为不一样的视图(view),从section来看就是elf的连接视图,从segment来看就是elf的执行视图。
(2)堆和栈。一个进程中的栈和堆都有对应的虚拟空间地址。C语言中的malloc()是从堆里分配的。一个进程包含如下几种VMA(虚拟空间地址),讨论segment,基本也就指这几种VMA:
程序的运行是根据虚拟空间(可执行文件的一种映射)来进行的。
七.动态连接
1.静态连接对计算机内存和磁盘空间的浪费严重,例如,linux中一个程序所需的C语言静态库至少1M,那么若是机器中运行着100个这样的程序,就要浪费近100M内存空间。若是磁盘中有两千个这样的程序文件,得占2G磁盘。
也就是说同一个目标文件被两个程序都静态连接时,它会在内存和磁盘中出现两个副本,这就是一种浪费。
另外,若是对任意一个静态连接库进行修改,那么整个程序就要所有从新连接,这不利于程序的发布,所以那种万年不会变的库采用动态连接。
2.动态连接的基本思想是将程序的模块拆分红相对独立的模块(主程序(可执行文件)、动态连接库都是模块),当程序运行时才将他们连接在一块儿造成完整的程序。而不像静态连接一个将全部须要的模块都连接成一个完整的可执行文件。当某一个动态连接库被加载到内存中后,若是其余程序在运行过程当中也须要加载它,那么直接连接已经在内存中存在的动态连接库就能够了,这样一个动态连接库老是在内存中只有一个副本。动态连接让程序开发更加灵活。
3.动态连接的过程:程序编程成目标文件->静态连接造成可执行文件,同时将程序中须要动态连接的符号标记一下->将可执行文件与动态连接库(linux中叫动态共享对象dso)装载连接运行。
这里须要注意的是,静态连接(连接器)过程当中,动态连接库仍然会被做为输入文件之一,由于连接器将会利用它的符号表将程序中须要动态连接的地方作标记。
4.动态连接库的虚拟地址空间没法预先固定,举个例子:某个程序,模块A(多是动态连接库或可执行文件)的地址为0x1000-0x2000,模块B的地址为0x2000-0x3000,另一我的写了一个程序,要调用调用A里的函数,但不调用B,这时对于改程序而言,0x2000-0x3000这块地址是空闲的,因而程序将这块地址分配给一个开发的新的模块C,若是其余程序要调用B和C时会发生严重的目标地址冲突。
为了解决这个问题,程序中动态连接对象的虚拟地址肯定应在动态连接库装载完成后,在进行重定位。当动态连接库装载地址肯定后,系统会对目标程序中全部标记了动态连接对象的地方进行重定位。例如,当动态连接库被装载到进程虚拟空间的0x10000000地址后,假设其foo()函数位于0x100000100处,这时系统将遍历目标程序的重定位表,将全部调动foo的地方所有替换为0x100000100。这种重定位原理与静态连接的重定位同样,静态连接是:程序编译时不知道的指令地址在链接时重定位,动态连接是:连接后仍不知道的指令地址在装载时重定位。
我以为上述重定位的过程只是把原来在静态连接时的重定位延后到装载时进行了,其他并无什么区别。
这种方法有一个问题就是不一样的进程之间将不能共享指令部分,缘由是重定位时有指令会被修改,好比,模块A调用了模块B,模块B重定位后须要修改A的指令,其余进程调用A时,显然不能共享前面已经装载的A。这样丧失了其节省内存的优点。
5.为了解决上述问题,使用一种地址无关代码的技术。
咱们根据各类类型的地址引用方式来分别介绍代码无关技术:
(1)模块内部函数的调用、跳转等。这个最简单,模块内部的函数调用处于同一模块,相对位置固定,能够直接利用相对地址调用。所以这自己就是一种地址无关的代码。
(2)模块内数据访问。虽然代码段占若干页、数据段占若干页,但他们的页之间的相对位置也是固定的。这也是一种地址无关代码。
(3)模块间的数据访问。因为要访问的数据被定义在另外的模块,只能在装载的时候再肯定,为了使指令部分的地址与代码无关,将与地址有关的代码所有放到数据段里面。这样数据段(包含一部分代码)就是地址相关的咯,而代码段为地址无关的。ELF文件会在数据段里面创建一些指向须要调用的外部变量的指针数组,称为全局偏移表(GOT),当代码须要该全局变量(或定义在其它模块的静态变量)时能够经过GOT间接引用。每一个变量的地址在GOT中占4字节,装载完成的时候,连接器会找到这些变量的地址,将它们填充到GOT。因为GOT放在数据段,因此即便它在模块装载时须要修改也不受影响,由于每一个进程中,被调模块的数据段老是有独立的副本。
(4)模块间的调用、跳转等。方法与上面相似,只不过GOT中保存的是函数的位置。
地址无关的共享对象叫作PIC,linux能够在编译时指定参数 -fPIC来实现。同理还能够实现地址无关的可执行文件PIE。
6.动态连接的过程
(1)动态连接器的自举。首先动态连接器自己也是一个共享对象,首要工做是先将本身重定位。
(2)装载共享对象。a.动态连接器将可执行文件和连接器自己的符号表合并成全局符号表。b。动态连接器寻找可执行文件依赖的共享对象,并将它们的名字放入到装载集合中。c.连接器开始从装载集合中取出一个名字,打开该文件,将其代码段和数据段映射到进程的内存空间中来。新共享对象的符号表会与全局符号表合并。d.判断如若该共享对象还依赖于其余共享对象则对其进行上述循环。
(3)重定位与初始化。
以上三个过程完成后,动态连接器就会将进程的控制权转交给入口程序。
八.windows下的动态连接
dll文件和exe文件其实是一个概念。dll与so相比更加注重模块化设计,使得模块之间能够松散耦合、重用和升级,Windows上大量的软件都是经过升级dll进行完善,微软常常将这些升级补丁累计到一个软件升级包,如office、VS、甚至windows操做系统等。
九.程序的内存布局
1.通常来说,应用程序在内存中有如下“默认”区域:
(1)栈,用于维护函数调用上下文。一般在程序的最高地址处分配,一般有数兆字节。windows默认一个线程是1M的栈
(2)堆,用来容纳应用程序动态分配的内存区域(malloc,new),堆通常比栈大不少几十甚至数百兆。
(3)可执行文件映像,存储可执行文件在内存中的映像(注意,采用的是页映射),包括代码段、数据段等。
(4)保留区,并非指一个单一的区域,而是内存中受到保护而禁止访问的内存区域总称。
(5)动态连接库映射区,用于映射装载的动态连接库。
下图是一个典型的linux内存分布图:图中的箭头表明大小可变区域的尺寸增加方向。
2.栈。一般保存了一个函数调用所须要的维护信息,包括:
(1)函数的返回地址与参数。
(2)临时变量。包括函数的非静态局部变量,以及编译器生成的临时变量。
(3)保存的上下文。包括函数调用先后须要保持不变的寄存器。
每个函数都有一块栈区,咱们称之为栈帧。
下面说说函数p调用函数q时的具体状况。当执行call q(y1)时,会为函数q建立一个新的栈帧,具体过程是:先保存上一帧的地址,若是有返回值的话为返回值分配存储空间,而后保存返回地址。而后为y1分配空间并把它初始化为调用q时给的参数。接着分配另外一个参数的空间y2,这个参数用于在函数内部计算。
3.堆。申请的堆在询空间中是连续的,但在物理空间中就不必定是连续的了。
十.运行库
1.C/C++程序运行步骤:
(1)操做系统建立进行,将控制权交给入口函数,注意这里入口函数指的并非main函数,每每是运行库中的某个入口函数。
(2)入口函数对程序的运行环境和运行库进行初始化,包括堆、线程、I/O、全局变量构造等等。
(3)入口函数在初始化完成后调用main函数,执行程序主体。
(4)main函数执行结束后,返回入口函数,进行各类清理工做。