本篇文章,继续来和大家分享与Linux相关的知识。本次内容主要涉及程序没加载前的地址,程序加载后的地址以及动态库的地址。

我们先做一个简单的回顾:

什么是虚拟地址?用来标定进程地址空间中位置的地址,称之虚拟地址,也称之为线性地址。

什么是物理地址?用来标定物理内存中位置的地址。

思考两个问题

1.CPU读到的指令里面用的地址,是什么地址?

2.gcc -fPIC,什么是与地址无关码?

Linxu-二谈进程地址空间_程序加载后的地址

程序没加载前的地址

问大家一个问题,程序编译好之后,内部有地址的概念吗?答案是有的。

编译器在编译程序的时候,不是随便编译的,你用这个地址,他用那个地址,它是需要考虑操作系统的感受。编译器会根据进程地址空间的区域划分来给代码编制地址的。我们称这种编址的方式为平坦模式。过去,没有这种模式,采用段+偏移量的方式来编址。尽管现在,不采用段+偏移量的方式,程序依然可划分成很多段,比如代码段,数据段,BSS段,堆,栈。

我们可以简单的理解程序没加载是下面这样的。其实,在程序没有加载前,它里面的地址就已经是虚拟地址了。只不过为了便于区分,我们把存在磁盘的程序中的地址,称为逻辑地址

Linxu-二谈进程地址空间_fPIC与位置无关码_02

程序加载后的地址(进程)

我们可以简单的编写一个程序。

Linxu-二谈进程地址空间_动态库的地址_03

编译形成a.out,使用指令

objdump -S a.out

就可以查看可执行程序的反汇编,我可以看到代码都变成了sub,mov等指令,每条指令都会有自己的地址和长度。一条指令的地址加上自己的长度就是下一条指令的地址。

Linxu-二谈进程地址空间_动态库的地址_04

CPU怎么认识sub,push这些指令?在CPU会内置很多的基本指令,内置的指令形成的集合,称之为指令集。指令集又分为精简指令复杂指令。sub,push这些指令都由一些内置的基本指令形成,所以,CPU能认识。

代码加载到内存中,要不要分配内存?当然要!!物理内存中的空间都有自己的物理地址。代码被加载到内存中,有了自己的物理空间,也有了自己的物理地址。代码在程序里有虚拟地址,在加载到内存后,又有了物理地址。代码天然的就具备了两种地址。CPU把虚拟地址和物理地址,往页表里一填,虚拟地址和物理地址的映射关系就建立好了。这个过程是有先后顺序的,先有了task_struck结构体和页表,页表的虚拟地址先被填写,然后,再是物理地址,如果读到的代码不在内存中,会引发缺页中断。把相应的代码加载进来,再填写物理地址。

正文代码区有那么多的指令,CPU怎么知道如何执行第一条指令?之前,我们说可执行程序是ELF格式的,它有一个表头,表头里面包含了很多信息,其中一个信息就是入口地址entry。我们的task_struck中,保存了可执行程序的工作目录,CPU可以通过工作目录找到要执行的程序,把入口地址entry读到寄存器EIP/PC中。

当你要执行程序时,CPU就会拿着入口地址entry到页表,根据映射关系找到第一条指令所在的位置。然后,往后执行。当CPU读到call这种指令,call指令是函数里有地址,这个地址是什么地址?当然是虚拟地址啦!!函数也是代码的一部分,代码在程序里用的地址是虚拟地址。

这时,我们就可以回答我们一开始的问题了。CPU读到的指令中,可能有数据,也可能有地址。这个地址是虚拟地址

我会发现,CPU从程序中读取指令,到分析指令,再到二次访问程序中的指令,这一整个过程用的全都是虚拟地址!!

Linxu-二谈进程地址空间_程序加载后的地址_05

动态库的地址

我们在进程地址空间0到全F中的地址,叫绝对地址

Linxu-二谈进程地址空间_程序没有加载前的地址_06

假设有一条跑道,长100m。跑道40处,有一棵树。小明,从起点跑到50米处。这时候,他的好朋友,小华打电话问他在哪?小明说:我在跑道的50米处,这叫绝对地址。在树的往右10米处,这叫相对地址。如果这颗树是在起点的位置,相对地址也就成了绝对地址。所以,相对地址,也叫逻辑地址

Linxu-二谈进程地址空间_fPIC与位置无关码_07

我们知道程序在没有加载前,代码已经有了自己的虚拟地址。那问题来了。当我们调用printf函数的时候,会跳转到共享区的某个位置,那动态库是不是必须加载到这个位置?一个进程可能会使用很多的动态库,如果在调用printf之前,调用了一个其他库的函数,这个库一加载,把我们原来要给C库的位置给占了。那该怎么办?C库又该具体映射到哪里?

很显然,动态库被加载到固定地址空间中的位置是不可能的。否则,会引发一系列的问题。

那怎么做?才能使得库可以在虚拟内存中,任意位置加载!!

动态库让自己内部的函数不要采用绝对编址,只表示每个函数在库中的偏移量即可。未加载程序中,库函数printf的逻辑地址,并不是一个绝对地址,而是一个相对地址,表示它在它所在的库中,相对于库的起始位置的偏移量

当程序加载到内存后,printf的虚拟地址为,动态库实际加载到内存中的起始地址+它在库中的偏移量,也就是下图中的例子:lib_start + 0x1122。

现在,你就能理解-fPIC选项,产生位置无关码了。告诉编译器直接用偏移量对库中的函数进行编制。

静态库为什么不谈加载,不谈位置无关码?静态库是直接把看库函数拷贝到可执行程序中,直接采用绝对编址即可。

Linxu-二谈进程地址空间_动态库的地址_08

好了,到这里,我们本次的分享就到此结束了,不知道我有没有说明白,给予你一点点收获。关于更多和Linux相关的知识,会在后面的文章更新。如果你有所收获,别忘了给我点个赞,这是对我最好的回馈,当然你也可以在评论发表一下你的收获和心得,亦或者指出我的不足之处。如果喜欢我的分享,别忘了给我点关注噢。