我知道一般的理论,但我不能融入细节。
我知道程序驻留在计算机的二级内存中。一旦程序开始执行,它就被完全复制到RAM中。然后处理器一次检索几条指令(这取决于总线的大小),将它们放入寄存器并执行它们。
我还知道,计算机程序使用两种内存:堆栈和堆,它们也是计算机主内存的一部分。堆栈用于非动态内存,堆用于动态内存(例如,与new运算符(C++)
我不明白的是这两件事是如何联系在一起的。堆栈在什么时候被用来执行指令?指令从RAM到堆栈还是寄存器?
发布于 2017-12-27 07:39:53
它确实取决于系统,但是现代操作系统虚拟存储器倾向于加载它们的进程映像并分配内存,如下所示:
+---------+
| stack | function-local variables, return addresses, return values, etc.
| | often grows downward, commonly accessed via "push" and "pop" (but can be
| | accessed randomly, as well; disassemble a program to see)
+---------+
| shared | mapped shared libraries (C libraries, math libs, etc.)
| libs |
+---------+
| hole | unused memory allocated between the heap and stack "chunks", spans the
| | difference between your max and min memory, minus the other totals
+---------+
| heap | dynamic, random-access storage, allocated with 'malloc' and the like.
+---------+
| bss | Uninitialized global variables; must be in read-write memory area
+---------+
| data | data segment, for globals and static variables that are initialized
| | (can further be split up into read-only and read-write areas, with
| | read-only areas being stored elsewhere in ROM on some systems)
+---------+
| text | program code, this is the actual executable code that is running.
+---------+这是许多常见的虚拟内存系统上的一般进程地址空间.。“洞”是内存总量的大小,减去所有其他区域占用的空间;这为堆的成长提供了很大的空间。这也是“虚拟”,意思是它映射到实际通过转换表存储内存,并且可以实际存储在实际内存中的任何位置。这样做是为了保护一个进程不访问另一个进程的内存,并使每个进程都认为它运行在一个完整的系统上。
注意,在某些系统上,例如堆栈和堆的位置可能不同(参见比利·奥尼尔的回答有关Win 32的更多详细信息,请参见下面。
其他系统非常不一样。例如,DoS跑进来了。实模,以及在运行程序时其内存分配情况有很大不同:
+-----------+ top of memory
| extended | above the high memory area, and up to your total memory; needed drivers to
| | be able to access it.
+-----------+ 0x110000
| high | just over 1MB->1MB+64KB, used by 286s and above.
+-----------+ 0x100000
| upper | upper memory area, from 640kb->1MB, had mapped memory for video devices, the
| | DOS "transient" area, etc. some was often free, and could be used for drivers
+-----------+ 0xA0000
| USER PROC | user process address space, from the end of DOS up to 640KB
+-----------+
|command.com| DOS command interpreter
+-----------+
| DOS | DOS permanent area, kept as small as possible, provided routines for display,
| kernel | *basic* hardware access, etc.
+-----------+ 0x600
| BIOS data | BIOS data area, contained simple hardware descriptions, etc.
+-----------+ 0x400
| interrupt | the interrupt vector table, starting from 0 and going to 1k, contained
| vector | the addresses of routines called when interrupts occurred. e.g.
| table | interrupt 0x21 checked the address at 0x21*4 and far-jumped to that
| | location to service the interrupt.
+-----------+ 0x0您可以看到DOS允许直接访问操作系统内存,没有任何保护,这意味着用户空间程序通常可以直接访问或覆盖任何他们喜欢的东西。
然而,在进程地址空间中,程序看起来很相似,只是它们被描述为代码段、数据段、堆、堆栈段等,并且映射方式略有不同。但是大部分的一般区域仍然在那里。
当将程序和必要的共享库加载到内存中,并将程序的各个部分分配到正确的区域时,操作系统就开始执行您的进程,不管它的主要方法在哪里,您的程序从那里接管,在需要时根据需要进行系统调用。
不同的系统(嵌入式系统)可能有非常不同的体系结构,如无堆栈系统、哈佛体系结构系统(代码和数据保存在单独的物理内存中)、实际上将BSS保存在只读内存中的系统(最初由程序员设置)等等。
你说过:
我还知道,计算机程序使用两种内存:堆栈和堆,它们也是计算机主内存的一部分。
“堆栈”和“堆”只是抽象的概念,而不是(必然)物理上不同的“类型”内存。
阿堆叠只是一种先入先出的数据结构。在x86体系结构中,实际上可以通过从末尾使用偏移量来随机处理它,但最常见的函数是Push和POP,分别从其中添加和删除项。它通常用于函数局部变量(所谓的“自动存储”)、函数参数、返回地址等。
阿“堆”只是一个可以按需分配的内存块的昵称,地址是随机的(意思是,您可以直接访问其中的任何位置)。它通常用于您在运行时分配的数据结构(在C++中,使用new and delete, and malloc以及C等的朋友)。
在x86体系结构上,堆栈和堆都物理地驻留在系统内存(RAM)中,并通过虚拟内存分配映射到进程地址空间,如上所述。
大寄存器(仍然在x86上),物理地驻留在处理器内(而不是RAM),由处理器从文本区域加载(也可以根据实际执行的CPU指令从内存中的其他地方或其他地方加载)。它们基本上是非常小的,非常快的片上存储器位置,用于许多不同的用途。
寄存器布局高度依赖于体系结构(实际上,寄存器、指令集和内存布局/设计正是“体系结构”的含义),因此我不打算对其进行扩展,但建议您选修汇编语言课程以更好地理解它们。
你的问题:
堆栈在什么时候被用来执行指令?指令从RAM到堆栈还是寄存器?
堆栈(在有和使用它们的系统/语言中)通常如下所用:
int mul( int x, int y ) {
return x * y; // this stores the result of MULtiplying the two variables
// from the stack into the return value address previously
// allocated, then issues a RET, which resets the stack frame
// based on the arg list, and returns to the address set by
// the CALLer.
}
int main() {
int x = 2, y = 3; // these variables are stored on the stack
mul( x, y ); // this pushes y onto the stack, then x, then a return address,
// allocates space on the stack for a return value,
// then issues an assembly CALL instruction.
}编写这样一个简单的程序,然后将其编译成程序集(gcc -S foo.c如果您可以访问gcc),请看一看。这个装配很容易跟踪。您可以看到堆栈用于函数局部变量,用于调用函数、存储它们的参数和返回值。这也是为什么当你做这样的事情时:
f( g( h( i ) ) ); 所有这些都会依次被呼叫。它实际上是构建一个函数调用和它们的参数的堆栈,执行它们,然后在它结束(或向上)时弹出它们。但是,如上所述,堆栈(在x86上)实际上驻留在进程内存空间中(在虚拟内存中),因此可以直接操作它;在执行过程中它不是一个单独的步骤(或者至少与进程正交)。
FYI,以上是C calling convention,,也被C++使用。其他语言/系统可能会以不同的顺序将参数推到堆栈上,而有些语言/平台甚至不使用堆栈,并以不同的方式进行操作。
另外,请注意,这些不是执行的C代码的实际行。编译器已经将它们转换为可执行文件中的机器语言指令。然后(通常)将它们从文本区域复制到CPU管道,然后复制到CPU寄存器中,然后从那里执行。[这是不正确的。见本·沃格特校正以下是]
发布于 2017-12-27 08:47:42
Sdaz在很短的时间内获得了大量的支持,但不幸的是,它正在使指令如何在CPU中移动的错误观念永久化。
问题是:
指令从RAM到堆栈还是寄存器?
Sdaz说:
另外,请注意,这些不是执行的C代码的实际行。编译器已经将它们转换为可执行文件中的机器语言指令。然后(通常)将它们从文本区域复制到CPU管道,然后复制到CPU寄存器中,然后从那里执行。
但这是错误的。除了特殊情况下的自修改代码,指令永远不会进入数据路径.。它们不是,也不能是从数据路径执行的。
x86 CPU寄存器是:
还有一些浮点寄存器和SIMD寄存器,但为了本讨论的目的,我们将这些寄存器归类为协处理器的一部分,而不是CPU。CPU内部的内存管理单元也有自己的一些寄存器,我们将再次将其作为一个单独的处理单元来处理。
这些寄存器都不用于可执行代码。EIP包含正在执行的指令的地址,而不是指令本身。
指令在CPU中通过与数据完全不同的路径(哈佛架构)。所有当前的机器都是CPU内部的哈佛架构。如今,大多数时候都是缓存中的哈佛架构。x86(您的普通桌面机器)是主内存中的VonNeumann体系结构,这意味着数据和代码混合在RAM中。这与问题无关,因为我们讨论的是CPU内部发生的事情。
计算机体系结构中的经典顺序是提取-解码-执行。存储器控制器查找存储在地址上的指令。EIP.指令的位经过一些组合逻辑,为处理器中不同的多路复用器产生所有的控制信号。经过一些循环后,算术逻辑单元到达一个结果,该结果被锁定到目的地。然后获取下一个指令。
在现代处理器上,事情的工作方式有点不同。每个传入指令被转换成一系列的微码指令。这就启用了流水线,因为以后不需要第一个微指令所使用的资源,所以它们可以从下一个指令开始处理第一个微指令。
更重要的是,术语有点混淆,因为寄存器是一个D触发器集合的电子工程术语。并且指令(或特别是微指令)可能暂时存储在这种D触发器的集合中。但是,当计算机科学家或软件工程师或普通开发人员使用术语“注册”时,这不是什么意思。它们是指上面列出的数据路径寄存器,这些寄存器不用于传输代码。
对于其他CPU架构,如ARM、MIPS、Alpha、PowerPC,数据路径寄存器的名称和数量各不相同,但它们都执行指令而不通过ALU传递它们。
发布于 2017-12-27 09:03:17
进程正在执行时,内存的精确布局完全取决于您使用的平台。考虑以下测试程序:
#include <stdlib.h>
#include <stdio.h>
int main()
{
int stackValue = 0;
int *addressOnStack = &stackValue;
int *addressOnHeap = malloc(sizeof(int));
if (addressOnStack > addressOnHeap)
{
puts("The stack is above the heap.");
}
else
{
puts("The heap is above the stack.");
}
}在Windows NT(及其子程序)上,该程序通常会生成:
堆在堆栈的上方。
在POSIX框中,它会说:
堆栈位于堆的上方。
UNIX内存模型在这里由@Sdaz MacSkibbons很好地解释,所以我不会在这里重申这一点。但这并不是唯一的记忆模型。POSIX需要此模型的原因是斯布鲁克系统呼叫。基本上,在POSIX盒上,为了获得更多的内存,进程只是告诉内核将“孔”和“堆”之间的分隔器进一步移动到“孔”区域。无法将内存返回到操作系统,而且操作系统本身也不管理堆。您的C运行时库必须提供(通过malloc)。
这对POSIX二进制文件中实际使用的代码类型也有影响。POSIX框(几乎普遍使用)使用ELF文件格式。在这种格式中,操作系统负责不同ELF文件中的库之间的通信。因此,所有库都使用与位置无关的代码(也就是说,代码本身可以加载到不同的内存地址并仍然操作),库之间的所有调用都通过一个查找表传递,以查找控件需要跳转到跨库函数调用的位置。这增加了一些开销,如果其中一个库更改了查找表,则可以利用它。
Windows的内存模型是不同的,因为它使用的代码类型不同。Windows使用PE文件格式,使代码保持位置依赖的格式.。也就是说,代码取决于代码在虚拟内存中的确切加载位置。PE规范中有一个标志,它告诉操作系统,当程序运行时,程序库或可执行文件在内存中的确切位置。如果程序或库无法加载到其首选地址,Windows加载程序必须重基库/可执行文件--基本上,它将与位置相关的代码移动到新的位置--它不需要查找表,也不能被利用,因为没有要覆盖的查找表。不幸的是,这需要Windows加载程序中非常复杂的实现,而且如果需要重基映像,则需要相当长的启动时间开销。大型的商业软件包通常会修改它们的库,使其有目的地从不同的地址开始,以避免重基;Windows本身使用自己的库(例如ntdll.dll、kernel32.dll、psapi.dll等)来实现这一点--在默认情况下,它们都有不同的起始地址。
在Windows上,通过调用Virtualalloc,并且它将通过以下方式返回到系统VirtualFree(好的,从技术上讲,Virtualalloc把内存转移到NtAllocateVirtualMemory,但这是一个实现细节)(与POSIX相比,POSIX无法回收内存)。这个过程很慢(IIRC,要求您以物理页面大小的块分配,通常是4kb或更多)。Windows还将自己的堆函数(Heapalloc、HeapFree等)作为称为RtlHeap的库的一部分提供,RtlHeap作为Windows本身的一部分包含在其中,C运行时(即,malloc和朋友)通常都是实现的。
Windows还拥有相当多的遗留内存分配API,这些API在处理旧的80386 s时就已经存在了,而这些功能现在都建立在RtlHeap之上。有关在Windows中控制内存管理的各种API的更多信息,请参见本MSDN文章:http://msdn.microsoft.com/en-us/Library/ms 810627...
还请注意,这意味着在Windows上,一个进程(通常是一个进程)有多个堆。(通常,每个共享库都会创建自己的堆。)
(大部分信息来自Robert Seacord的“C和C++中的安全编码”)
https://stackoverflow.com/questions/-100001310
复制相似问题