个人主页:Lei宝啊
愿所有美好如期而遇
系统角度理解
我们先来谈谈进程地址空间,当我们将一个可执行程序跑起来的时候,操作系统首先会在内存中创建出task_struct,也就是进程控制块,然后将可执行程序的代码和数据加载进内存,和进程地址空间的地址通过页表建立映射关系。
我们提到了进程虚拟地址空间,但是没有提到过他的数据是怎么来的,这个我们后面会说,现在这里想说的是,可执行程序链接静态库后,运行时不再需要静态库,因为静态库的代码已经拷贝了一份到可执行程序中,但是动态库不是这样,也就是说,当可执行程序执行到了某行代码,这行代码调用的是动态库的内容,那么我们其实是需要动态库的。
所以也就是说,即使动态库文件不是和程序一起加载进内存,那么在程序需要时,仍然需要加载进内存,并且动态库将被映射到进程地址空间的共享区,所以动态库也叫做共享库。
如果我们有多个进程同时需要这一个库,那么现在我们假设,有一个进程调用这个库,如果这个库没有被加载进内存,那么就先将他加载进来,然后映射到进程的共享区,如果说已经加载进来了,那么就直接映射。
本质也就是进程中公共的代码和数据,只需要一份,这样就提高了效率,所以一般经常被使用的文件我们打包成动态库,而不是静态库。
编址问题
首先我们提出可执行程序是有自己的格式信息的,并且在被加载进内存时就已经被划分出了各个区域:
text就是代码区域,data就是已经初始化的数据区,bss就是全局未被初始化的数据。
这里我们抛出一个问题:可执行程序中存在地址吗?
是的,存在,而且进程地址空间中的数据是使用程序中的数据进行初始化的,但是是怎么初始化的呢?进程地址空间是一个结构体,记录着每一个区域的起始和结束,也就是说,既然你程序可以初始化这样的区域,而且你程序中存在地址,也就是说,程序必然有自己的一套编址方式喽,这里我们又要说到,进程地址空间是由操作系统维护,而程序是编译器编译的,也就是说,操作系统和编译器也是有关系的吗?也就是说,虚拟地址空间不仅操作系统要维护,编译器在编址时也要遵守这套规则。
这里我们提出两个概念,绝对编址和相对编址,绝对编址就像是一个坐标系的原点,我们所写的坐标都是按照原点来确定,相对编址就像是给定一个坐标点,其他坐标相对于这个坐标的位置。
我们现代的编译器是按照这种绝对编址,也就是平坦模式进行编址的,过去的编译器是按照相对编址,给出一个段地址,下面的代码地址都是相对于段地址的偏移量,而我们现在的绝对编址其实也可以按照这种方式来理解,只不过我们的段地址只有一个,就是0x0000 0000,剩下的偏移量其实也就是地址,也就是说,我们可以认为我们的可执行程序只有一个区域。
现在,我们可以说到,虚拟地址其实就是这些地址,在程序加载进内存后,使用这些地址初始化进程地址空间,然后用实际的物理地址在页表中构建映射关系。
我们可以通过反汇编来看一下程序中的地址:
使用objdump -S 可执行程序 > 一个文件中
如果是相对编址的话,那么<_init>下面那些地址应该是000000,000004这样的相对地址。
而这些地址,不正是我们将来的虚拟地址吗?!我们在调试时看到的地址也就是这些地址!
理解动态库链接和加载问题
一般程序的加载(链接静态库):
假设我们有一个test.c文件,当我们将他编译成,o文件时,这个文件里的代码就已经有了地址,有了他自己的格式,并且是按照相对编址方式进行编址,静态库也是如此。
当我们将这个.o文件和静态库链接形成可执行程序时,将会将他们的地址以绝对编址的方式重新进行编址,可执行程序会将静态库中需要的代码拷贝进来,最终就是我们上面可执行程序的样子。
当我们将这个可执行程序跑起来时,首先操作系统会先在内存中创建task_struct,此时页表和进程地址空间也会创建,接着将可执行程序的代码和数据加载进内存,此时可执行程序内部有逻辑地址,加载进内存我们称之为虚拟地址,并且现在这些代码有了他们的物理地址,此时就会将这些虚拟地址和物理地址在页表中建立映射关系,并且用可执行程序表头中的数据初始化进程地址空间(mm_struct),并且将pc指针初始化为main的虚拟地址。
接下来,就要开始执行正文部分的代码了,有了pc指针,将他存的虚拟地址通过MMU和页表转化为物理地址,找到物理地址中存储的指令,将其加载进指令寄存器中,就可以开始执行这行指令,执行过这句指令后,pc指针会找到下一条指令的虚拟地址,并继续通过MMU和页表转化为物理地址......,这样,我们的程序就执行起来了。
动态库的加载:
动态库里代码的地址也是根据相对编址进行的编址,同时,可执行程序链接动态库时,不会将代码拷贝进去,只会留下库名+方法的偏移量。
当我们将程序和动态库加载进内存,即将执行到target+0x01这行指令时,程序计数器中保存着他的虚拟地址,根据他的虚拟地址找到他的物理地址,将这行指令加载进指令寄存器中,解析时,根据这个库名找到库的起始虚拟地址,然后这个虚拟地址加上偏移量找到方法的虚拟地址,在MMU和页表转化为物理地址后找到方法的物理地址。