当计算机存储变量时,当程序需要获取变量的值时,计算机如何知道在内存中查找该变量的值的位置?
当计算机存储变量时,当程序需要获取变量的值时,计算机如何知道在内存中查找该变量的值的位置?
Answers:
我建议您研究一下编译器构建的美好世界!答案是,这有点复杂。
为了给您一个直观的印象,请记住,变量名称纯粹是出于程序员的考虑。最后,计算机最终会将所有内容转换为地址。
局部变量(通常)存储在堆栈中:也就是说,它们是表示函数调用的数据结构的一部分。通过查看该函数,我们可以确定该函数将(可能)使用的变量的完整列表,因此编译器可以查看该函数需要多少个变量以及每个变量占用多少空间。
有一点魔力叫做堆栈指针,这是一个始终存储当前堆栈开始地址的寄存器。
每个变量都有一个“堆栈偏移量”,即它在堆栈中的存储位置。然后,当程序需要访问变量时x
,编译器将替换x
为STACK_POINTER + x_offset
,以获取存储在内存中的实际物理位置。
需要注意的是,这就是为什么你会得到一个指针回来时,你使用malloc
或new
使用C或C ++。您无法确定堆分配值在内存中的确切位置,因此您必须保留指向它的指针。该指针将在堆栈上,但将指向堆。
当计算机存储变量时,当程序需要获取变量的值时,计算机如何知道在内存中查找该变量的值的位置?
该程序告诉它。计算机本身并不具有“变量”的概念-这完全是一种高级语言!
这是一个C程序:
int main(void)
{
int a = 1;
return a + 3;
}
这是它编译成的汇编代码:(以开头的注释;
)
main:
; {
pushq %rbp
movq %rsp, %rbp
; int a = 1
movl $1, -4(%rbp)
; return a + 3
movl -4(%rbp), %eax
addl $3, %eax
; }
popq %rbp
ret
对于“ int a = 1;” CPU看到指令“将值1存储在地址(寄存器rbp的值减去4)”。它知道值1的存储位置,因为程序会告诉它。
同样,下一条指令说“将地址的值(寄存器rbp的值,减4)加载到寄存器eax中”。计算机不需要知道诸如变量之类的东西。
%rsp
是CPU的堆栈指针。%rbp
是一个寄存器,该寄存器引用当前函数使用的堆栈位。使用两个寄存器可简化调试。
确切的方法取决于您正在谈论的内容以及要深入的范围。例如,在硬盘驱动器上存储文件与在内存中存储文件或在数据库中存储文件不同。虽然概念相似。以及您如何在编程级别执行此操作与计算机如何在I / O级别执行此操作不同。
大多数系统使用某种目录/索引/注册表机制来允许计算机查找和访问数据。该索引/目录将包含一个或多个键,以及数据实际位于的地址(无论是硬盘驱动器,RAM,数据库等)。
计算机程序示例
计算机程序可以通过多种方式访问内存。通常,操作系统为程序提供一个地址空间,并且程序可以使用该地址空间执行所需的操作。它可以直接写入其存储空间内的任何地址,并且可以跟踪其所需方式。这有时会因编程语言和操作系统,甚至根据程序员的首选技术而有所不同。
正如在其他一些答案中提到的那样,所使用的确切编码或编程有所不同,但通常在幕后使用诸如堆栈之类的东西。它有一个寄存器,用于存储当前堆栈开始处的内存位置,然后提供一种方法来知道函数或变量在该堆栈中的位置。
在许多高级编程语言中,它可以为您解决所有这些问题。您要做的就是声明一个变量,然后在该变量中存储一些内容,然后为您在幕后创建必要的堆栈和数组。
但是考虑到编程的通用性,实际上并没有一个答案,因为程序员可以随时选择直接写入其分配空间内的任何地址(假设他正在使用允许该地址的编程语言)。然后,他可以将其位置存储在数组中,甚至可以将其硬编码到程序中(即,变量“ alpha”始终存储在堆栈的开头,或者始终存储在分配的内存的前32位中)。
摘要
因此,基本上,幕后必须有某种机制可以告诉计算机数据的存储位置。最受欢迎的方法之一是某种包含关键字和内存地址的索引/目录。这是通过各种方式实现的,通常是由用户封装的(有时甚至是由程序员封装的)。
它知道是因为模板和格式。
程序/功能/计算机实际上不知道什么在哪里。它只是希望某些东西在某个地方。让我们举个例子。
class simpleClass{
public:
int varA=58;
int varB=73;
simpleClass* nextObject=NULL;
};
我们的新类'simpleClass'包含3个重要变量-两个整数,可以在需要时包含一些数据,以及一个指向另一个'simpleClass对象'的指针。为了简单起见,假设我们在32位计算机上。'gcc'或另一个'C'编译器将为我们创建一个模板,以分配一些数据。
简单类型
首先,当使用简单类型的关键字(如“ int”)时,编译器在可执行文件的“ .data”或“ .bss”部分中进行注释,以便在操作系统执行时,数据为该程序可用。“ int”关键字将分配4个字节(32位),而“ long int”将分配8个字节(64位)。
有时,以逐个单元的方式,变量可能会在应该将其加载到内存的指令之后出现,因此在伪汇编中看起来像这样:
...
clear register EAX
clear register EBX
load the immediate (next) value into EAX
5
copy the value in register EAX to register EBX
...
这将在EAX和EBX中以值“ 5”结尾。
程序执行时,除立即数 “ 5”外,所有指令均被执行,因为立即加载引用了该指令并使CPU跳过它。
这种方法的缺点是它仅对常量真正有用,因为将数组/缓冲区/字符串保留在代码中间是不切实际的。因此,通常,大多数变量都保存在程序头文件中。
如果需要访问这些动态变量之一,则可以将立即值视为指针:
...
clear register EAX
clear register EBX
load the immediate value into EAX
0x0AF2CE66 (Let's say this is the address of a cell containing '5')
load the value pointed to by EAX into EBX
...
这将以寄存器EAX中的值“ 0x0AF2CE66”和寄存器EBX中的值“ 5”结束。也可以将寄存器中的值一起添加,因此我们可以使用此方法查找数组或字符串的元素。
另一个重要的一点是,在以类似方式使用地址时,人们能够存储值,以便以后可以在这些单元格中引用值。
复杂类型
如果我们创建此类的两个对象:
simpleClass newObjA;
simpleClass newObjB;
然后我们可以将指向第二个对象的指针分配给第一个对象可用的字段:
newObjA.nextObject=&newObjB;
现在程序可以期望在第一个对象的指针字段中找到第二个对象的地址。在内存中,这看起来像:
newObjA: 58
73
&newObjB
...
newObjB: 58
73
NULL
这里要注意的一个非常重要的事实是,“ newObjA”和“ newObjB”在编译时没有名称。它们只是我们期望获得一些数据的地方。因此,如果将2个单元格添加到&newObjA,则将找到充当“ nextObject”的单元格。因此,如果我们知道'newObjA'的地址以及'nextObject'单元相对于其的位置,那么我们就可以知道'newObjB'的地址:
...
load the immediate value into EAX
&newObjA
add the immediate value to EAX
2
load the value in EAX into EBX
这将以“ EAX”中的“ 2 +&newObjA”和“ EBX”中的“&newObjB”结尾。
模板/格式
当编译器编译类定义时,它实际上是在编译一种格式,一种写入格式以及从一种格式读取的方法。
上面给出的示例是带有两个“ int”变量的单链接列表的模板。这些类型的构造以及二进制和n元树对于动态内存分配非常重要。n元树的实际应用将是由指向文件的目录组成的文件系统,目录或驱动程序/操作系统识别的其他实例。
为了访问所有元素,请考虑一个蠕虫在结构中上下移动。这样,程序/功能/计算机什么都不知道,它只是执行指令以移动数据。