1

如果我执行以下操作:

int i, *p = &i;
int **p2p = &p;

我得到 i 的地址(在堆栈上)并将其传递给 p,然后我得到 p 的地址(在堆栈上)并将其传递给 p2p。

我的问题是,我们知道 的值i保存在内存地址中p等等,但是操作系统如何知道该地址在哪里?我想他们的地址被组织在堆栈中。每个声明的变量(标识符)是否被视为与堆栈当前位置的偏移量?全局变量呢,操作系统和/或编译器如何在执行期间处理这些变量?操作系统和编译器如何在不使用内存的情况下“记住”每个标识符的地址?是否所有变量都按顺序输入(推送)到堆栈中,并且它们的名称被替换为它们的偏移量?如果是这样,可以改变声明顺序的条件代码呢?

4

5 回答 5

2

我曾经是一名汇编语言程序员,所以我知道我曾经使用过的 CPU 的答案。要点是 CPU 的寄存器之一被用作堆栈指针,称为 SP(或esp现在在 x86 CPU 上)。编译器引用相对于 SP 的变量(在您的情况下为 i、p 和 p2p)。换句话说,编译器决定每个变量的偏移量应该是SP,并相应地生成机器代码。

于 2013-04-13T13:31:35.003 回答
1

从概念上讲,数据可以存储在 4 个不同的内存区域中,具体取决于其范围以及它是常量还是变量。我说“概念上”是因为内存分配非常依赖于平台,为了尽可能提高现代架构所能提供的效率,策略可能会变得非常复杂。

同样重要的是要意识到,除了少数例外,操作系统不知道也不关心变量驻留在哪里。CPU会。它是 CPU 处理程序中的每个操作、计算地址以及读写内存。事实上,操作系统本身只是一个程序,它有自己的变量,由 CPU 执行。

通常,编译器决定为每个变量分配哪种类型的内存(例如堆栈、堆、寄存器)。如果它选择一个寄存器,它也决定分配哪个寄存器。如果它选择另一种类型的内存,它会计算变量从那部分内存开始的偏移量。它创建一个“对象”文件,该文件仍然引用这些变量作为从其部分开始的偏移量。

然后链接器读取每个目标文件,将它们的变量组合和排序到适当的部分,然后“修复”偏移量。(这是技术术语。真的。)

常数数据

它是什么?
由于这些数据永远不会改变,它通常与程序本身一起存储在只读内存区域中。在嵌入式系统中,例如微波炉,这可能在(传统上便宜的)ROM 中,而不是(更昂贵的)RAM 中。在 PC 上,它是一段被操作系统指定为仅就绪的 RAM 段,因此尝试写入它会导致分段错误并在程序“非法”更改不应更改的内容之前停止程序。

它是如何访问的?
编译器通常引用常量数据作为从常量数据段开始的偏移量。链接器知道段实际所在的位置,因此它修复了段的起始地址。

全局和静态数据

它是什么?
这些数据必须在运行程序的整个生命周期内都可用,因此它必须驻留在已分配给程序的内存“堆”上。由于数据可以更改,因此堆不能像常量数据那样驻留在只读内存中;它必须驻留在可写 RAM 中。

它是如何访问的?
CPU 以与常量数据相同的方式访问全局和静态数据:它被引用为从堆开始的偏移量,堆的起始地址由链接器固定。

本地数据

它是什么? 这些是仅在封闭函数处于活动状态时才存在的变量。它们驻留在动态分配的 RAM 中,然后在函数退出时立即返回给系统。从概念上讲,它们是从随着函数被调用和创建变量而增长的“堆栈”分配的;它随着每个函数的返回而缩小。堆栈还保存每个函数调用的“返回地址”:CPU 记录其在程序中的当前位置,并在调用函数之前将该地址“推入”堆栈;然后,当函数返回时,它会从堆栈中“弹出”地址,以便它可以从函数调用之前的任何位置恢复。但同样,实际的实现取决于架构;重要的是要记住函数的本地数据变得无效,

它是如何访问的?
本地数据通过其从堆栈开头的偏移量访问。编译器在进入函数时知道下一个可用的堆栈地址,并且忽略一些深奥的情况,它还知道局部变量需要多少内存,因此它移动“堆栈指针”以跳过该内存。然后它通过计算堆栈中的地址来引用每个局部变量。

寄存器

这些是什么? 寄存器是 CPU 内部的一小块内存区域。所有计算都发生在寄存器内,寄存器操作非常快。CPU 包含的寄存器数量相对较少,因此它们是有限的资源。

它们是如何访问的? CPU 可以直接访问寄存器,这使得寄存器操作非常快速。编译器可能会选择将寄存器分配给变量作为优化,因此它在获取数据或将数据写入 RAM 时无需等待。通常,只有本地数据分配给寄存器。例如,循环计数器可能驻留在寄存器中,而堆栈指针本身就是一个寄存器。

您的问题的答案: 当您在堆栈上声明一个变量时,编译器会计算它的大小并为其分配内存,从堆栈上的下一个可用位置开始。让我们看一下您的示例,做出以下假设:

1. 调用函数时SP,堆栈中的下一个可用地址是向下增长的。
2. sizeof(int)= 2(只是为了使它与指针的大小不同)。
3. sizeof(int *)= sizeof(int **)= 4(即所有指针大小相同)。
然后:

    整数 i, *p = &i;
    int **p2p = &p;
您声明了 3 个变量:

i: Addr = SP, size = 2, contents = : Addr = , size = , contents = (address of ) : Addr = , size = , contents = (address of )uninitialized
pSP-24SP i
p2pSP-64SP-2 p

于 2013-04-13T15:16:26.463 回答
1

操作系统不关心您的程序使用的地址。每当发出需要使用地址空间内的缓冲区的系统调用时,您的程序都会提供缓冲区的地址。

您的编译器为您的每个函数提供一个堆栈框架。

push ebp
mov ebp,esp

然后,任何函数参数或局部变量都可以相对于 EBP 寄存器的值进行寻址,该值是该堆栈帧的基地址。这由编译器通过特定于您的编译器的参考表来处理。

退出函数后,编译器会拆除堆栈帧:

mov esp,ebp
pop ebp

在低级别,CPU 仅适用于文字 BYTE/WORD/DWORD/etc 值和地址(它们相同,但使用方式不同)。

所需的内存地址要么存储在命名缓冲区(例如全局变量)中,编译器在编译时用其已知地址替换它,要么存储在 CPU 的寄存器中(非常简化,但仍然正确)

进入操作系统开发,如果您愿意,我很乐意更深入地解释我所知道的任何事情,但这肯定超出了 SOF 的范围,所以如果您有兴趣,我们需要寻找另一个渠道。

于 2013-04-13T13:33:11.667 回答
1

i 的值保存在内存地址 p 中等等,但是操作系统如何知道该地址在哪里?

操作系统不知道也不关心变量在哪里。

我想 [variables'] 地址被组织在堆栈中。

堆栈不组织变量的地址。它只是包含/保存变量的值。

每个声明的变量(标识符)是否被视为与堆栈当前位置的偏移量?

这可能确实适用于某些局部变量。但是,优化可以将变量移动到 CPU 寄存器中,也可以完全消除它们。

全局变量呢,操作系统和/或编译器如何在执行期间处理这些变量?

当程序已经编译时,编译器不处理变量。它已经完成了它的工作。

操作系统和编译器如何在不使用内存的情况下“记住”每个标识符的地址?

操作系统不记得任何这些。它甚至对你程序的变量一无所知。对于操作系统来说,你的程序只是一些无定形的代码和数据的集合。变量的名称是无意义的,并且在编译程序中很少可用。只有程序员和编译器才需要它们。CPU 和操作系统都不需要它们。

是否所有变量都按顺序输入(推送)到堆栈中,并且它们的名称被替换为它们的偏移量?

这将是局部变量的合理简化模型。

如果是这样,可以改变声明顺序的条件代码呢?

这就是编译器必须处理的问题。程序编译完成后,一切都已处理完毕。

于 2013-04-13T13:34:45.860 回答
0

正如@Stochasticly 解释的那样:

编译器引用相对于 SP 的变量(在您的情况下为 i、p 和 p2p)。换句话说,编译器决定每个变量的偏移量应该是SP,并相应地生成机器代码。

也许这个例子还向你解释了它。它在 amd64 上,因此指针大小为 8 个字节。如您所见,没有变量,只有寄存器的偏移量。

#include <cstdlib>
#include <stdio.h>
using namespace std;

/*
 * 
 */
int main(int argc, char** argv) {

    int i, *p = &i;
    int **p2p = &p;
    printf("address 0f i: %p",p);//0x7fff4d24ae8c
    return 0;
}

拆卸:

!int main(int argc, char** argv) {
main(int, char**)+0: push   %rbp
main(int, char**)+1: mov    %rsp,%rbp
main(int, char**)+4: sub    $0x30,%rsp
main(int, char**)+8: mov    %edi,-0x24(%rbp)
main(int, char**)+11: mov    %rsi,-0x30(%rbp)
!
!    int i, *p = &i;
main(int, char**)+15: lea    -0x4(%rbp),%rax
main(int, char**)+19: mov    %rax,-0x10(%rbp)  //8(pointer)+4(int)=12=0x10-0x4
!    int **p2p = &p;
main(int, char**)+23: lea    -0x10(%rbp),%rax
main(int, char**)+27: mov    %rax,-0x18(%rbp) //8(pointer)
!    printf("address 0f i: %p",p);//0x7fff4d24ae8c
main(int, char**)+31: mov    -0x10(%rbp),%rax //this is pointer
main(int, char**)+35: mov    %rax,%rsi //get address of variable, value would be %esi
main(int, char**)+38: mov    $0x4006fc,%edi
main(int, char**)+43: mov    $0x0,%eax
main(int, char**)+48: callq  0x4004c0 <printf@plt>
!    return 0;
main(int, char**)+53: mov    $0x0,%eax
!}
main(int, char**)()
main(int, char**)+58: leaveq 
main(int, char**)+59: retq 
于 2013-04-13T13:41:53.173 回答