【问题标题】:How global pointer variables are stored in memory?全局指针变量如何存储在内存中?
【发布时间】:2017-05-03 20:32:27
【问题描述】:

假设我们有一个简单的代码:

int* q = new int(13);

int main() {
    return 0;
}

很明显,变量q 是全局变量并已初始化。从this answer,我们期望q变量存储在程序文件中的初始化数据段(.data)中,但它是一个指针,所以它是值(这是堆段中的地址)是在运行时确定的。那么程序文件中数据段中存储的值是多少呢?

我的尝试:
在我的想法中,编译器在 data 段中为变量q(64 位地址通常为 8 个字节)分配了一些空间,没有有意义的值。然后,在main函数代码之前的text段中放置一些初始化代码,以在运行时初始化q变量。汇编中的这样的东西:

     ....
     mov  edi, 4
     call operator new(unsigned long)
     mov  DWORD PTR [rax], 13  // rax: 64 bit address (pointer value)

     // offset : q variable offset in data segment, calculated by compiler
     mov  QWORD PTR [ds+offset], rax // store address in data segment
     ....
main:
     ....

有什么想法吗?

【问题讨论】:

  • gcc链接时,默认起点为_start。此初始化将在其中包含代码,然后它将调用main。所以人们在没有 clib 的情况下进行汇编编程,但与 gcc 链接,必须在他们的代码开头放置 _start: 标签,而链接默认 clib 的人从 main: 开始(在他们的源代码中,二进制文件从 @ 987654335@ 来自 lib)。 :)
  • 详细说明,_start 不是函数,因此您不能在 C 或 C++ 中编写 _start。在汇编中,你不必写函数,你可以写任意代码,所以你可以自己写_start
  • 知道了,谢谢 Ped7g 和 Dietrich Epp。
  • int *q 将进入.bss,而不是.data 部分,因为它仅在运行时由构造函数初始化。可执行文件的数据段中不需要有 8 个字节。

标签: c++ pointers assembly heap-memory compile-time


【解决方案1】:

是的,本质上就是这样。

请注意,在 ELF 中,.data.bss.text 实际上是段,而不是段。您可以通过运行编译器自己查看程序集:

c++ -S -O2 test.cpp

您通常会看到一个main 函数,以及该函数之外的某种初始化代码。程序入口点(C++ 运行时的一部分)将调用初始化代码,然后调用main。初始化代码还负责运行诸如构造函数之类的东西。

【讨论】:

  • 谢谢,您能简单说说segment和section之间的区别吗?
  • 节在链接时用于从目标文件生成二进制文件。段在运行时用于将二进制文件加载到内存中。段没有名称。
  • @DietrichEpp:谈论文本段(链接器放置.text.rodata 和其他各种内容的位置)或数据段(.data)是非常标准的,或者BSS。不过,我想这不是 ELF 的官方部分,因为readelf -a 输出仅显示节到段映射中的编号段。
  • @MehranTorki:如果您想知道,Linux 中的 ELF 段与 x86 段寄存器无关。不过,它们都来自相似的历史概念。
【解决方案2】:

int *q 将进入 .bss,而不是 .data 部分,因为它仅在运行时由非常量初始化程序初始化(因此这仅在 C++ 中合法,在 C 中不合法)。可执行文件的数据段中不需要有 8 个字节。

编译器通过将初始化函数的地址放入初始化程序数组中来安排初始化程序函数的运行,CRT(C 运行时)启动代码在调用 main 之前调用这些初始化程序。

在 Godbolt 编译器资源管理器中,您可以看到 init 函数的 asm,而不会受到指令的干扰。请注意,寻址模式只是对q 的简单RIP 相对访问。此时链接器会填充与 RIP 的正确偏移量,因为这是一个链接时间常数,即使 .text.bss 部分最终位于不同的段中。

Godbolt 的 compiler-noise filtering 不适合我们。一些指令是相关的,但其中许多不是。下面是gcc6.2 -O3 asm output with Godbolt's "filter directives" option unchecked 的手工选择组合,仅用于int* q = new int(13); 语句。 (无需同时编译main,我们没有链接可执行文件)。

# gcc6.2 -O3 output
_GLOBAL__sub_I_q:      # presumably stands for subroutine
    sub     rsp, 8           # align the stack for calling another function
    mov     edi, 4           # 4 bytes
    call    operator new(unsigned long)   # this is the demangled name, like from objdump -dC
    mov     DWORD PTR [rax], 13
    mov     QWORD PTR q[rip], rax      # clang uses the equivalent `[rip + q]`
    add     rsp, 8
    ret

    .globl  q
    .bss
q:
    .zero   8      # reserve 8 bytes in the BSS

没有对 ELF 数据(或任何其他)段的基础的引用。

也绝对没有段寄存器覆盖。 ELF 段与 x86 段无关。 (无论如何,默认的段寄存器是DS,所以编译器不需要发出[ds:rip+q]或任何东西。一些反汇编程序可能是显式的并显示DS,即使指令上没有段覆盖前缀,不过。)


这是编译器安排它在main()之前被调用的方式:

    # the "aw" sets options / flags for this section to tell the linker about it.
    .section        .init_array,"aw"
    .align 8
    .quad   _GLOBAL__sub_I_q       # this assembles to the absolute address of the function.

CRT 起始代码有一个循环,该循环知道.init_array 部分的大小,并在每个函数指针上依次使用内存间接call 指令。

.init_array 部分被标记为可写,因此它进入数据段。我不确定它写的是什么。也许 CRT 代码通过在调用它们后将指针归零来将其标记为已经完成?


Linux 中有类似的机制用于在动态库中运行初始化程序,这是由 ELF 解释器在进行动态链接时完成的。这就是为什么您可以在从手写 asm 创建的动态链接二进制文件中调用 printf() 或其他 glibc stdio 函数的原因. (有关构建静态或动态二进制文件的更多信息,请参阅this Q&A,这些二进制文件定义了自己的_start 或只是main(),有或没有libc)。

【讨论】:

  • 我用 & 不带指针变量编译了我的问题中的代码,结果是 data 部分大小发生了变化。使用size 命令检查它。所以它必须存储在data部分。
  • @MehranTorki:用什么编译器? 64 位还是 32 位代码?什么操作系统(我假设是 Linux)?由于q 是全局的,也许一些额外的.data 大小用于GOT 或其他什么?在我展示的 asm 输出中,指针本身肯定存储在 .bss 中,就像我预期的那样。任何编译器将其放入 .data 都是零意义的,除非它可以在编译时评估 new 并避免运行时初始化程序。但这意味着delete-able 指针将嵌入到可执行文件中,因此编译器必须依赖于 libstdc++ 的内部工作。
  • @MehranTorki:我自己试过了。一个空的 .cpp 编译为一个带有 0/0/0 test/data/bss 的 .o。 gcc5.2 -O3 将 int* q = new int(13); 编译为 .o,size 表示在文本段中有 80 个字节,在数据段中有 8 个字节,在 bss 中有 8 个字节。 bss 中的 8 个字节用于q,正如我在objdump -D 中看到的那样(它将符号显示为.bss 部分的一部分)。 readelf -a 证实了这一点:q 在第 3 部分,即 .bss。我还没有弄清楚.data 中的 是什么,但它绝对不是q 本身的存储空间。
  • @MehranTorki:哦,我刚刚想通了:.init_array 部分进入数据段readelf -a 表明它的标志是WA(可写,已分配),这意味着它的非零可写数据,所以它需要进入数据段。它的大小是 8 个字节。
  • 这真的很有挑战性 :) 感谢您的努力。顺便说一句,我有两个问题:1.为什么rip寄存器用于访问存储rax? 2. “ELF 段与 x86 段无关”我不明白,没有ds 寄存器如何访问数据段中的变量?你能给我一个链接以供进一步阅读吗?对不起,我对这个话题几乎是新手。谢谢
猜你喜欢
  • 1970-01-01
  • 2016-04-24
  • 1970-01-01
  • 1970-01-01
  • 2021-10-18
  • 2013-10-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多