基础知识
介绍:
来自于学习知识与《程序员的自我修养》与hollk师傅的博客
https://hollk.blog.csdn.net/?type=blog
汇编:
机器语言:机器指令的集合
汇编语言组成:
1、汇编指令(机器码的助记符)
2、伪指令(由编译器执行)
3、其他符号(由编译器识别)
基本指令:
push:压栈
pop:弹栈,从esp指向的内存地址获得数据,将其加载到指令寄存器中(通常是一个寄存器),然后将esp+4
函数调用:
x86:
函数头:
push ebp;将ebp压入栈中===old ebp===》为了在函数返回时恢复以前ebp的值
mov ebp,esp;ebp=esp====》此时ebp指向栈顶,此时栈顶就是old ebp,这里是为了保存esp本来的位置==》注意,ebp是一直指向栈顶的
可选:sub esp,xxx;开栈,在栈上分配xxx字节的临时空间
可选:push xxx:保存名为xxx的寄存器====》保持某些寄存器在函数调用前后保持不变
返回:
可选:pop xxx:恢复保存的寄存器
mov esp,ebp;==》esp=ebp==》恢复esp的同时回收局部空间
pop ebp; 恢复保存的ebp
ret;:从栈中取得返回地址,并跳转到该位置
函数调用约定:调用函数前要先把参数压入栈再传递给函数
0x86/0x64区别:
64位程序传参===》rdi,rsi,rdx,rcx,r8,r9
系统软件介绍:
管理计算机本身的软件称为系统软件:
分为两部分:
1、平台性:例 操作系统内核,驱动程序,运行库,系统工具
2、程序开发:编译器,汇编器,链接器等开发 工具和开发库
系统软件体系结构:
应用程序: | 开发工具:
网络浏览器,视频播放器 | c/c++编译器,汇编程序
文本编辑器,图片编辑器 | 库调用工具,调试工具
电子邮件客户端…… | 开发库……
-----------------操作系统应用程序编程接口------------------
运行库
----------------------系统调用接口------------------------
操作系统内核
-------------------------硬件规格-------------------------
硬件
每个层次之间的相互通信====》通信协议===》接口
接口的下层是接口的提供者,上层是接口的使用者,每一个中间层第是对它下面那层的包装和拓展
在软件体系中,位于最上层的是应用程序
从整个层次结构===》开发工具与应用程序同属一个层次===》都使用操作系统应用程序编程接口
应用程序接口的提供者是运行库===》例:Linux下的glibc库提供POSIX的API
运行库使用操作系统提供的系统调用接口====》以软件中断的方式提供
====》例:ret2sys,使用0x80号作为系统调用接口
操作系统内核层使用硬件层的硬件接口,而硬件是接口的定义者,硬件接口决定了操作系统内核===》这种接口称为硬件规格
操作系统对计算机资源的使用:
操作系统就是为了能够充分使用计算机中的资源,主要是cpu,存储器(内存和磁盘),I/O设备
1、cpu:
多道程序:在某个程序不使用cpu时,把cpu资源分给其他等待使用的程序
分时任务:程序运行模式变成写作模式,每个程序运行一段时间后主动让出cpu给其他程序
多任务:操作系统接管所有硬件资源,本身运行在一个受硬件保护的级别,所有应用程序都以进程的方式运行在比操作系统权限更低的级别,每个进程都有自己独立的地址空间,进程之间需要相互隔离
抢占式:操作系统掌管cpu资源,每个进程根据优先级高低都有机会得到cpu资源,但在运行一定时间后操作系统会暂停程序,强制剥夺cpu资源并分配给他认为目前最需要的进程
2、硬件驱动:
在UNIX中,硬件设备的访问形式跟普通文件形式一样,繁琐的硬件细节全部交给操作系统中的硬件驱动程序来完成
驱动程序可以看作是操作系统的一部分,它和操作系统内核一起运行在特权区,但是又与操作系统内核之间又一定的独立性
3、cpu利用率问题:
地址空间不隔离:
所有程序都可以直接访问物理地址,程序所使用的内存空间不是相互隔离的,恶意程序可以改写其他程序的内存数据,非恶意程序也会不小心修改其他程序的数据导致崩溃
内存利用效率低:
多程序执行任务切换时,磁盘的读写数据量比较大,大部分的时间都用来换入和换出,导致效率十分低下
程序运行地址不确定:
程序需要装入运行时,从内存中分配空间区域的位置不确定,设计程序的重定位问题
4、解决问题:
添加中间层,把程序给出的地址看作是一种虚拟地址,通过某些映射方式,将整个虚拟地址转成实际的物理地址
隔离:
通过让程序运行在虚拟地址让所有的程序都以为自己运行在物理地址上,实现隔离
分段:
把一段与所需要的内存空间大小的虚拟空间映射到某个地址空间,解决了地址空间不隔离和程序运行地址不确定的问题
分页:
把地址空间人为地分为固定大小的页,每一页的大小由硬件决定,大部分PC上的操作系统都是4KB的页
接下来可以把进程的虚拟地址按页分隔,常用的数据和代码装载到内存中,不常用的代码保存到磁盘中,需要时再从磁盘中取出
进程经常被访问的数据或代码会被映射到物理页(PP,Physical Page)中
还未被访问的数据或代码暂时停留再虚拟页(VP,Virtual Page)中
不经常使用的数据或代码可以写进磁盘页(DP,Disk Page),需要的时候读出来并装入内存
分页===》资源共享,分类存放,不需要将全部的数据或代码都存放在内存中,提高内存的使用效率
线程:
有时被称为轻量化进程(LWP),是程序执行流程的最小单元,经常以多线程的方式进行工作
Linux多线程:
Linux内核中并不存在真正意义上的多线程,Linux将所有执行实体(无论是线程还是进程)都称之为“任务”(Task)
每一个任务概念上都类似于单线程的进程,具有内存空间,执行实体,文件资源等
不同任务之间可以选择共享内存空间,共享了同一个内存空间的多个任务构成一个进程,这些任务也就成了这进程的线程
目标文件:
目标文件从结构来说,是已经编译后的可执行文件格式,但是还没有经过链接的过程,和真正的可执行文件在结构上有区别,但目标文件一般和可执行文件格式一起采用一种格式存储,从广义上看,可以把目标文件与可执行文件看成一种类型的文件。
Linux的可执行文件格式主要是ELF,在Linux下通常将目标文件和可执行文件和可执行文件统称为ELF文件,不光是可执行文件。
动态链接库===》Linux的.so
静态链接库===》Linux的.a
ELF文件类型:
可重定位文件:
包含代码和数据,可以用来链接成可执行文件或共享目标文件,静态链接库可以归为这一类===》Linux的.o
可执行文件:
包含了可直接执行的程序,例:ELF可执行文件,一般没有扩展名===》/bin/bash文件等
共享目标文件:
包含代码和数据,分两种情况使用:一种是链接器可以直接使用着这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件;第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像得一部分来运行
核心存储文件:
当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件
目标文件主要构造:
文件头:File Header
文件属性,描述是否可执行,标明静态链接还是动态链接,入口地址(可执行文件),目标硬件,目标操作系统段表,描述了文件中各个段在文件中的偏移位置及段属性
代码段:.code / .text
程序源代码编译后的机器指令
数据段:.data
已初始化的全局变量和局部静态变量
BSS段:.bss
未初始化的全局变量和局部变量
程序源代码被编译后主要分为两种段:程序指令和数据指令
代码段属于程序指令
数据段和.bss段属于程序数据
真实文件结构:
除文件主要结构外:
只读数据段:.rodata
注释信息段:.comment
堆栈提示段:.note.GUN-stack
gcc处理异常处理段.en_frame
group段
段所有属性:
size:段的长度
File Offset:段所在的位置
CONTENTS:表示段在文件中存在
其他段:
.rotata:存放只读数据,例:字符串常量
.comment:存放的是编译器版本信息,比如字符串:"GUN:(GUN)4.2.0"
.debug:调试信息
.dynamic:动态链接信息
.hash:符号哈希表
.line:调试时的型号表,即源代码行号与编译后指令的对应表
.note:额外的编译器信息,比如程序的公司名,发布版本等
.strab:String Table字符串标,用于存储ELF文件中用到的各种字符串
.symtab:Symbol Table符号表
.shstrrab:Section String Table段名表
.plt/.got:动态链接的跳转表和全局入口表
.init/.fini:程序初始化与终结代码段
ELF文件总体结构:
ELF Header
--------------------
.text
--------------------
.data
--------------------
.bss
--------------------
other sections
--------------------
section header table
--------------------
string tables
stmbol tables
……
段与节:
代码段(text)包含代码和只读数据
.text
.plt
数据段(data)包含可读可写数据
.data
.got
.got.plt
.bss
未初始化的全局变量放在bss
局部变量放在栈中
文件头:
ELF魔数,文件机器字节长度,数据存储方式,版本,运行平台,ABI版本,ELF重定位类型,硬件平台,硬件平台版本,入口地址,程序入口和长度,段表的位置及段的数量
ELF文件头结构及相关常数被定义在“/usr/include/elf.h”中
32位与64位版本的ELF文件头内容一样,成员不一样
elf.h使用typede定义了自己的一套变量体系,表格内的就是结构体内定义成员的自定义类型
自定义类型 | 描述 | 原始类型 | 长度(字节) |
---|---|---|---|
Elf32_Addr | 32位版本程序地址 | uint32_t | 4 |
Elf32_Half | 32位版本的无符号短整形 | uint16_t | 2 |
Elf32_Off | 32位版本的偏移地址 | uint32_t | 4 |
Elf32_Sword | 32位版本有符号整形 | uint32_t | 4 |
Elf32_word | 32位版本无符号整形 | Int32_t | 4 |
Elf64_Addr | 64位版本程序地址 | uint64_t | 8 |
Elf64_Half | 64位版本的无符号短整形 | uint16_t | 2 |
Elf64_Off | 64位版本的偏移地址 | uint64_t | 8 |
Elf64_Sword | 64位版本有符号整形 | uint32_t | 4 |
Elf64_word | 64位版本无符号整形 | int32_t | 4 |
例:ELF32_Ehdr
typedef struct{
unsigned char e_ident[16];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
段表:
段表是一个以“Elf32_Shdr”结构体为元素的数组,数组元素的个数等于段的个数,每个“Elf32_Shdr”结构体对应一个段
“Elf32_Shdr”被称为段描述符
段描述符成员:
sh_name:段名
sh_type:段的类型
sh_flags:段的标志位
sh_addr:段虚拟地址
sh_offset:段偏移
sh_size:段的长度
sh_link和sh_info:段链接信息
sh_addralign:段地址对齐
sh_entsize:项的长度
段的类型(sh_type):段名只是在链接和编译过程中有意义,但是它不能表示真正表示段的类型
主要决定段的属性是短的类型(sh_type)和标志位(sh_flag),段的类型常量:
常量 | 值 | 含义 |
---|---|---|
SHT_NULL | 0 | 无效段 |
SHT_PROGBITS | 1 | 程序段。代码段、数据段都是这种类型的 |
SHT_SYMTAB | 2 | 表示该段的内容为符号表 |
SHT_STRTAB | 3 | 表示该段的内容为字符串表 |
SHT_RELA | 4 | 重定位表,该段包含了重定位信息 |
SHT_HASH | 5 | 符号表的哈希表 |
SHT_DYNAMIC | 6 | 动态链接信息 |
SHT_NOTE | 7 | 提示性信息 |
SHT_NOBITS | 8 | 表示该段在文件中没内容,比如.bss段 |
SHT_REL | 9 | 该段包含了重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_SNYSYM | 11 | 动态链接的符号表 |
段的标志位(sh_flag):表示在该进程虚拟地址中间的属性,段的标志位常量
常量 | 值 | 含义 |
---|---|---|
SHF_WRITE | 1 | 表示该段在进程空间中可写 |
SHF_ALLOC | 2 | 表示该段在进程空间中须要分配空间,有些包含提示或控制信息的段不须要在进程空间中被分配空间,它们一般不会有这个标志,像代码段、数据段、bss段都会有这个标志位 |
SHF_EXECINSTR | 4 | 表示该段在进程空间中可以被执行,一般指代码段 |
系统保留段:
名称 | 类型(sh_type) | 标志(sh_flag) |
---|---|---|
.bss | SHT_NOBITS | STF_ALLOC + SHF_WRITE |
.comment | SHT_PROGBITS | none |
.data | SHT_PROGBITS | STF_ALLOC + SHF_WRITE |
.data l | SHT_PROGBITS | STF_ALLOC + SHF_WRITE |
.debug | SHT_PROGBITS | none |
.dynamic | SHT_DYNAMIC | STF_ALLOC + SHF_WRITE 在有些系统下.dynamic段可能是只读的,所以没有SHF_WRITE标志位 |
.hash | SHT_HASH | SHF_ALLOC |
.line | SHT_PROGBITS | none |
.note | SHT_NOTE | none |
.rodata | SHT_PROGBITS | SHF_ALLOC |
.rodata l | SHT_PROGBITS | SHF_ALLOC |
.shstrab | SHT_STRTAB | none |
.strtab | SHT_STRTAB | 如果该ELF文件中有可装载的段需要用到该字符串表,那么该字符串表也将被装载到进程空间,则有SHF_ALLOC标志位 |
.symtab | SHT_SYMTAB | 同字符串表 |
.text | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR |
段的链接信息(sh_link,sh_info):如果段的类型是链接相关的(不论是动态链接还是静态链接),比如重定位表,符号表等,那么sh_link,sh_info这两个成员所包含的意义如下,对于其他类型的段,这两个成员没有意义:
类型(sh_type) | 链接信息(sh_link) | 链接信息(sh_info) |
---|---|---|
SHT_DYNAMIC | 该段所使用的字符串表在段表中的下标 | 0 |
SHT_HASH | 该段所使用的的符号表在段表的下标 | 0 |
SHT_REL | 该段所使用的的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_RELA | 该段所使用的的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_SYMTAB | 操作系统相关的 | 操作系统相关的 |
SHT_DYNAYM | 操作系统相关的 | 操作系统相关的 |
other | SHN_UNDEF | 0 |
重定位表:
一个重定位表是ELF中的一个段,这个段的类型是(SHT_REL),sh_link表示符号表的下标,sh_info表示它作用于哪个段
例:.rel.text作用于.text段,.text段的下标为1===》.rel.text的sh_info为1
字符串表:
ELF字符串偏移表:
偏移 | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 |
---|---|---|---|---|---|---|---|---|---|---|
+0 | \0 | h | e | l | l | o | w | o | r | l |
+10 | d | \0 | M | y | v | a | r | i | a | b |
+20 | l | e | \0 |
例:字符串与对应偏移:
偏移 | 字符串 |
---|---|
0 | 空字符串 |
1 | helloworld |
6 | world |
12 | Myvariable |
使用字符串偏移表,引用字符串只需给出一个数字下标即可,单个字符以\0结尾,无需考虑长度问题
ELF中的两个表:
字符串表:常见段名===》.strtab
段表字符串表:常见段名===》.shstrtab
通过分许ELF文件头,即可得到段表与段表字符串表的位置
链接的接口:
链接过程的本质:将多个不同的文件通过函数和变量引用的方式链接起来
例:
目标文件b中需要用到目标文件a中的函数xxx===》目标文件a定义了函数xxx===》目标文件b引用了目标文件a中的函数xxx
(此例同样适用于变量)
在连接中,将函数和变量===》符号,函数名或变量名===》符号名
在链接过程中可以将符号看作粘合剂====》符号被统一管理
每一个目标文件都有一个对应的符号表,表中记录了目标文件中所用到的所有符号,每个定义的符号都有一个对应的值===》符号值
符号分类:
定义在目标文件的全局符号,可以被其他目标引进
在本目标文件中引用的全局符号,却没有定义在本目标文件===》一般称为外部符号
段名,这类符号往往有编译器产生,它的值就是该段的起始地址
局部符号,这类文件只在编译但愿内部可见
行号信息,即目标文件指令与源代码中代码行对应关系,可选
ELF结构表:
ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”,表结构是一个Elf32_Sym结构的数组
结构定义:
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char sy_other;
Elf32_Half st_shndx;
} Elf32_Sym;
成员定义:
成员名 | 解释 |
---|---|
st_name | 符号名,这个成员包含了该符号名在字符串表中的下标 |
st_value | 符号相应的值,这个值和符号有关,可能是一个绝对值,也可能是一个地址等 |
st_size | 符号大小,对于包含数据的符号,这个值是该数据类型的大小,比如一个double类型的符号占用8个字节,如果该值为0,则表示该符号大小为0或位置 |
st_info | 符号类型和绑定信息 |
st_other | 该成员目前为0,没用 |
st_shndx | 符号所在段 |
程序的编译与链接:
C语言代码—编译—》汇编代码—汇编—》机器码
可执行文件:
广义:文件中的数据是可执行代码的文件
狭义:文件中的数据是机器码的文件
静态链接:
空间地址分配:
1、将输入的目标文件按照次序叠加
问题:输出文件将会有很多零散的段,比如规模稍大的应用程序可能会有数百个目标文件,如果每个目标文件都分别由.text,.data,.bss段,那最后的输出文件将会有成百上千个零散的段===》浪费空间,每个段都需要有一定的地址和空间对其要求
2、将相同性质的段合并在一起
.bss段在目标文件中并不占用空间,但是在装载时占用地址空间===》链接器在合并各个段的时候也会将.bss合并,并且分配虚拟空间
====》空间分配起始只关注与虚拟空间的分配
目前链接器空间分配的策略都采用相似合并的方法,使用这种方法的链接器采用的是两部链接的方法:
1、空间地址分配:
扫描所有的输入目标文件,并且获得各个段的长度,属性和位置,并将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表====》链接器将能够获得所有输入目标文件的段长度,并将其合并,计算输出文件中各个段合并后的长度与位置,并建立映射关系
2、符号解析与重定位
使用上一步收集到的所有信息===》读取输入文件中段的数据,重定位信息====》进行符号解析与重定位,调整代码中的地址
符号解析与重定位:
1、重定位:
当a.c的源程序中用到xxx符号,当源码a.c被编译成目标文件时,编译器不知道xxx的地址,因为xxx被定义在其他目标文件中===》两种情况
1、当使用mov 外部变量;esp指令调用时===》函数地址会被表示成00 00 00 00===》因为这个时候a.c还不知道外部变量地址是多少===》空出四位空间进行地址预留
2、当使用call 外部函数地址;外部函数地址会被表示成fc ff ff ff(小端序)===》他是一种-4的补码形式,-4相对的是call 指令的下一条指令的偏移,因为不知道外部函数的地址====》找一个已知的指令地址作为基址,往前空出4位空间进行地址预留
重定位表:
专门保存重定位相关的信息.在ELF文件中往往是一个或多个段,对于每个要被重定位的ELF段都有一个对应的重定位表,而一个重定位表往往就是ELF文件中的一个段===》重定位表也可以叫重定位段
每个要被重定位的地方===》重定位入口
重定位入口的偏移表示该入口在要被重定位的段中的位置
重定位表的结构是一个Elf32_Rel结构的数组,每一个数组元素对应一个重定位入口
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
成员定义:
成员 | 含义 |
---|---|
r_offset | 重定位入口的偏移。对于可重定位文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于段起始的偏移;对于可执行文件或共享对象文件来说,这个值是该重定位入口所要修正的位置的第一个字节的虚拟地址 |
r_info | 入口的类型和符号,这个成员的低8位表示重定位入口的类型,高24位表示重定位入口的符号在符号表中的下标 因为各种处理器的指令格式不一样,所以重定位所修正的指令地址格式也不一样,每种处理器都有自己一套重定位入口的类型,对于可执行文件和共享目标文件来说,他们的重定位入口是动态链接类型的 |
符号解析:
在重定位过程中,每个重定位的入口都是对一个符号的引用,当链接器需要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址===》这个链接器就会去找所有输入目标文件的符号表组成的全局符号表,找到相应的符号进行重定位===》在链接器扫描完所有输入目标文件后,所有未定义的符号都应该能在全局符号表中找到,否则链接器会报符号未定义错误
指令修正方式:
寻址方式:
近址寻址或远址寻址
绝对寻址或相对寻址
寻址长度为8位,16位,32位,64位。但是对于32位x86平台下的ELF文件的重定位入口修正的指令寻址==》绝对近址32位寻址,相对近址32位寻址===》这两种重定位方式指令修正每个修正的位置长度都为32位,即4字节,都是近址寻址,不考虑段间远址寻址,唯一区别就是相对寻址和绝对寻址
重定位入口的r_info成员低8位表示重定位入口类型:
宏定义 | 值 | 重定位修正方法 |
---|---|---|
R_386_32 | 1 | 绝对修正S+A |
R_386_PC32 | 2 | 相对寻址修正S+A-P |
A=保存在被修正位置的值
P=被修正的位置(相对于段开始的偏移量或虚拟地址),该值可通过r_offset得到
S=符号的实际地址,即由r_info的高24位指定的符号的实际地址
静态库链接:
程序如果需要进行输入输出交互===》使用操作系统提供的应用程序编程接口(API),但是API不能直接通过代码进行调用,需要通过静态库来实现调用
起始静态库可以看成一组目标文件的集合,在Linux中最常用的C语言静态库libc位于/usr/lib/libc.a,属于glibc的一部分
静态库形成:
glibc本身是C语言开发==》很多C语言源代码===》编译完成后有同样数量的目标文件===》使用“ar”压缩程序将这些目标文件压缩在一起===》对其进行编号和索引====》形成libc.a的静态库
静态库是怎么链接:
例:helloworld程序===》使用printf函数,printf函数不在原有的源代码中,它在静态链接库中===》链接器会自动寻找所需要的符号及他们在静态库中的目标文件===》将这些目标文件从libc.a中解压出来===》将他们链接在一起称为一个可执行文件
可执行文件的装载与进程:
可执行文件分类:
Windows:PE
执行程序:.exe
动态链接库:.dll
静态链接库:.lib
Linux:ELF
可执行程序:.out
动态链接库:.so
静态链接库:.a
在Linux中,ELF动态链接文件被称为动态共享对象==》简称共享对象==》一般是以.so为扩展名的一些文件
对应windows中,动态链接就是平时常见的以.dll为扩展名的文件
进程虚拟地址空间:
程序与进程的区别:
程序:一个静态的概念,是一个预先编译好的指令和数据集合的一个文件
进程:一个动态的概念,是程序运行时的一个的一个进程,很多时候把动态库叫做运行时
每个程序被运行起来以后,都将拥有自己独立的虚拟地址空间,虚拟地址空间的大小由CPU位数决定===》32位的硬件平台决定了虚拟地址为0到2^32,即0x00000000~0xFFFFFFFF===》4GB的虚拟空间大小====》程序不能完全掌握这4GB的空间,原因:进程只能使用操作系统分配给进程,如果访问未经允许的空间,在Linux就会出现“Segmentation”的错误,并且会强制结束进程
整个4GB被划分成两个部分,其中操作系统本身用掉一部分:0xC0000000~0xFFFFFFFF,共1GB
剩下的从0x10000000到0xBFFFFFFF共3GB空间在“原则上”是留给进程使用,进程其实不能完全使用这3GB,其中由一部分是预留给其他用途
对于windows操作系统来说,他的进程虚拟地址空间划分是操作系统占用2GB,剩下的2GB给进程
2GB对于一些程序来说太少了,windows有个启动参数可以将操作系统占用的虚拟地址空间减少到1GB,与Linux分布一样
装载:
程序运行时是有局限性原理的,可以将程序最常用的部分留在内存中,将一些不太常用的数据存在磁盘中===》动态装入的基本原理
覆盖装入(已被淘汰)和页映射是两种典型的动态装载方法,都利用了程序的局部性原理
页映射:
页映射会将内存和所有磁盘中的数据和指令按照“页”为单位分成若干页,装载和操作的单位===》页
例:32位机器有16KB的内存,每个页为4096字节:
页编号 | 地址 |
---|---|
F0 | 0x00000000~0x00000FF |
F1 | 0x00001000~0x00001FFF |
F2 | 0x00002000~0x00002FFF |
F3 | 0x00003000~0x00003FFF |
如果程序指令和数据总和为32KB==》会被分为8页,在动态装载的原理看===》假设程序的入口在P0===》装载管理器发现程序P0不在内存中===》将内存F0分配给P0===》发现需要用到P5,检查后发现P5不在内存中===》将F1分配给P5…………用到哪一块,先检查是否在内存中,不在则分配空间
如果程序只需要P0,P3,P5,P6,程序运行没有问题,此时程序需要访问P4===》需要让出一块内存空间===》
空间考虑:
1、可以选择F0,因为它是第一个被分配的内存页===》FIFO,先进先出算法
2、假设装载管理器发现F2很少被访问===》选择F2===》LUR,最少使用算法
进程的建立:
1、创建一个独立的虚拟地址空间
一个虚拟地址由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,在i386的Linux下,创建虚拟地址空间实际上只是分配一个页目录就可以了,不需要设置页映射关系,等到后面程序发生页错误的时候再进行设置
2、读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
程序发生页错误===》操作系统将从物理内存中分配一个物理页===》将该页从磁盘中读取到内存中===》设置缺页的虚拟页和物理页的映射关系
在操作系统捕获到缺页错误,需要知道程序在当前所需要的页在可执行文件中的位置====》虚拟空间与可执行文件之间的映射关系
Linux中将进程虚拟空间中的一个段叫做虚拟内存区域,操作系统创建进程相应的数据结构中设置一个.text段的VMA===》在虚拟空间中的地址为0x08048000~0x08049000,对应ELF文件中的偏移为0的.text,属性为只读
3、将CPU的指令寄存器设置成可执行文件的入口地址,启动运行
操作系统通过设置CPU的指令寄存器将控制权交给进程,从进程角度看===》操作系统执行了一条跳转指令,直接跳转到可执行文件的入口地址===》ELF文件中保存有入口的地址
可执行文件中不止有代码段,还有数据段,bss段等===》映射到进程虚拟空间得不止一个段
进程虚拟空间分布:
段得数量增多会产生空间浪费===》从操作系统装载可执行文件的角度看===》它不关心可执行文件各个段所包含的的实际内容,更多的是关心装载相关问题===》主要是段的权限(可读,可写,可执行)
ELF文件中段权限的组合:
1、以代码段为代表的权限为可读可执行段
2、以数据段和bss段为代表的权限为可读可写段
3、以只读数据段为代表的权限为只读的段
对于相同权限的段,把他们合并到一起当作一段进行映射
例:两个段.text和.init,分别是程序的可执行代码和初始化代码===》都具有可读可执行权限
如果两个段分别映射就需要占用3个页面,但是合并一起映射只需要两个页面
ELF把属性相似,又连在一起的段叫做一个“segment”,系统按照“segment”来映射可执行文件
一个“segment”包含一个或多个属性类似的“section”
如果将.text和.init段一起看作是一个segment===》装载的时候就可以看作是一个整体一起映射,映射以后进程空间中只有一个相对应的VMA,可以减少页面内部碎片,进而节省内存空间
segmnet和section是从不同角度来划分同一个ELF文件:
从section来看:ELF文件就是链接视图
从segment来看:ELF文件就是执行视图
当ELF装载时,段专门指segment,其他情况都是指section
ELF可执行文件有一个专门的数据结构===》程序头表:用来保存segment信息====//====因为ELF目标文件不需要被装载,所以它没有程序头表====//====ELF的可执行文件和共享库文件都有,和段表结构一样
程序头表是一个结构体数组:
typedef struct {
ELF32_Word p_type;
ELF32_Off p_offset;
ELF32_Addr p_vaddr;
ELF32_Addr p_paddr;
ELF32_Word p_filesz;
ELF32_Word p_memsz;
ELF32_Word p_flags;
ELF32_Word p_align;
} Elf32_Phdr
成员含义:
成员 | 含义 |
---|---|
p_type | “Segment”的类型,基本上只关注“LOAD”类型的”Segment”。“LOAD”类型的常量为1 |
p_offset | “Segment”在文件中的偏移 |
p_vaddr | “Segment”的第一个字节在进程虚拟地址空间的起始位置。整个程序头表中,所有“LOAD”类型的元素按照p_vaddr从小到大排列 |
p_paddr | “Segment”的物理装载地址 |
p_filesz | “Segment”在ELF文件中所占空间的长度,它的值可能是0,因为有可能这个”Segment”在ELF文件中不存在内容 |
p_memsz | “Segment”在进程虚拟地址空间中所占用的长度,它的值也可能是0 |
p_flags | “Segment”的权限属性,比如可读“R”、可写“W”、和可执行“X” |
p_align | “Segment”的对齐属性,实际对其字节等于2的p_align次方 |
成员注意点:对于LOAD类型的segment来说,p_memsz的值不可以小于p_filesz===》segment在内存中所分配的空间大小超过文件中实际的大小,多余的部分全部填充为0===》好处:构造ELF可执行文件时不需要再额外设立BSS的segment
堆和栈:
操作系统通过使用VMA来对进程的地址空间进行管理,例:堆和栈===》它们在进程的虚拟空间中的表现也是以VMA的形式存在,并且一个进程中的栈和堆分别都有一个对应的VMA
操作系统通过给进程空间划分出一个个的VMA来管理进程的虚拟空间,基本原则是将相同权限属性的,有相同映像文件的映射成一个VMA,一个进程基本上可以分为如下几个VMA区域:
1、代码VMA,权限只读,可执行,有映像文件
2、数据VMA,权限可读写,可执行,有映像文件
3、堆VMA,权限可读写,可执行,无映像文件,匿名,可向上扩展
4、栈VMA,权限可读写,不可执行,无;映像文件,匿名,可向下扩展
常见进程的虚拟空间:
段地址对齐:
可执行文件需要被操作系统装载运行,装载过程一般是通过虚拟内存的页映射完成,要映射一段物理内存和进程虚拟地址空间之间建立映射关系,这段内存空间的长度必须是页大小的整数倍
例:intel 80x86系列处理器默认页大小为4096字节,假设有一个ELF可执行文件,有三个段需要装载,分别为SEG0,SEG1,SEG2:
段 | 长度(字节) | 偏移(字节) | 权限 |
---|---|---|---|
SEG0 | 127 | 34 | 可读可执行 |
SEG1 | 9899 | 164 | 可读可写 |
SEG2 | 1988 | 只读 |
每个段的长度都不是页长度的整数倍,最简单的映射方法就是每个段分开映射,对于长度不足一个页的部分则占一页。通常ELF可执行文件的起始虚拟地址为0x08048000:
段 | 起始虚拟地址 | 大小 | 有效字节 | 偏移 | 权限 |
---|---|---|---|---|---|
SEG0 | 0x08048000 | 0x1000 | 127 | 34 | 可读可执行 |
SEG1 | 0x08049000 | 0x3000 | 9899 | 164 | 可读可写 |
SEG2 | 0x0804C000 | 0x1000 | 1988 | 只读 |
以上方法总结:
在文件段的内部会有很多内部碎片,浪费空间,三段加起来12014字节,却占用5页
优化:
让各个段接壤部分共享一个物理页面,然后将改物理页面分别映射两次
比如对于SEG0和SEG1的接壤部分的物理页,系统将他们映射两份到虚拟地址空间===》一份SEG0,一份SEG1,其他页都按照正常的页粒度进行映射
UNIX系统将ELF的文件头看作系统的一个段,将其映射到进程的地址空间,进程中某一区域就是整个ELF文件的映像,对于一些需要访问ELF文件头的操作(动态链接器读ELF文件头)可直接通过读写内存地址空间进行
===》整个文件从最开始到某个点结束,被逻辑上分成以4096字节为单位的若干块,每个块都被装载到物理内存中,对于位于两段中间的块===》会被映射两次:
段 | 起始虚拟地址 | 大小 | 偏移 | 权限 |
---|---|---|---|---|
SEG0 | 0x08048022 | 127 | 34 | 可读可执行 |
SEG1 | 0x080490A4 | 9899 | 164 | 可读可写 |
SEG2 | 0x0804C74F | 1988 | 可读可写 |
进程栈初始化:
在进程刚开始启动时,需要一些进程运行的环境,基本的是系统环境变量和进程的运行参数
操作系统在进程启动前将这些信息提前保存到进程的虚拟空间的栈中国,假设系统中有两个环境变量:
HOME=/home/user
PATH=/usr/bin
运行命令:
prog 123
假设栈底地址0xBF802000
进程初始化后的堆栈:
解析:
最前面的4个字节==》2==》表示命令行参数的数量
接下来的就是分布指向这两个参数字符串的指针===》prog 123===》0xBF801FD8 0xBF801FDE
0表示返回地址===》结束
再接下来是两个指向环境变量字符串的指针===》HOME=/home/user PATH=/usr/bin
0结束
进程再启动以后,程序的库部分会把栈中的初始化信息中的参数传递给main函数===》argc argv===》这两个参数分别对应这里命令行参数数量和命令行参数字符串指针数组
Linux内核装载ELF过程:
在用户层面,bash进程会调用fork()系统调用创建一个新的进程===》新的进程调用execve()系统调用执行指定的ELF文件,原先得bash进程继续返回等待刚才启动的新进程结束===》等待用户输入命令
exceve()系统调用被定义在unistd.h,原型:
int execve(const char *filename, char *const argv[], char *const envp[]);
参数分别是之星文件的文件名,执行参数和环境变量
glibc对execve()系统调用进行包装===》提供execl(),execlp(),execle(),execvp()等5个不同形式的exec系列API
只是在调用参数形式上会有区别,但最终都会调用execve()这个系统调用
进入execve()系统调用,Linux内核就开始进行真正的工作:
1、在内核中execve()系统调用相应的入口是sys_execve(),sys_execve()进行一些参数的检查复制后,调用do_execve()
2、do_execve()会首先查找被执行的文件,如果找到文件==》读取文件的前128字节判断文件格式
3、调用search_binary_handle()通过判断文件头部的魔数搜索和匹配适合可执行文件装载处理过程,例:
ELF可执行文件的装载处理过程===》load_elf_binary()
4、当load_elf_binary()执行完毕===》返回do_execve()再返回sys_execve()===》sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转至ELF程序的入口地址===》新的程序开始执行,ELF可执行文件装载完成
do_execve()读取文件前128字节:
Linux支持的可执行文件不止ELF,还有a.out,java程序和以”#!“开始的脚本程序
do_execve()读取文件前128个字节的目的时判断文件的格式
每种可执行文件的前4个字节===》魔数
通过对魔数的判断可以确定文件的格式和类型===》ELF的可执行文件格式的头4个字节为0x7f,e,l,f
java的可执行文件格式的头4个字节为c,a,f,e
如果被执行的是shell脚本或perl,python等解释型语言的脚本===》第一行为”#!/bin/sh“或”#!/usr/bin/perl“或”#!/usr/bin/python“===》前两个字节’#!‘就构成魔数,系统一旦判断这两个字符,就会对后面的字符串进行解析,确定具体解释程序的路径
load_elf_binary()函数指令步骤:
1、检查ELF可执行文件格式的有效性===》魔数,程序头表的数量
2、寻找动态链接的".interp"段,设置动态链接器路径
3、根据ELF可执行文件的程序头表描述,对ELF文件进行映射,比如代码,数据,只读数据
4、初始化ELF进程环境===》进程启动时EDX寄存器的地址应该是DT_FINI地址
5、将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式===》
静态链接==》ELF文件头中的e_entry所指的地址
动态链接==》程序入口就是动态链接器
动态链接:
静态链接例:
静态链接===》集装打包
两个程序:p1和p2
包含模块:p1.o,p2.o
公用libc.o
静态链接情况下===》p1和p2都用到libc.o===》同时在链接输出的可执行文件有两个副本,同时运行p1和p2时libc.o在磁盘中的内存中都有两份副本===》空间浪费
动态链接:
动态链接===》模块化使用===》需要用到哪部分就载入哪部分,多个模块同时需要用到某个部分,就把这个部分作为一个共享===》不对那些组成程序的目标文件进行链接,等到程序要运行时再链接
将链接过程推迟到运行时再进行===》动态链接
两个程序:p1和p2
包含模块:p1.o,p2.o
公用libc.o
1、当运行p1时,系统首先加载p1.o
2、当系统发现p1.o中还用到libc.o==》p1.o依赖于libc.o===》系统加载libc.o
3、如果p1.o或libc.o还依赖于其他目标文件,系统会按照这种方法将他们全部加载至内存===》所有需要的目标文件加载完毕之后,如果满足依赖,所有依赖的目标文件都存于磁盘,系统开始进行链接工作
4、完成上述===》系统开始把控制权交给p1.o的程序入口
5、如果需要运行p2===》系统只需要加载p2.o即可,因为内存中已经存在一份libc.o的副本,系统只需要将p2.0和libc.o链接即可
动态链接总结:
不会造成空间浪费,多个模块运行时公共使用的部分不必重复加载
减少物理页面的换入换出,增加CPU缓存的命中率
使程序升级更容易,理论上在升级程序库或者程序共享的某个模块时,可以直接覆盖旧的目标文件
程序开发时可以分开实现,实现模块独立
程序在运行时可以动态选择加载各种程序模块==》用来做插件
加强程序的兼容性,相当于在程序和操作系统之间增加一个中间层,消除程序对平台之间的依赖的差异性
例:动态链接重定位机制:
在p1.c中使用了定义于lib.c中的xxx函数
当程序模块p1.c被编译成p1.o时===》编译器不知道xxx函数的地址
当链接器将p1.o链接成可执行文件时===》链接器必须确定p1.o中引用的xxx函数的性质===》
如果xxx函数是一个定义与其他静态目标模块中的函数,那么链接器将会按照静态链接规则,将p1.o中的xxx函数地址引用重定位
如果xxx函数是一个定义在某个动态共享对象中的函数,那么链接器就会将这个符号引用标记为一个动态链接符号,不对他进行重定位,把这个过程留到装载时进行
xxx的引用是一个静态符号还是一个动态符号===》lib.so中保存了完整的符号信息===》运行时进行动态链接还需要使用到符号信息===》把lib.so作为链接的输入文件之一,链接器在解析符号时就可以知道xxx是一个定义在lib.so的动态符号
地址无关代码==》
1、共享模块全局变量:
进程: 进程A和进程B都使用lib.so===》当进程A改变全局变量G的值===》lib.so被两个进程加载,它的数据段在每个进程中都有独立的副本===》共享对象中的全局变量实际上和定义在程序内部的全局变量没什么区别,任何一个进程访问的只是自己的副本,不影响其他进程
线程:一个进程中的线程A和线程B===》访问同一个进程地址空间===》同一个lib.so副本===》对于G的修改,双方都有影响
2、数据段地址无关性:
static int a;
static int* p = &a;
===》指针p的地址是一个绝对地址,指向变量a,a的地址会随着共享对象的装载地址改变而改变===》对于数据段,它在每一个进程都有一份独立的副本,不担心被进程改变===》选择装载时重定位解决数据段中绝对地址引用问题
解决:
对于共享对象===》如果数据段中有绝对地址引用===》编译器和链接器就会产生一个重定位表,包含了R_386_RELATIVE类型的重定位入口
动态链接情况下,可执行文件的装载与静态链接情况基本一样==》首先操作系统会读取可执行文件的头部,检查文件的合法性===》然后从头部的Program Header中读取每个Segment的虚拟地址,文件地址和属性, 并将它们映射到进程虚拟空间的相应位置===》此时操作系统还不能在装载完可执行文件,因为可执行文件依赖于很多共享对象===》可执行文件里对于外部符号的引用还处于无效地址状态===》即还没有跟相应的共享对象中的实际位置连接起来===》在映射完可执行文件之后,操作系统会先启动一个动态链接器
在Linux下,动态链接器ld.so实际上是一个共享对象,操作系统同样通过映射的方式将它加载到进程的地址空间中===》操作系统在加载完动态链接器后,就会将控制权交给动态交给动态链接器的入口地址,之后动态链接器执行一系列自身的初始化操作,然后根据当前环境参数,开始对可执行文件进行动态链接工作===》当所有链接工作完成之后,动态链接器会将控制权交给可执行文件入口地址
动态链接相关结构:
.interp:
系统中链接器的位置由ELF可执行文件决定
.interp中只存放一个字符串===》该字符串为可执行文件所需要的动态链接路径
在Linux下,可执行文件所需要的动态链接器的路径几乎是/lib/ld-linux.so.2
在Linux系统中,/lib/ld-linux.so.2通常是一个软链接,操作系统在可执行文件进行加载时,它回去寻找装载该可执行文件所需要相应的东涛链接器===》.interp段执行的路径的共享对象
.dynamic:
保存了动态链接器所需要的基本信息===》
依赖于哪些共享对象,动态链接符号表的位置,动态链接重定位表的位置,共享对象初始化代码地址等
.dynamic段结构数组:
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
Elf_Dyn结构由一个类型值加上一个附加的数值或指针,对于不同的类型,后面附加的数值或者指针有不同的含义:
d_tad类型 | d_un的含义 |
---|---|
DT_SYMTAB | 动态链接符号表地址,d_ptr表示“.dynsym”的地址 |
DT_STRTAB | 动态链接字符串表地址,d_ptr表示“.dynstr”的地址 |
DT_STRSZ | 动态链接字符串表大小,d_val表示大小 |
DT_ | 动态链接哈希表地址,d_ptr表示“.hash”的地址 |
DT_SONAME | 动态链接 |
DT_RPATH | 动态链接共享对象搜索路径 |
DT_INIT | 初始化代码地址 |
DT_FINIT | 动态链接 |
DT_NEED | 依赖的共享对象文件,d_ptr表示所依赖的共享对象文件名 |
DT_REL DT_RELA |
动态链接重定位表地址 |
DT_RELENT DT_RELAENT |
动态重读位表入口数量 |
===》类似ELF文件头===》被称为动态连接下ELF的文件头
动态符号表:.dynsym
只保存与动态链接相关的符号,不保存模块内部符号,比如模块私有变量
动态符号表辅助表:
动态符号字符串表==》保存符号名的字符串表
符号哈希表(.hash)===》动态链接下在程序加快查找符号
动态链接重定位表:
动态链接下,一旦有可执行文件或共享对象依赖于其他共享对象==》有导入符号,它的代码或数据中就会有对于导入符号的引用==》编译时符号地址未知(导入符号的地址在运行时才确定)===》需要在运行时将这些导入符号的引用修正===》重定位
重定位相关结构(动态链接):
.rel.dyn和.rel.plt:
相当于静态链接的.rel.text和.rel.data
.rel.dyn===》对数据引用的修正,所修正的位置位于.got以及数据段
.rel.plt===》对函数引用的修正,所修正的位置位于.got.plt
动态链接时进程堆栈初始化信息
操作系统将控制权交给动态链接器==》开始做连接工作===》操作系统会将可执行文件有几个段(segment),每个段的属性,程序的入口地址等信息交给动态链接器,保存在进程的堆栈里===》在进程初始化时,栈里保存了关于进程执行环境和命令参数等,还保存了动态链接器所需要的一些辅助信息数组
辅助信息:===》辅助信息数组位于环境指针之后
typedef struct{
uint32_t a_type;
union{
uint32_t a_val;
} a_un;
} Elf32_auxv_t;
结构定义:
a_type定义 | a_type值 | a_val的含义 |
---|---|---|
AT_NULL | 0 | 表示辅助信息数组结束 |
AT_EXEFD | 2 | 表示可执行文件的文件句柄。动态链接器需要知道一些关于可执行文件的信息,当进程开始执行可执行文件时,操作系统会先将文件打开,这时候就会产生文件句柄。那么操作系统可以将文件句柄传递给动态链接器,动态链接器可以通过操作系统的文件读写来访问可执行文件 |
AT_PHDR | 3 | 可执行文件中程序头表(Program Header)在进程中的地址。动态链接器可以通过操作系统的文件读写功能来访问可执行文件。但是很多操作系统会把可执行文件映射到进程的虚拟空间里,从而动态链接器不需要通过读写文件,而是可以直接访问内存中的文件映像。当操作系统选择映像的方式时,必须提供后面的AT_PHENT、AT_PHNUM和AT_ENTRY |
AT_PHENT | 4 | 可执行文件头中程序头表中每个入口(Entry)的大小 |
AT_PHNUM | 5 | 可执行文件头中程序头表中入口(Entry)的数量 |
AT_BASE | 7 | 表示动态链接器本身的装载地址 |
AT_ENTRY | 9 | 可执行文件入口地址,即启动地址 |
动态链接的步骤和实现:
1、启动动态链接器
2、装载所有需要的共享对象
3、重定位和初始化
动态链接器自举:
1、动态链接器:
对于普通文件,它的重定位工作是由动态链接器完成,但是动态链接器自己本身也是共享对象===》
实现过程:
1、动态链接器本身不可以依赖于其他任何共享对象===》可以进行人为控制,在编写动态链接器时保证不使用任何系统库,运行库===》动态链接器是静态链接
2、动态链接器本身所需要的全局变量和静态变量的重定位工作必须由它本身完成===》动态链接器必须在启动时有一段代码可以完成重定位工作,同时又不能全部用到全局变量和局部变量===》这种具有一定限制条件的启动代码称为===》自举
动态链接器入口地址就是自举代码的入口,当操作系统将进程控制权交给动态链接器时,动态链接器的自举代码开始执行==》
自举代码运行过程:
1、找到自己的GOT
2、GOT的第一个入口保存的就是.dynamic段的偏移地址===》找到动态链接器本身的.dynamic段
3、通过.dynamic中的信息,自举代码可以获得动态链接器本身的重定位表和符号表
4、得到动态链接器本身的重定位入口,将他们全部重定位===》从这一步开始,动态链接器代码中才可以使用自己的全局变量和静态变量
装载共享对象:
装载共享对象过程:
1、完成基本自举后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表中,称之为全局符号表
2、链接器开始寻找可执行文件所依赖的共享对象,在.dynamic段中,有一类入口是DT_NEEDED,它所指的是该可执行文件(共享对象)所依赖的共享对象===》链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中
3、链接器开始从集合中寻找一个所需要的共享对象的名字,找到相对应的文件后打开文件,读取相应的ELF文件头和.dynamic段,然后将它相应的代码段和数据段映射到进程空间,如果这个ELF共享文件还依赖于其他共享对象,那么将所依赖的共享对象的名字放到集合中
4、如此循环直到所有的共享对象都被装载进来为止
5、当一个新的共享对象被装载进来的时候,他的符号表会被合并到全局符号表中,当所有的共享对象都被装载进来的时候,全局符号表里将包含进程中所有的动态链接所需要的符号
符号的优先级===》
一个共享对象里面的全局符号被另一个共享对象的同名全局符号覆盖的现象被称为===》共享对象全局符号介入
规则===》
当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略
如果两个符号重名又执行不同功能,那么程序运行时可能会将所有该符号名的引用解析到第一个被加入全局符号表的使用该符号的符号,从而导致错误
重定位和初始化:
以上步骤完成后===》链接器开始重新遍历可执行文件和每个共享对象的重定位表,将他们的GOT/PLT中每个需要重定位的位置进行修正===》重定位完成===》某个共享对象有.init段===》动态链接器会执行.init段中的代码,用来实现共享对象特有的初始化===》共享对象中有.finit段===》当进程退出时会执行.finit段的代码,用来实现类似C++全局对象析构之类的操作===》完成重定位和初始化之后,动态链接器将进程的控制权交给程序入口并开始执行
(如果进程的可执行文件有.init段==》动态链接器不会执行==》可执行文件中的.init和.finit段是由程序初始化部分代码执行)
延迟绑定:
动态链接比静态链接灵活,但是失去了一部分性能==》
动态链接下对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址,对于模块间的调用也要先定位GOT,然后进行间接跳转,速度变慢
动态链接的链接工作是在运行时完成的,动态链接器会寻找并装载所需要的共享对象,然后进行符号查找地址定位等工作===》减慢程序的启动速度
解决方法:延迟绑定===》
当函数第一次被使用时才进行绑定(符号查找,重定位等),如果没有用到则不进行绑定===》程序开始时,模块间的函数调用没有进行绑定,需要用到的时候才由动态链接器负责绑定
===》
使用PLT方法===》当在调用某个外部模块的函数时,PLT为了实现延迟绑定,在这个过程中有增加一层间接跳转,调用函数并不直接通过GOT跳转,而是通过PLT项的结构进行跳转,每个外部函数在PLT中都有一个相应的项,例:bar()函数在plt中的项的地址为bar@plt
实现过程:
未延迟绑定过程
bar@plt:
jmp *(bar@GOT) ===》通过GOT间接跳转指令,bar@got表示GOT中保存bar()这个函数相应的项
==》如果链接器在初始化阶段已经初始化该项,并且将bar()的地址填入该项,那么这个跳转指令跳转指令的结果就是跳转至bar(),实现函数调用
push n
push moduleID
jump __dl_runtime_resolve
但是为了实现延迟绑定,链接器在初始化时并没有将bar()的地址填入到该项,而是将push n的地址填入bar@GOT中
===》
延迟绑定过程
bar@plt:
jmp *(bar@GOT)===》跳转至第二条指令
push n====》将一个数字n压入栈中,n是bar这个符号引用在重定位表“.rel.plt”中的下标
push moduleID===》将模块的ID压入栈中
jump __dl_runtime_resolve===》跳转至_dl_runtime_resolve
相关函数:
lookup(module,function)
先将所需要决议符号的下标压入栈,再将模块ID压入栈,然后调用动态链接器_dl_runtime_resolve()函数来完成符号解析和重定位
在Glibc中,lookup()函数的真名为_dl_runtime_resolve()
===》bar()函数被解析完毕==》再次调用bar@plt===》第一条指令直接跳转到真正的bar()函数,bar()函数返回的时候会根据栈里保存的EIP直接返回调用者,不会执行bar@plt中第二条指令开始的指令===》这段代码只在符号未被解析时执行一次
ELF将GOT拆分成两个表===》”.got”和”.got.plt”===》
.got用来存放全局变量引用地址
.got.plt用来保存函数引用地址
所有外部函数的引用全部分离出来放在.got.plt中
.got.plt特殊前三项:
第一项是.dynamic段的地址==》这个段描述了本模块动态链接相关的信息
第二项保存的是本模块的ID
第三项保存的是_dl_runtime_resolve()的地址
第二项和第三项由动态链接器在装载共享模块的时候将他们初始化.got.plt的其余项分别对应外部函数的引用
PLT结构为了减少代码重复===》ELF将最后两条指令放在PLT中的第一项,并规定每一项的长度是16个字节,刚好存放3条指令
实际PLT基本结构:
结构代码:
PLT0:
push *(GOT + 4)
jump *(GOT + 8)
...
bar@plt:
jmp *(bar@GOT)
push n
jump PLT0
程序的内存布局:
栈:栈用于维护函数调用的上下文,离开了栈函数调用就没法实现,通常在用户空间的最高地址处分配,通常有兆字节的大小
堆:堆是用来容纳应用程序动态分配的内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里,某些时候堆也可能没有固定统一的存储区域,堆一般比栈大,有几十至数百兆字节的容量
可执行文件影响:是由装载器在装载时将可执行文件的内存读取或映射到这里
保留区:保留区并不是一个单个的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称===》在多数操作系统中,极小的地址通常是不允许访问==》NULL
程序运行:
1、操作系统在创建进程开始,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数
2、入口函数对运行库和运行环境进行初始化===》堆,I/O,线程,全局变量构造等
3、入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分
4、main函数执行完毕后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程