缺点:
以程序为单位,对内存进行映射,如将A的0-1G地址对应内存中某个1G的内存,可以做到隔离和地址确定,但内存使用率低。
如果内存不足,换出的是整个文件。根据程序局部性原理,一个程序运行时,某个时间段只用到了一小部分数据,大部分数据都没用到,需要更小的粒度分割和映射
对程序的数据和代码段进行分割,常用的放到内存,不常用的扔在磁盘,需要的时候放入内存。
相同点:
采用离散分配方式,通过地址映射机构实现地址变换
不同点:
段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
源程序送入扫描器,运用有限状态机简单进行词法分析,输出一系列token,例如:
生成的词一般分为:关键字,标识符,字面量(数字 字符串等),特殊符号
对扫描器产生的记号进行语法分析,通过上下文无关语法,生成以表达式为节点的语法树,
上下文相关语法:
是乔姆斯基(Chomsky, N.)引进的.设G=(V,T,P,s)为一个短语结构文法,若限定式中的所有产生式a->b都满足下列条件:b的长度不小于a之长度.则称G为上下文相关文法.由上下文相关文法产生的语言称为上下文相关语言.上下文相关语言都是递归的,但反之不然.
上下文无关语法:
上下文无关文法(英语:context-free grammar,缩写为CFG),在计算机科学中,若一个形式文法G = (N, Σ, P, S) 的产生式规则都取如下的形式:V->w,则谓之。其中 V∈N ,w∈(N∪Σ)* 。上下文无关文法取名为“上下文无关”的原因就是因为字符 V 总可以被字串 w 自由替换,而无需考虑字符 V 出现的上下文。一个形式语言是上下文无关的,如果它是由上下文无关文法生成的(条目上下文无关语言)。
语法分析仅仅完成了对表达式的语法层面的分析,不了解语句是否真的有意义,例如C语言两个指针相乘,合法但无意义。语义分析由语义分析器完成,编译器能完成的是静态语义,即在编译期间能确定的语义信息,相对于的动态语义只有在运行期才能确定的语义。
经过语义分析,整个语法树的表达式被标识了类型。
优化一些在编译时期可以确定的表达式,比如2+6就被优化成8减少表达式数量,输出的结果是中间代码
中间代码转化为目标机器代码,依赖于目标机器的配置,如字长,寄存器,整数类型 浮点类型等,目标机器代码可以是汇编形式。
对目标机器代码进行优化,比如选择合适的寻址方式,比如使用移位来代替乘除法,删除多余的指令,等等。最终的结果是汇编代码,但此时的一些变量的地址仍未确定,需要等等链接确定。
gcc -c hello.s -o hello.o
例如A中引用了B的fun函数,在编译A时并不知道fun的地址,需要将fun地址在链接的时候填入。(如何知道这是外部的地址?通过头文件。具体的代码在什么地方?在B文件生成的库文件里。)
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。链接是由叫链接器(linker)的程序自动执行的。
ELF文件 executable linkable file,链接前的.o,链接后的可执行文件以及静态库动态库均是ELF格式
ELFheader。描述整个文件基本属性,如ELF文件版本,目标机器型号,程序入口地址等。
.rel.text text段的重定位表 对于每个需要重定位的代码段和数据段,在重定位表进行存储。
.rel.data data段的重定位表
.text 代码段
.data 数据段 已初始化的全局变量和静态变量
.comment 注释信息,例如编译器版本信息等
.rodata 只读数据,const变量和字符串常量等
.bss 未初始化的全局变量和局部静态变量
section table 段表 除头之外最重要的结构,描述每个段的信息,如段名,段的长度,文件中的偏移,读写权限及段的其他属性。编译器,链接器和装载器都是依据段表访问段。
strtab/shstrtab 字符串表/段字符串表
symtab 符号表 链接过程即不同文件间函数和变量地址的引用,对于不同的符合,将其存储至符号表便于链接。
.init与.finit 程序初始化与终结代码段,可以用于C++的析构和初始化等
ld a.o b.o -e main -o ab
生成静态库,需要先对源文件进行汇编操作 (使用参数 -c) 得到二进制格式的目标文件 (.o 格式), 然后在通过 ar 工具将目标文件打包就可以得到静态库文件了 (libxxx.a)。
使用 ar 工具创建静态库的时候需要三个参数:
参数c:创建一个库,不管库是否存在,都将创建。
参数s:创建目标文件索引,这在创建较大的库时能加快时间。
参数r:在库中插入模块 (替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。
# hello.o生成libmyhello.a静态库
ar -crs libmyhello.a hello.o
会输出全局符号表,这里的映射是与虚拟内存地址的映射,但此时还是没加载到内存的,不考虑生成的磁盘上的可执行文件的内部存储
依据ELF文件的重定位表和第一步输出的全局符号表对需要重定位的数据或代码进行查找和重定位(偏移量),通过绝对地址修正或相对地址修正进行地址改写。
动态链接就是将链接的过程推迟到运行时在进行。使得每个库文件在内存和磁盘上都只有一个副本,共用同一个库内存的好处是减少内存的换入换出,增加cpu缓存命中率,程序可扩展性和兼容性强。性能略有下降,但这点性能换来空间节省和灵活性,可接受的。
# a.c文件生成 a.so动态库
gcc -fPIC -shared -o liba.so a.c
#使用动态库
gcc main.c -o main -L ./ -la
#其中-L指明动态链接库的路径,-l后是链接库的名称,省略lib
地址无关代码为了解决装载时动态模块有绝对地址引用的问题,且希望共享的部分在装载时可以共享内存,不因装载地址的改变而改变。
由于装载时重定位的指令部分无法共享的确定,由此产生了地址无关码
对于内部数据/函数的访问,直接通过相对寻址方式在运行前即可确定
对于外部数据/函数,往往意味着对全局变量的访问,对于在全局对象中定义的变量来说,这些符号的地址与模块装载地址有关,elf使用GOT(全局偏移表)来对这些变量进行间接引用需要在装载时确定。称之为全局偏移表(Global offest Table)。
模块在编译时,可以确定内部变量相对于当前指令的偏移,即编译时可以确定GOT的偏移。在加载时,动态链接器会重定位GOT中的每个条目,使他们指向正确的地址
对于全局/静态的数据,由于不能事先确定是否引用了外部的库,所以也使用GOT处理
即使使用了地址无关代码,动态链接仍存在问题:
为了解决第二个问题,提出了延迟绑定,使用PLT(Procedure Linkage Table,过程链接表)实现,基本思想是当函数第一次被用到时才进行绑定(符号查找,重定位等),没有用就不绑定。实现思路是在GOT前再加一层间接跳转,调用函数不通过GOT直接跳,而是通过PLT,PLT记录了每个函数对应的地址。
PLT简单实现,假设调用 bar() 函数,在PLT中存在与其对应的bar@plt。
bar@plt初始时存储的是下一条指令的地址,所以初始时会进行一次绑定,之后直接跳转到调用函数的地址。
bar@plt:
jmp *(bar@GOT) //如果是第一次链接,该语句的效果只是跳转到下一句指令。否则,将会跳转到 bar()函数对应的位置
push n //压栈 n,n 是 bar 这个符号在重定位表 .rel.plt 中的下标
push moduleID // 压栈当前模块的模块ID,上述例子中的 liba.so
jump _dl_runtime_resolve() //跳转到动态链接器中的地址绑定处理函数
ELF将GOT拆为了两个表叫做“.got”,“.got.plt”。其中 .got 用来保存全局变量的引用地址,.got.plt 用来保存函数引用的地址,也就是说,所有对于外部函数的引用被分离到了 .got.plt 表。
plt表的前三项:
动态链接器是所有程序运行时的代码入口,动态链接器本身也是一个共享对象,但它不能依赖于其他对象。
从ELF文件头和dynamic中得到依赖的所有共享对象集合,找到相应的共享对象映射到进程空间,若共享对象有依赖就将依赖的也放入集合中,整个装载的过程是广度优先搜索的过程。当对象被装载后,符号表会合并到全局符号表,当所有的共享对象都装载后,符号表包含所有符合。
装载共享对象完成后,链接器开始遍历可执行文件和各个共享对象的重定位表,将GOT/PLT的内容进行修正,之后依据地址无关代码和延迟绑定进行重定位。
eax寄存器存储返回值。但eax本身只有四字节,若大于四字节,则在调用函数前的函数栈内申请temp中间内存,在调用函数内部将得到的结果拷贝到temp中,之后返回后将temp的内存拷贝到返回的结果中。需要两次拷贝。
初始 mmap 系统调用可以将用户空间的虚拟内存地址与文件进行映射(绑定),对映射后的虚拟内存地址进行读写操作就如同对文件进行读写操作一样。不映射到文件则可以用作堆空间。
为了让应用程序访问操作系统的资源或借助操作系统完成相应行为,操作系统为应用程序提供一些接口供其使用。
进程运行时,有两种不同的特权级别,内核态和用户态。用户态程序通过中断从用户态切换到内核态。
什么是中断?中断是一个硬件或软件发出请求,要求CPU暂停当前的手头工作转手去处理更加重要的事情。
中断具有两个属性,中断号和中断处理程序,不同中断具有不同的中断号,内核有一个中断向量表,包含了指向指定中断号的执行函数的指针,中断到来,中断向量表查找相应代码,执行中断代码,之后返回继续原先工作。
中断有两类:硬件中断和软件中断。硬件中断包括电源掉电,键盘被按下等。软件中断通常是一条带有中断号的指令,用户可以手动的触发。在windows下,系统调用的中断号是int 0x2e;linux下是int 0x80。
由于中断号宝贵,所有多个系统调用的接口都是使用同一个80中断号。如何区分不同的系统调用?通过EAX寄存器,EAX寄存器中断调用前可以传递系统调用号,调用结束后可以传递返回结果。