其中是保持的c/c++指针的内存地址

Where is the memory address of a c/c++ pointer held?

本文关键字:c++ 指针 内存 地址      更新时间:2023-10-16

如果我执行以下操作:

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

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

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

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

从概念上讲,数据可以存储在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(即所有指针大小相同)
然后:

int i,*p=&i;int**p2p=&p
您正在声明3个变量:

i:nbsp nbsp;Addr=SP nbsp;size=2;contents=uninitialized
p: nbsp;Addr=SP-2;size=4;contents=SP(i的地址)
p2p:;Addr=SP-6;size=4;contents=SP-2(p的地址)

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

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

push ebp
mov ebp,esp

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

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

mov esp,ebp
pop ebp

在低级别,CPU只处理字面BYTE/WORD/DDWORD/等值和地址(这些值和地址相同,但使用不同)。

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

对于操作系统开发,如果你愿意,我很乐意更深入地解释我所知道的一切,但这肯定超出了SOF的范围,所以如果你感兴趣,我们需要找到另一个渠道。

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

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

我想[variables']地址在堆栈中是有组织的。

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

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

对于某些局部变量来说,这可能确实成立。然而,优化可以将变量移动到CPU寄存器中,也可以完全消除它们。

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

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

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

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

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

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

如果是,那么可以更改声明顺序的条件代码呢?

这就是编译器必须处理的问题。一旦程序被编译,所有的事情都得到了处理。

正如@严格解释的那样:

编译器引用变量(在您的情况下是i、p和p2p)相对于SP。换句话说,编译器决定偏移量每个变量的应来自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