1.可执行程序格式 ELF
[wws@hcss-ecs-178e myshell]$ ll
total 56
-rw-rw-r-- 1 wws wws 92 Oct 17 19:14 file
-rw-rw-r-- 1 wws wws 82 Oct 12 16:51 makefile
-rw-r--r-- 1 wws wws 90 Oct 17 19:13 myfile
-rwxrwxr-x 1 wws wws 20128 Oct 16 21:02 myshell
-rw-rw-r-- 1 wws wws 7627 Oct 16 21:16 myshell.cc
[wws@hcss-ecs-178e myshell]$ file myshell
myshell: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=285576a85c296a5f746e5570999a6c447128bd52, not stripped
[wws@hcss-ecs-178e myshell]$ size myshell
text data bss dec hex filename
9159 900 3192 13251 33c3 myshell
file 查看文件的基本情况 size查看可执行程序的结构情况,text数据段大小 data初始化数据大小 bss未初始化数据大小 dec10进制总大小 hex16进制总大小 filename名称
前两个部分是系统在加载时用到的,中间黄色部分Section 每一个小框就是一个节 不同的节存储不同的内容 比如
text节:
存储程序的机器代码,也就是可执行指令。
data节:
存储初始化的全局和静态变量。这些变量在程序启动时会被加载到内存中。
bss节:
存储未初始化的全局和静态变量。
...
最后一个部分Section Header Table 它的作用是保存每个节的偏移量和大小...,通过节的偏移量和大小,可以计算出每个节在ELF文件中的范围。
2.重谈地址空间(可执行程序 加载)
我们知道每一个进程都有对应的mm_struct虚拟地址空间,它通过页表与物理内存建立映射关系。每次访问虚拟地址时,系统会通过页表查找相应的物理地址。
现在有几个问题
1.可执行性程序在加载到内存前有地址吗?
在加载到内存之前,可执行程序确实具有地址,但这些地址主要是虚拟地址,而非物理地址。我们在编译时就可以看到地址,但这些地址其实是虚拟地址。
从这张图就可以看出在加载到物理内存前,可执行程序就有程序自己生成的逻辑地址了,之前不说说虚拟地址吗?逻辑地址是什么?逻辑地址=起始地址+偏移量(平坦模式下起始地址==0)知道一条指令的地址+它的长度=下一个指令的地址 逻辑地址在数字层面上就和虚拟地址一样了 只不过在磁盘ELF中一般说逻辑地址 加载到内存时说虚拟地址
2.mm_struct由谁初始化的?
我们知道可执行程序加载到内存前就有虚拟地址,每个节的起始的虚拟地址也知道(节偏移+节大小)
程序内部进行函数跳转也是用的虚拟地址。
每个节的虚拟地址的范围就是mm_struct中每个区域的起始范围。
3.现在可执行程序加载到内存,虚拟地址空间也通过页表跟内存建立映射关系,接下来CPU怎么开始调度呢?
将 PC 寄存器设置为入口点地址,要从ELF的ELF Header中读取
但这是一个虚拟地址,CPU怎么找对应的物理地址呢?
PC:程序计数器是一个指向下一条将要执行的指令的内存地址的寄存器。
EIP:用于指向当前正在执行的指令。保存了下一条要执行的指令的地址,并且在程序执行过程中会自动更新。
MMU硬件:是负责虚拟地址到物理地址转换的硬件模块。
CR3:存储页表的物理地址
现在pc中有第一条指令的虚拟地址,再根据MUU+CR3找到对应的物理地址,再把该物理地址里面的内容(指令+虚拟地址)读入到EIP,最后根据读入的虚拟地址+指令长度=下一个指令的虚拟地址更新PC。形成闭环
所以虚拟地址是CPU 操作系统 编译器 共同协助下的产物。
为什么要有虚拟地址空间?
有了虚拟地址空间,编译器在编址的时候就不用考虑物理内存的情况,直接在平坦模式下从0000开始不断生成地址,完成编译器与操作系统的解耦。
3.重谈区域划分
现在我们知道了mm_struct里面代码段 数据段是根据可执行程序ELF格式中的Section(text节 data节...)的虚拟地址的范围划分的。但里面栈区 堆区 共享区的范围怎么划分的?又怎么对它们进行管理?
其实在mm_struct中有struct vm_area_struct结构体,vm_area_struct是 Linux 内核中用于管理虚拟内存区域的结构体。里面存有该区域的起始位置
struct vm_area_struct { struct mm_struct *vm_mm; // 指向该虚拟内存区域所属的内存管理结构 unsigned long vm_start; // 虚拟内存区域的起始地址 unsigned long vm_end; // 虚拟内存区域的结束地址 struct vm_area_struct *vm_next; // 指向下一个虚拟内存区域的指针 pgprot_t vm_page_prot; // 页的保护属性 unsigned long vm_flags; // 该虚拟内存区域的标志 struct file *vm_file; // 如果是映射文件,该指针指向文件结构 unsigned long vm_pgoff; // 在映射文件时的偏移 // 可能还有其他字段 };
每一个区域都有自己的vm_area_struct,并通过链表的形式相互连接。
4.加载动态库
我们知道动态库也是ELF结构,对里面函数方式进行编址,也是按照基址0+偏移量 进行的。(库名:偏移量 库名会变成地址)
库加载到内存也需要被管理,struct libso 里面记录了加载到内存中的库的信息,引用计数,指针
#include <stddef.h> // for size_t // 定义共享库结构体 struct libso { char *name; // 库的名称 char *path; // 库的路径 void *handle; // 库的句柄 size_t size; // 库的大小 int ref_count; // 引用计数 struct libso *next; // 指向下一个 libso 结构体的指针 };
库映射到mm_struct。知道库在物理内存占据的空间大小,以及物理地址起始位置。通过页表建立映射关系,映射到mm_struct的位置由系统找等大小的虚拟地址空间。并在vm_area_struct的链表中链接自己的vm_area_struct(虚拟地址的起始位置)
所以正文代码中怎么找到库函数呢?
库名:偏移量 库名会变成地址 当系统找到等大的虚拟地址空间分配给库,库就有了虚拟起始地址,此时把库名改成 库的虚拟起始地址 再加上偏移量,就能找到库函数的虚拟地址了。
但正文代码段是不能修改的,也就不能把库名该为 库的虚拟起始地址。
其实在ELF中的节有一个是GOT全局偏移表,它里面存放的就是库函数的虚拟地址(库名:偏移量)。
代码区找库函数时是这样写的 call GOT虚拟地址:目标库函数的下标
GOT的虚拟地址在加载到内存前就已经编址好了。通过call GOT虚拟地址:目标库函数的下标 就可以找到目标库函数的虚拟地址(需要等系统分配好虚拟地址 才能找到库的虚拟起始地址 进而通过偏移量找到库函数虚拟地址)