采取这样一个简单的程序:
int main(void)
{
char p;
char *q;
q = &p;
return 0;
}
&p
如何确定?编译器是在事先计算所有这样的引用还是在运行时完成?如果在运行时,是否有一些变量表或某些东西,它看起来像这些东西?操作系统是否跟踪它们,它只是询问操作系统?
我的问题在正确解释的背景下甚至可能没有意义,所以请随意让我直截了当。
答案 0 :(得分:6)
&p
如何确定?编译器是在事先计算所有这些引用还是在运行时完成?
这是编译器的实现细节。不同的编译器可以根据它们生成代码的操作系统的类型和编译器编写者的想法选择不同的技术。
让我先介绍一下如何在像Windows这样的现代操作系统上完成这项工作。
当进程启动时,操作系统会为进程提供一个虚拟地址空间,即2GB。在2GB中,1MB的部分留作"堆栈"为主线程。堆栈是一个内存区域,其中所有内容都在"下面"当前的堆栈指针是"正在使用",以及该1MB部分中的所有内容"以上"这是"免费"。操作系统如何选择哪个1MB的虚拟地址空间是堆栈,这是Windows的实现细节。
(除此之外:自由空间是否位于堆栈的"顶部"或"底部"是否"有效"空间增长" up"或" down"也是一个实现细节。不同芯片上的不同操作系统有不同的做法。假设堆栈从高地址变为低地址。)
操作系统确保在调用main
时,寄存器ESP
包含堆栈的有效部分和空闲部分之间的分界线的地址。
(除此之外:ESP
是第一个有效点的地址还是第一个 free 点是实现细节。)
编译器生成main
的代码,推送堆栈指针,让我们说五个字节,如果堆栈正在增长" down"则减去它。它减少了五,因为p
需要一个字节,q
需要四个字节。所以堆栈指针改变了;现在还有五个"有效"字节和五个更少"免费"字节。
让我们说q
是ESP
到ESP+3
中的内存,而p
是ESP+4
中的内存。要将p
的地址分配给q
,编译器会生成将四个字节值 ESP+4
复制到位置 {的代码{1}}到ESP
。
(旁白:请注意,编译器很可能会放置堆栈,以便所有具有其地址的值都在ESP+3
值上,该值可以被4整除。某些芯片的要求是地址可以被整除通过指针大小。再次,这是一个实现细节。)
如果您不理解用作值的地址与用作存储位置的地址之间的区别,请指出。如果不理解关键差异,你将无法在C中获得成功。
这是可以工作的一种方式,但就像我说的那样,不同的编译器可以选择以他们认为合适的方式进行不同的操作。
答案 1 :(得分:5)
编译器在编译时无法知道p
的完整地址,因为不同的调用者可以多次调用函数,p
可以有不同的值。
当然,编译器必须知道如何在运行时计算地址p
,不仅仅是地址运算符,而只是为了生成代码适用于p
变量。在常规体系结构中,{em}堆栈上分配了p
等局部变量,即相对于当前堆栈帧地址具有固定偏移量的位置。
因此,行q = &p
只存储到q
(在堆栈上分配的另一个局部变量)地址p
在当前堆栈帧中。
请注意,通常,编译器执行或不知道的是与实现相关的。例如,优化编译器在分析其操作没有可观察到的影响后,可以很好地优化整个main
。以上是在主流架构和编译器的假设下编写的,以及可由多个调用者调用的非静态函数(main
除外)。
答案 2 :(得分:2)
这实际上是一个非常难以回答的问题,因为它在virtual memory,address space layout randomization和relocation之间大量复杂化。
简短的回答是,编译器基本上处理来自某些“基础”的偏移量,这是由执行程序加载程序在执行程序时决定的。您的变量p
和q
将非常接近stack的“底部”(尽管VM中的堆栈基数通常非常高并且“向下”增长)。
答案 3 :(得分:1)
p
是具有自动存储功能的变量。它的存在只有它在生命中的功能。每次调用它的函数时,它都是从堆栈中获取的,因此,它的地址可以更改,直到运行时才会知道。
答案 4 :(得分:1)
在任何函数中,函数参数和局部变量在堆栈上分配,在最后一个函数的位置(程序计数器)之后调用当前函数。这些变量如何在堆栈上分配,然后在从函数返回时释放,在编译期间由编译器负责。
例如对于这种情况,p(1字节)可以先在堆栈上分配,然后是q(32位架构为4字节)。代码将p的地址分配给q。然后,p的地址自然地从堆栈指针的最后一个值中加上或减去。那么,类似的东西,取决于堆栈指针的值如何更新以及堆栈是向上还是向下增长。
返回值如何传递回调用函数是我不确定的,但我猜测它是通过寄存器而不是堆栈传递的。因此,当调用返回时,底层汇编代码应解除分配p和q,将0置于寄存器中,然后返回调用函数的最后位置。当然,在这种情况下,它是主要功能,因此它更复杂,它导致OS终止进程。但在其他情况下,它只是回到调用函数。
在ANSI C中,所有局部变量都应放在函数的顶部,并在进入函数时一次分配到堆栈中,并在从函数返回时取消分配。在C ++或更高版本的C中,当局部变量也可以在块内部声明时(例如if-else或while语句块),这变得更加复杂。在这种情况下,局部变量在进入块时被分配到堆栈中,并在离开块时被释放。
在所有情况下,局部变量的地址始终是从堆栈指针中添加或减去的固定数字(由编译器计算,相对于包含块),变量的大小由变量类型确定
然而,static
局部变量和全局变量在C中是不同的。这些在内存中的固定位置分配,因此它们具有固定的地址(或相对于过程的固定偏移量#39; boundary),由链接器计算。
然而第三种是使用malloc / new和free / delete在堆上分配的内存。我认为如果我们也包含这个讨论,这个讨论会过于冗长。
也就是说,我的描述仅适用于典型的硬件架构和操作系统。如Emmet所述,所有这些都依赖于各种各样的东西。
答案 5 :(得分:1)
在编译时无法完全计算局部变量的地址。局部变量通常在堆栈中分配。调用时,每个函数都会分配一个堆栈帧 - 一个连续的内存块,它会存储所有局部变量。在编译时无法预测堆栈帧在内存中的物理位置。它只会在运行时知道。每个堆栈帧的开头通常在运行时存储在专用处理器寄存器中,如英特尔平台上的ebp
。
同时,堆栈帧的内部存储器布局由编译器在编译时预先确定,即编译器决定局部变量如何在堆栈帧内布局。这意味着编译器知道堆栈帧内每个局部变量的本地偏移量。
把这一切放在一起我们得到一个局部变量的确切绝对地址是堆栈帧本身地址的总和(运行时组件)和这个变量的偏移量在该框架内(编译时组件)。
这基本上就是
的编译代码q = &p;
会做的。它将获取堆栈帧寄存器的当前值,向其添加一些编译时常量(p
的偏移量)并将结果存储在q
中。