【问题标题】:Function Call: Labels into memory addresses函数调用:标记到内存地址
【发布时间】:2019-12-21 14:44:10
【问题描述】:

我很难理解正确的事件顺序。 当用抽象语言编写的程序被编译时,它被翻译成机器代码。 随后,只有在程序运行之后,它才会被加载到内存中的代码段中。 此时,程序中的每条指令都将位于特定的内存地址上。 在汇编中调用函数时,Call 语句通常后跟一个标签。 我假设这个标签将被编译器替换为函数的内存地址。 这是我绝对无法理解的地方。 如果指令只在程序运行时才加载到内存中,从而每条指令都获得了自己的内存地址,那么编译器怎么知道标签对应的内存地址呢? 如果函数还没有在内存中,那么以二进制代码编译且标签不再可用的程序如何知道与该标签相对应的内存地址,函数将在执行时加载到哪里?我有点困惑。帮帮我。

【问题讨论】:

  • 传统上,内存地址由链接器选择并且总是相同的。在现代系统中,您有时会看到事先不知道地址的位置无关代码。然而,即便如此,标签之间的距离是恒定的,这就是我们所需要的。

标签: function assembly linker call


【解决方案1】:

一个程序包含几个“部分”(有些是可选的):

  • 保存代码的部分,通常称为文本部分
  • 保存可变全局数据初始值的部分
  • 保存不可变常量的部分,通常称为rodata
  • 具有一组重定位记录的部分

一个节在磁盘上的程序文件中存储为一个连续的内存块或内存块。

加载器创建内存块并将代码、数据、rodata 加载到这些块中;根据操作系统的不同,堆栈将由加载程序创建,也可能由创建子进程的父进程的分叉创建。

知道最终地址后,加载程序还处理重定位记录。这些重定位描述了加载到内存中的部分的最终地址需要更新文本和数据部分中的哪些位置。

重定位机制是通用的,代码可以引用代码,代码可以引用数据,数据可以引用代码,数据可以引用数据。

单个重定位记录描述了需要更新的引用。每条记录描述:

  1. 引用源 — 在文本或数据部分的什么偏移处进行地址更新

  2. 引用目标 — 引用哪个部分:代码或数据

  3. 要进行什么样的更新(某些架构具有复杂的指令编码)

    有些更新是针对普通指针的,而有些更新是针对指令的。具有复杂指令偏移/立即编码的指令集架构,如 MIPS、RISC V、HP-PA,需要告知立即编码方法。

通常引用者已经有一个偏移量,因此更新是被引用部分的基数与引用源处已经存在的偏移量相加/求和的问题。

程序中的其他元数据描述了从哪里开始,例如程序计数器的初始值,它将作为文本部分的偏移量。

当今的大多数处理器都支持(如 fuz 所述)位置无关代码 (PIC)。这通常通过pc-relative 寻址完成。处理器使用 pc-relative addressing modes 在单个文本部分内执行分支和调用,因此这些指令不需要重定位记录。

动态加载的库增加了复杂性,因为每个 DLL 和要运行的主程序都有程序的格式,即它们都有自己的部分;每个都有自己的文本部分。重定位还能够描述对符号导入的引用,由包含符号名称、导入和导出的附加部分支持。

对象文件(编译器输出、预链接)通常也遵循这种格式。单个目标文件具有这些部分,包括重定位记录、符号名称、导入、导出。链接器的工作是将目标文件合并到单个程序或更大的目标文件中。在合并期间,链接器会解析一些重定位,但它不一定会解析所有重定位,因此可能会保留一些让 os loader 解析。

让我们想象一下,在使用 PIC 的系统上,有一个引用:从一个目标文件到另一个目标文件的调用(代码到代码),并且链接器合并这些目标文件。在调用者中将有一个重定位记录,它引用一个导入的符号名称(在另一个目标文件中,一个符号的导出定义为与其文本部分的某个偏移量)。一旦两个目标文件的部分被合并(例如,通过简单地将它们连接成一个更大的文本部分),那里的调用现在有一个段内引用,并且链接器可以计算调用者和被调用者地址之间的增量,并且这些不会因未来的链接或加载而改变。链接器将使用该增量调整调用指令中的偏移量/立即数,并且知道此引用现已解决,在合并中省略此重定位记录。

参考见:

【讨论】:

  • 重要的是要提到 .text.data.bss(以及所有其他部分)之间的距离也是固定的在链接时,可执行文件中静态数据的 PC 相对寻址(或用于访问库的私有静态数据的库代码)不需要 PIE 或非 PIE 或库中的任何运行时重定位。您只提到了文本 section 本身,而不是整个可执行文件/共享对象。 Why are global variables in x86-64 accessed relative to the instruction pointer?
  • @PeterCordes,很高兴知道;这意味着加载程序只为可加载项分配一个块,并且(文本)、rodata 和数据每个页面边界对齐,因此它们可以各自具有不同的权限。
  • 同时,其他系统(例如 AIX)使用 CPU 寄存器来引用数据段,然后使用从该基址开始的常量偏移量来访问该数据段:这使得函数指针更加复杂,因为它们需要携带数据节基值,但是,所有文本节都可以彼此位于同一位置,与所有rodata节以及数据节相同,这可以具有一些大页面的优势。
  • 还有其他系统要求数据段位于链接时硬编码地址(当然,这样做的系统更难支持 DLL,因为 DLL 段放置需要静态任务)。
【解决方案2】:

TL:DR:call 到其目标的距离是链接时间常数。

您从汇编 asm 中获得的 .o 目标文件具有该文件中未定义的符号的重定位记录。

当您将这些.o 文件链接到可执行文件或库中时,链接器会将每个.o 中的.text 部分布局为可执行文件的一个大.text 部分并计算每个call 到达其目标的相对距离。它将相对位移直接编码到每个call 的机器代码中。

在运行时不需要进一步的重定位:无论整个可执行文件加载到内存中的何处,指令之间的距离都不会改变。因此,相对调用不需要运行时重定位。

相关:Why are global variables in x86-64 accessed relative to the instruction pointer?

【讨论】:

    猜你喜欢
    • 2014-10-12
    • 1970-01-01
    • 2014-01-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-17
    相关资源
    最近更新 更多