再次认识虚拟地址空间 可执行程序的加载 ─── linux第21课
目录
1.可执行程序的格式
2.链接过程
ELF加载
加载可执行程序的步骤
虚拟地址空间
动态库的加载(类似可执行程序的加载)
多进程间看待动态库
与地址无关 =GOT+函数在库中的偏移量
1.可执行程序的格式
2.链接过程
- 可见linux下的可执行程序的格式是ELF格式 ,例如代码区放在section1 数据区放在是section2
- 其实动态库/静态库 , .o文件 ,可执行程序这三类都是ELF结构
- 链接就是将一个一个相同属性的section合并 ,所以命名时不能有重名
生成可执行程序的本质就是将.c文件变成.o文件 ,再将全部的.o文件链接成可执行程序
补充:文件是一个大型的一维的数组 ,想要找到某个区域 ,只需要找到偏移量和区域大小即可
ELF加载
我们前面学习了进程地址空间中存储的是虚拟地址
⼀个ELF会有多种不同的Section,在加载到内存的时候,也会进⾏Section合并,形成segment
问题:
- ⼀个ELF程序,在没有被加载到内存的时候,有没有地址呢?
- 进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?
答案:
⼀个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采⽤"平坦 模式"进行工作。所以也要求ELF对⾃⼰的代码和数据进行统⼀编址,下面是 objdump -S 反汇编之后的代码
最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量), 但是我们 认为起始地址是0. 也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进行统⼀编址了.
进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?
从ELF各个 segment来,每个segment有自己的起始地址和自己的长度,⽤来初始化内核结构中的[start, end] 等范围数据,另外在用详细地址,填充页表
结论: 虚拟地址是编译器 操作系统 磁盘三方的共同产物
磁盘ELF中的逻辑地址 == 内存中的虚拟地址(在数值上相同 ,可以将两者看为一个东西)
加载可执行程序的步骤
可执行程序生成时就会产生虚拟地址(逻辑地址)存储在磁盘上 , 将可执行程序加载到物理内存中,每条指令占据了内存空间 ,每条指令产生了自己的物理内存地址 ,现在每条指令都有虚拟地址和物理内存地址 , 页表将每个指令的物理内存地址和虚拟地址进行映射
cpu运行可执行时 ,拿到的是可执行程序虚拟地址的起始地址 ,通过页表拿到对应物理内存地址, 进而拿到指令内容 , 加载到cpu中进行执行 , 执行完这一指令 ,根据此指令的长度+此指令虚拟地址的起始地址 = 下一条指令的虚拟地址 , 拿到下一个指令的虚拟地址 , 通过页表拿到物理内存地址 , 将下一条指令的内容加载到cpu ,以此类推 ,执行可执行程序
虚拟地址空间
进程的虚拟地址空间其实是一段一段的 ,每个段都有特定的用途和属性,例如代码段、数据段、堆、栈等 每一段都是vm_area_struct的对象
动态库加载 ,从磁盘加载到内存后 ,建立vm_area_struct 链入到mm_struct中.
动态库的加载(类似可执行程序的加载)
1.系统层面: 会创建struct libso的数据结构 ,系统只需要管理结构体就可以管理动态库,多一个进程链接这个动态库,这个动态库中的引用计数就++.
2.进程层面: 将动态库的虚拟地址和物理内存地址映射在页表 ,创建vm_area_struct 用动态库的虚拟地址的开始和结束地址初始化vm_area_struct ,将vm_area_struct链入到整个进程虚拟地址空间的共享区.
3.还有一步操作可以看成(因为正文代码只能读,通过ELF中GOT实现下面的操作) 在进程正文代码处进行修改 ,将使用到的动态库函数换成动态库函数的虚拟地址 (动态库的虚拟起始地址+库函数在此库中的偏移量) ,用了多少动态库函数就改多少.
多进程间看待动态库
注意:
一个库映射在不同进程中的虚拟地址的起始地址可能不同 ,因为动态库也是vm_area_struct链到mm_struct中的
找函数实现时,用对应进程的地址空间中动态库的虚拟地址的起始地址+函数偏移量
与地址无关 =GOT+函数在库中的偏移量
让代码通过查表的方式找到库中的方法,执行完返回