【问题标题】:How do pointers work "under the hood" in C?指针如何在 C 中“在幕后”工作?
【发布时间】:2014-03-14 17:34:26
【问题描述】:

举一个这样的简单程序:

int main(void)
{
    char p;
    char *q;

    q = &p;

    return 0;
}

&p 是如何确定的?编译器是预先计算所有此类引用还是在运行时完成?如果在运行时,是否有一些变量表或在其中查找这些东西的东西?操作系统是否会跟踪它们并且它只是询问操作系统?

我的问题在正确解释的上下文中甚至可能没有意义,所以请随时直截了当。

【问题讨论】:

  • 您应该查看编译器生成的机器代码。如果你不懂机器代码,那么没有答案对你来说是非常有意义的,所以这将是开始学习的东西。
  • 感谢@Kerrek 的建议,我完全忘记了这些东西的组装说明。对于任何未来的读者,这里是使用objdump 获取汇编代码的链接:stackoverflow.com/questions/1289881/…

标签: c pointers memory-address


【解决方案1】:

&p 是如何确定的?编译器是预先计算所有这些引用还是在运行时完成?

这是编译器的实现细节。不同的编译器可以选择不同的技术,具体取决于它们生成代码的操作系统类型以及编译器编写者的突发奇想。

让我为您描述一下在 Windows 等现代操作系统上这通常是如何完成的。

当进程启动时,操作系统给进程一个虚拟地址空间,比如说2GB。在这 2GB 中,其中的 1MB 部分被留作主线程的“堆栈”。堆栈是一个内存区域,其中当前堆栈指针“下方”的所有内容都“正在使用”,而该 1MB 部分“上方”的所有内容都是“空闲”的。操作系统如何选择哪一块 1MB 的虚拟地址空间作为栈是 Windows 的一个实现细节。

(旁白:空闲空间是在堆栈的“顶部”还是“底部”,“有效”空间是“向上”还是“向下”增长也是一个实现细节。不同的操作系统在不同的芯片上换一种方式。假设堆栈从高地址向低地址增长。)

操作系统确保在调用main 时,寄存器ESP 包含堆栈的有效部分和空闲部分之间的分界线地址。

(另外,ESP 是第一个 valid 点的地址还是第一个 free 点的地址是一个实现细节。)

编译器为main 生成代码,推送堆栈指针,比如说五个字节,如果堆栈“向下”增长,则从它减去。它减少了 5,因为 p 需要 1 个字节,q 需要 4 个字节。所以堆栈指针改变了;现在有五个“有效”字节和五个“空闲”字节。

假设q 是现在位于ESPESP+3 中的内存,p 是现在位于ESP+4 中的内存。要将p 的地址分配给q,编译器会生成将四个字节 ESP+4 复制到位置 ESPESP+3 的代码.

(旁白:请注意,编译器很可能会布置堆栈,以便获取地址的所有内容都位于可被 4 整除的 ESP+offset 值上。某些芯片要求地址可被指针整除大小。同样,这是一个实现细节。)

如果您不了解用作值的地址与用作存储位置的地址之间的区别,请弄清楚。如果不了解关键区别,您将无法在 C 语言中取得成功。

这是它可以工作的一种方式,但就像我说的那样,不同的编译器可以根据他们认为合适的方式选择不同的方式。

【讨论】:

  • 非常感谢您花时间写这篇文章。你很好地将这方面的许多不同方面联系在一起。一件小事:如果你想镜像我上面发布的程序,你可能应该将其更改为“p 一个字节,q 四个字节”。
  • @AmadeusDrZaius:你说得对,我错过了 pchar
  • @AmadeusDrZaius:非常欢迎您!要了解更多信息,我建议您编译一些程序禁用所有优化,然后在调试器中检查生成的汇编代码。您将很快看到编译器如何发挥它的魔力。
【解决方案2】:

编译器在编译时无法知道p 的完整地址,因为一个函数可以被不同的调用者多次调用,而p 可以有不同的值。

当然,编译器必须知道如何在运行时计算p 的地址,不仅仅是为了地址运算符,而是为了生成有效的代码使用 p 变量。在常规架构上,像p 这样的局部变量被分配在堆栈上,即相对于当前堆栈帧的地址具有固定偏移的位置。

因此,q = &p 行只是将p 在当前堆栈帧中的地址存储到q(分配在堆栈上的另一个局部变量)中。

请注意,一般而言,编译器知道或不知道的内容取决于实现。例如,优化编译器很可能会在分析其行为没有可观察到的效果后优化掉整个main。以上是在主流架构和编译器的假设下编写的,并且是一个可以被多个调用者调用的非静态函数(main除外)。

【讨论】:

  • 我明白了。我想我只是忘记了这一点,但基本上它会在汇编代码中使用lea(加载有效地址),偏移量由类型决定?
  • @AmadeusDrZaius 偏移量将在编译时确定;例如,变量p 可能获得偏移量0,变量q 偏移量4。存储在寄存器中的变量根本不会获得偏移量,等等。如果您了解汇编,只需查看汇编输出 - 在 GCC 下你可以通过gcc -S foo.c 得到它。
【解决方案3】:

这实际上是一个非常难以全面回答的问题,因为virtual memoryaddress space layout randomizationrelocation 非常复杂。

简短的回答是,编译器基本上处理一些“基”的偏移量,这是由运行时加载程序在执行程序时决定的。您的变量pq 将看起来非常接近stack 的“底部”(尽管堆栈基数在VM 中通常非常高并且它会“向下”增长)。

【讨论】:

  • 感谢您的链接。
【解决方案4】:

在编译时无法完全计算出局部变量的地址。局部变量通常分配在堆栈中。当被调用时,每个函数都会分配一个 堆栈帧 - 一个连续的内存块,其中存储了它的所有局部变量。在编译时无法预测堆栈帧在内存中的物理位置。它只会在运行时为人所知。每个堆栈帧的开头通常在运行时存储在专用处理器寄存器中,例如英特尔平台上的ebp

同时,堆栈帧的内部存储器布局由编译器在编译时预先确定,即编译器决定局部变量在堆栈帧内的布局方式。这意味着编译器知道堆栈帧内每个局部变量的局部偏移量。

把这一切放在一起,我们得到一个局部变量的确切绝对地址是堆栈帧本身(运行时组件)的地址和这个变量的偏移量的总和在那个框架内(编译时组件)。

这基本上就是编译后的代码

q = &p;

会的。它将获取堆栈帧寄存器的当前值,为其添加一些编译时常量(p 的偏移量)并将结果存储在q

【讨论】:

    【解决方案5】:

    在任何函数中,函数参数和局部变量都分配在堆栈上,在最后一个函数调用当前函数的位置(程序计数器)之后。这些变量如何在堆栈上分配,然后在从函数返回时释放,由编译器在编译时处理。

    例如对于这种情况,p(1 字节)可以首先在堆栈上分配,然后是 q(4 字节对于 32 位架构)。代码将 p 的地址分配给 q。 p 的地址自然是堆栈指针的最后一个值加或减 5。嗯,类似的事情,取决于堆栈指针的值如何更新以及堆栈是向上还是向下增长。

    返回值如何传递回调用函数我不确定,但我猜它是通过寄存器而不是堆栈传递的。因此,当调用 return 时,底层汇编代码应该释放 p 和 q,将零放入寄存器,然后返回调用函数的最后一个位置。当然,在这种情况下,它是主函数,所以更复杂的是,它会导致操作系统终止进程。但在其他情况下,它只是回到调用函数。

    在 ANSI C 中,所有局部变量都应该放在函数的顶部,并在进入函数时分配一次到堆栈中,并在从函数返回时释放。在 C++ 或更高版本的 C 中,当局部变量也可以在块内声明时(如 if-else 或 while 语句块),这变得更加复杂。在这种情况下,局部变量在进入块时被分配到堆栈上,在离开块时被释放。

    在所有情况下,局部变量的地址始终是从堆栈指针中添加或减去的固定数字(由编译器计算,相对于包含块),并且变量的大小由变量类型确定.

    但是,static 局部变量和全局变量在 C 中是不同的。它们分配在内存中的固定位置,因此它们有一个固定地址(或相对于进程边界的固定偏移量),这由链接器计算。

    第三种是使用 malloc/new 和 free/delete 在堆上分配的内存。我认为如果我们也包括在内,这个讨论会太长。

    也就是说,我的描述仅针对典型的硬件架构和操作系统。正如 Emmet 所提到的,所有这些也都依赖于各种各样的事物。

    【讨论】:

      【解决方案6】:

      p 是一个自动存储的变量。它的生命周期与它在生命周期中的功能一样长。每次调用它的函数时都会从堆栈中取出它的内存,因此,它的地址可以改变并且直到运行时才知道。

      【讨论】:

      • 你的意思是自动存储期限?
      • @ouah 是的。自动持续时间/存储。谢谢。
      猜你喜欢
      • 1970-01-01
      • 2022-01-11
      • 1970-01-01
      • 2010-09-23
      • 2011-04-15
      • 1970-01-01
      • 1970-01-01
      • 2013-08-17
      • 1970-01-01
      相关资源
      最近更新 更多