位置相关代码的示例是什么



位置相关代码是为了加载到内存中的特定物理地址中并从中运行而编写的。这种类型的代码带来的问题之一是,它阻碍了处理器同时运行多个进程的能力,主要是当从同一地址写入运行的不同进程试图同时执行时。

话虽如此,我从未遇到过指定要在其上执行的内存地址的代码,因此我发现很难想象这样的代码会是什么样子。我可以看到,给定的代码可以指定特定变量存储在内存中的地址,但当涉及到要加载程序的[第一个]内存地址时,我不明白为什么这不是操作系统的工作,而不是程序的责任。

这类代码带来的问题之一是,它阻碍了处理器同时运行多个进程的能力,主要是当从同一地址写入并运行的不同进程试图同时执行时。

在大多数台式消费电脑上,都有分页功能。对于分页,CPU指令包含虚拟地址,而不是物理地址。在执行之前,指令操作的地址被传递到MMU(存储器管理单元)以通过页表进行转换。这些页表可以在物理RAM中的任何位置转换虚拟地址。

在今天的计算机上,每个线程都在一个特定的核心上运行。每个核心都有自己的页表基指针(PTBP)寄存器,该寄存器包含第一级页表开头的物理地址。当操作系统想要切换线程时,它会保存当前正在执行的线程的信息,并将其切换到其他线程的信息(包括PTBP寄存器)。

由于虚拟地址(VA)可以在RAM中的任何位置进行转换,因此每个线程都可以访问所有可用的虚拟地址空间(VAS)。由于VAS的跨度超过10万GB,因此每个线程的RAM数量仅受可用内存的物理量限制。每个线程都可以从同一地址开始,并且只要页面表将它们提到的地址转换为不同的物理地址,就可以同时执行。

我从未遇到过指定要在其上执行的内存地址的代码,因此我发现很难想象这样的代码会是什么样子。

您遇到的大多数代码实际上都指定了起始地址。起始地址主要是一个建议,不一定会受到操作系统加载程序的尊重。这样做是因为在ASLR出现之前,代码实际上不是位置独立的,只是简单地提到所有地址都是绝对的。正如您所提到的,如今大多数代码都是独立于位置的。即使使用与位置无关的代码,也看不到编译器从代码中输出的地址,但有很多地址。编译器负责计算代码中的地址和偏移量,以访问某些函数或某些数据。

目前现代计算机上主要有三种类型的存储器:

  1. 自动内存(堆栈)

对于自动存储,编译器只计算堆栈指针寄存器的偏移量。它也可以是基指针寄存器的偏移量。例如,在x64上,如果没有优化,你会有这样的东西:

int main( void ) {
int a;
a = 3;
return 0;
}

编译为:

main:
push    rbp
mov     rbp, rsp
mov     DWORD PTR [rbp-4], 3
mov     eax, 0
pop     rbp
ret

在这里,您可以看到第三条指令mov DWORD PTR [rbp-4], 3正在访问RBP偏移量为4字节的堆栈,并根据请求将值3放置在该地址。

  1. 静态/全局数据

静态或全局数据是声明为静态或在函数外部声明的数据。编译后,此数据在可执行文件中保留了空间。操作系统在程序加载期间将数据放入RAM。使用PC相对寻址(与程序计数器有偏移)访问数据,以使访问位置独立。再次在x64上,你会有类似的东西:

int glob;
int main( void ) {
glob = 5;
return 0;
}

编译到:

glob:
.zero   4
main:
push    rbp
mov     rbp, rsp
mov     DWORD PTR glob[rip], 5
mov     eax, 0
pop     rbp
ret

同样,第三条指令通过取消引用RIP(x64上的PC)的偏移量,将5放入全局整数中。

  1. 堆数据

堆为其数据保留了几乎一半的VAS。它可以从可执行代码和静态数据的末尾扩展到VAS的一半(超过100k GB)。较高的一半保留给内核(显然内核不需要全部)。堆实际上是动态分配的。您需要在C/C++中进行系统调用以获得内存。为此,您将调用malloc()或使用new关键字。由于该内存是动态的,因此只需使用绝对地址即可访问该内存。根据操作的不同,它可能会使用一个临时寄存器来保存值或地址。

最新更新