编译、链接到载入、运行的大致过程----3.载入

December 09, 2023
测试
测试
测试
测试
4 分钟阅读

在Linux下,elf文件有三类,分别是: relocatable , shared object, executable. 见下面的例子:

[root@www ~]# file main.obj  /usr/bin/cat /lib/librt-2.17.so 
main.obj:           ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
/usr/bin/cat:       ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=8ac8b57ae50762a4a0480486839107e87b3c284d, stripped
/lib/librt-2.17.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=e9050e3a9543278c0fe04e541644287e67356ff1, for GNU/Linux 2.6.32, not stripped
[root@www ~]# 

其中的relocatable的文件,就是编译后生成的“目标文件”; 而 "shared object", 就是Linux下的动态链接库文件; executable 文件是 可以直接运行的 程序文件; 在这里要注意的是: 部分“shared object” 也是可以直接运行的,并不是所有的“动态链接库文件” 都不可以运行;

从上面的结果中还可以看到: executable的elf文件最后有一个“stripped” 的说明,而另外两种elf文件所对应的是"not stripped", 这表示什么意思呢?

编译的时候,每个文件中涉及到的对其他文件中函数的引用,都会用符号来进行表示,这是因为 当前文件文件中无法确定被引用函数的地址,所以就采用了符号来表示;而在链接的时候,会进行 地址重载,这时候,被引用的符号就被解析成了地址,并生成了最终的程序文件;而"stripped" 就表示 符号 已经被解析为 程序虚拟地址,而"not stripped" 就表示 符号没有被解析为 程序虚拟地址;所以 编译后的文件,其属于"not stripped"的类型,而 链接后的文件又分为两类: "shared object" 类型的文件,一般对外提供 程序接口的,这些对外提供的接口是以符号的方式提供的,而不是以 程序虚拟地址来提供,毕竟我们写代码的时候调用的都是函数名称,而不是用一串地址来调用的;所以 "shared object"的程序是 "not stripped"的,但是也有"shared object"是stripped, 而“executable ” 的elf文件,因为并不对外提供函数的接口,所以其符号 在编译的过程中已经被替换为程序虚拟地址;因此“executable” 的elf文件是"stripped"的;

程序的载入过程:

当我们运行一个程序的时候,操作系统打开程序文件做完相应的处理后,会把控制权交给 程序解释器(比如:/lib/ld-linux.so.2 就是程序解释器的一种 ), 程序解释器根据程序的头部信息,生成程序的 内存虚拟地址的入口,并从程序需要的动态链接库文件中查找对应的符号地址,这些找到的符号地址,被加载器进行了重定向,然后加入到当前程序的虚拟内存地址空间中合适的位置,从而完成 当前程序中的符号解析,至此完成程序虚拟地址到 内存虚拟地址的转换工作;然后程序解释器创建程序的进程映像,创建进程映像之后,会把控制权交给程序的入口地址,从而开始程序的执行;

在这个过程中:

1. 无论是程序本身,还是其依赖的动态链接库,被载入的都是 type=LOAD的segment;其他segment不会在程序的正常加载过程中被载入内存; 2. 载入内存后,在运行时候,访问的地址是: 内存虚拟地址。这个内存虚拟地址 并不是 “程序虚拟地址”,也不是“内存物理地址”;但是 这三者之间是有关系的: A. “程序虚拟地址“是源代码被编译链接之后生成的;这其中要关注的是type=LOAD的segment 对应的地址范围,因为这些segment会被加载到内存;通过 readelf -l 命令来查看segment的地址范围,也可以通过 readelf -S 来获取对应section的地址,从而计算出segment的地址; B. “内存虚拟地址” 是程序被加载后,其进程映像对应的 虚拟地址,它一般是由加载器分配的;如果程序运行过程中发生了或者启动时候发生了常见的segment报错,那么这个segment 地址一般都是 "内存虚拟地址",查看特定文件的内存虚拟地址可以通过命令: cat /proc/{PID_VALUE}/maps | grep FILE_PATH 来获得; C. type=LOAD的segment的地址是以程序虚拟地址来表示的 ,对于executable文件来说,它和内存虚拟地址是一致的,因为编译后的程序中的部分代码可能是地址相关的,所以为了保证 程序能够可靠运行,一般对于"executable"的elf文件来说,其 内存虚拟地址 和程序虚拟地址是相同的, 而 "shared object"的程序虚拟地址(TYPE=LOAD的segment) 总是从0开始,这个地址 和 内存虚拟地址是不同的,即便是 同一个“shared object” 文件,在不同的进程中 映射对应的 内存虚拟地址也是不同的,因为这个地址是 加载器 分配的. 这一点在ldd 命令的结果中体现的非常明显,我们知道ldd的输出结果表示对相应的共享库的依赖,其输出结果的最后一节是一个地址,而这个地址就是:内存虚拟地址;每次用ldd去查看其依赖的时候,这个地址都是发生变化的,因为是 加载器分配的; D. ”内存虚拟地址“和“程序虚拟地址” 都是虚拟地址,并且都采用分页的机制(默认page大小为4KB,也就是0x1000对齐),所以 对于 type=LOAD的segment 尽管在 “程序虚拟地址” 和“内存虚拟地址” 可能并不相同,但是对应的segment 的大小一定是相同的; E. “程序虚拟地址” 通过分析文件获得,依赖于程序文件;而“内存虚拟地址” 是 程序加载器 分配的,所以每次运行程序可能都会发生变化,实际上没有不发生变化的,程序的运行访问的都是"内存虚拟地址" ,所以"内存虚拟地址" 到 “内存物理地址”之间存在mapping, 这个mapping的工作是操作系统来完成的;

继续阅读

更多来自我们博客的帖子

如何安装 BuddyPress
由 测试 December 17, 2023
经过差不多一年的开发,BuddyPress 这个基于 WordPress Mu 的 SNS 插件正式版终于发布了。BuddyPress...
阅读更多
Filter如何工作
由 测试 December 17, 2023
在 web.xml...
阅读更多
如何理解CGAffineTransform
由 测试 December 17, 2023
CGAffineTransform A structure for holding an affine transformation matrix. ...
阅读更多