链接器在完成了符号解析之后,就可以进行重定位了。

不过由于重定位的内容比较复杂,当然如果你是做编译器或者逆向的话,重定位的原理是必须要求掌握的,
但是作为应用开发者来说这不需要,因为了解详细原理的意义不大,因此有关重定位,我们这里只介绍基本内容


一、重定位的种类

第一种:动态重定位

所谓动态重定位就是,重定位的动作是在程序运行的过程中动态完成的,因此被称为动态重定位。
有关动态重定位,在讲Uboot和内核移植时会介绍。

第二种:静态重定位

所谓静态重定位就是,由链接阶段完成的重定位,因为是在程序运行之前做的重定位,
因此被称为静态重定位。所以我们现在要讲的由链接器完成的静态重定位。


二、静态重定位做什么事情

做两件很重要的事情:

  1. 将同名节整合为新的同名聚合节
  2. 将可执行目标文件中各聚合节的地址,重定位为实际运行的地址

(1)将同名节整合为新的同名聚合节

其中重点是将.text.data节聚合为新的同名聚合节,为了能够将.text.data节聚合为新的聚合节,
需要依赖一些信息,这些信息存储在了.rel.text.rel.data

.rel.text中的信息:实现.text的聚合
.rel.data中的信息:实现.data的聚合

(2)将可执行目标文件中各聚合节的地址,重定位为实际运行的地址
其实就是将程序在内存中实际运行时的内存地址赋给聚合节,程序在内存中运行时,
就是按照重定位的地址来运行的,至于重定位的地址具体是多少,要分裸机和OS两种情况来看

  • 1)如果程序是直接裸机运行的话(没有OS
    程序是直接运行在物理内存上的,所以重定位的运行地址就是物理地址,
    所以CPU取指时所取得的指令地址就是物理地址
  • 2)如果程序是基于OS运行的话
    大部分的OS都有提供虚拟内存机制,所以程序是运行在OS虚拟内存上的,
    虚拟内存所提供的地址就是虚拟地址,所以CPU取指时,所取得的指令地址就是虚拟地址

我们这里所面对的OSLinuxLinux有虚拟内存机制,
所以gcc编译得到的程序基于Linux运行时,就是运行在虚拟内存上的


三、如何指定重定位运行地址

实际上是通过“链接脚本文件”来指定的,“链接脚本文件”里面会说明实际的运行地址是多少,
重定位时会把把实际的运行地址赋值给新的聚合节,如此一来,函数和全局变量就有了真正可以运行的运行地址

实际运行时,将程序拷贝到运行地址所指定的内存位置,
CPUPC存放第一条指令地址(指向第一条指令_start),然后整个程序就运行起来了

至于重定位时,给聚合节具体指定的运行地址应该是多少,这里要分裸机和OS两种情况来定


1. 裸机运行

运行地址(物理地址)是多少,可以由程序员自己来定,比如我们一般可以指定为0
表示程序需要拷贝到物理内存的0地址处,从0地址处开始存放

我们将0地址写到链接脚本文件中,gcc编译时给他指定链接脚本,重定位后运行地址就从0开始

我们后面讲到arm裸机时,会介绍如何修改这个链接脚本

裸机运行时,整个计算机上就一个程序,由于没有OS虚拟内存的参与,所以裸机只能运行一个程序(单进程)


2. 基于OS运行

运行地址(虚拟地址)为一个固定值,不同OS这个固定值不一样,比如在Linux这边

  • 32OS:从0x08048000开始
  • 64OS:从0x0000000000400000开始

程序运行时,程序会被拷贝到虚拟内存的0x08048000或者0x0000000000400000位置处,
然后pc指向_start,程序就运行起来了

这个地址也是指定在链接脚本中的,gcc编译基于Linux运行的程序时,这个链接脚本不需要我们自己给,
是自动给的,这个脚本中会指定0x08048000或者0x0000000000400000的地址

而且gcc编译每一个程序,链接重定位所指定的地址都是0x08048000或者0x0000000000400000


疑问:重定位时,如果每个程序都是相同的0x08048000或者0x0000000000400000的话,运行时不会冲突吗?

不会冲突,因为每个进程的虚拟内存是独立的,虽然都是相同的地址,
但是底层实际对应的都是不同的物理空间,将虚拟地址转换为物理地址后,
得到的物理地址是不同的,所以不同的物理地址所指向的物理空间不同,
自然不同物理内存空间中存放的是不同程序的指令