计算机程序运行时会发生什么?

我知道一般的理论,但我不能适应细节。

我知道一个程序位于计算机的次级存储器中。一旦程序开始执行,它就会完全复制到 RAM 中。然后处理器一次检索一些指令(这取决于总线的大小) ,将它们放入寄存器并执行它们。

我还知道计算机程序使用两种内存: 堆栈和堆,它们也是计算机主内存的一部分。堆栈用于非动态内存,堆用于动态内存(例如,与 C + + 中的 new操作符相关的所有内容)

我不明白的是这两件事是怎么联系起来的。堆栈在什么时候用于执行指令?指令从 RAM,到栈,再到寄存器?

26918 次浏览

这确实取决于系统,但是使用 虚拟存储器的现代操作系统倾向于加载它们的进程映像并分配内存,比如:

+---------+
|  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.
+---------+

这是许多常见虚拟内存系统上的通用进程地址空间。“空洞”是总内存的大小,减去所有其他区域占用的空间; 这为堆的增长提供了大量空间。这也是“虚拟的”,意味着它通过转换表映射到您的 真的内存,并且实际上可能存储在实际内存中的任何位置。这样做是为了保护一个进程不访问另一个进程的内存,并使每个进程认为它正在一个完整的系统上运行。

请注意,例如,堆栈和堆的位置在某些系统上可能有不同的顺序(有关 Win32的更多细节,请参见下面的 比利 · 奥尼尔的回答)。

其他系统可以是 非常不同。例如,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 + + 中,使用 newdelete,在 C 中使用 malloc和朋友,等等)。

在 x86架构上,堆栈和堆都物理上驻留在系统内存(RAM)中,并通过虚拟内存分配映射到上面描述的进程地址空间。

登记册(仍然在 x86上)实际上位于处理器内部(而不是 RAM) ,由处理器从 TEXT 区域加载(也可以从内存或其他地方加载,这取决于实际执行的 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,则为 gcc -S foo.c) ,然后查看。这个程序很容易理解。可以看到堆栈用于函数局部变量,以及调用函数、存储它们的参数和返回值。这也是为什么当你做这样的事情:

f( g( h( i ) ) );

所有这些都会被依次调用。它实际上是构建一堆函数调用和它们的参数,执行它们,然后在它逐渐结束(或结束;)时关闭它们。然而,如上所述,堆栈(在 x86上)实际上位于您的进程内存空间(在虚拟内存中)中,因此可以直接操作它; 它在执行期间不是一个单独的步骤(或者至少与进程正交)。

仅供参考,上面是 C 调用惯例,也是 C + + 使用的。其他语言/系统可能会以不同的顺序将参数推送到堆栈上,有些语言/平台甚至不使用堆栈,而是以不同的方式进行操作。

还要注意,这些不是实际执行的 C 代码行。编译器已将它们转换为可执行文件中的机器语言指令。然后(通常)将它们从 TEXT 区域复制到 CPU 管道中,然后复制到 CPU 寄存器中,并从那里执行。 这是不正确的,参见下面 Ben Voigt 的修正

Sdaz 在很短的时间内就获得了大量的赞成票,但是遗憾的是,对于指令如何在 CPU 中移动的误解一直存在。

问题是:

指令从 RAM,到栈,再到寄存器?

斯达兹表示:

还要注意,这些不是实际执行的 C 代码行。编译器已将它们转换为可执行文件中的机器语言指令。然后(通常)将它们从 TEXT 区域复制到 CPU 管道中,然后复制到 CPU 寄存器中,并从那里执行。

但这是不对的。除了程序自修改的特殊情况,指令永远不会进入数据路径。它们不能从数据路径执行。

X86 CPU 寄存器为:

  • 普通登记册 EAX EBX ECX EDX

  • 分段寄存器 CS DS ES FS GS SS

  • 索引和指针 ESP

  • 指示器 EFLAGS

还有一些浮点寄存器和 SIMD 寄存器,但是出于本文讨论的目的,我们将这些寄存器分类为协处理器的一部分,而不是 CPU 的一部分。CPU 内部的内存管理单元也有自己的寄存器,我们将再次把它当作一个单独的处理单元。

这些寄存器都不用于可执行代码。EIP包含执行指令的地址,而不是指令本身。

指令在 CPU 中经过与数据完全不同的路径(哈佛体系结构)。当前所有的机器都是 CPU 内部的哈佛架构。现在大多数情况下,哈佛的建筑也是隐藏在缓存中的。X86(你常用的桌面电脑)冯·诺伊曼结构在主存中,这意味着数据和代码混合在 RAM 中。这不是重点,因为我们讨论的是中央处理器内部发生的事情。

计算机体系结构中传授的经典顺序是取-译-执行。内存控制器查找存储在地址 EIP的指令。指令的各个部分经过一些组合逻辑电路,为处理器中不同的多路复用器创建所有的控制信号。经过几个循环之后,算术逻辑单元会得到一个结果,这个结果会被记录到目的地。然后获取下一条指令。

在一个现代的处理器上,事情的运作有点不同。每个传入的指令都被翻译成一系列的微码指令。这支持流水线,因为第一个微指令使用的资源在以后不再需要,所以他们可以从下一个指令开始处理第一个微指令。

最重要的是,术语有点混淆,因为 登记册是一个电气工程术语,用于 D- 触发器的集合。而且指令(尤其是微指令)可以很好地暂时存储在这样的 D 触发器集合中。但是,当计算机科学家、软件工程师或一般的开发人员使用 登记册这个术语时,就不是这个意思了。它们指的是上面列出的数据路径寄存器,这些寄存器不用于传输代码。

其他 CPU 架构(如 ARM、 MIPS、 Alpha、 PowerPC)的数据路径寄存器的名称和数量各不相同,但是所有这些寄存器都执行指令,而不通过 ALU 传递。

进程执行时内存的确切布局完全取决于所使用的平台。考虑下面的测试程序:

#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 需要此模型的原因是 Sbrk系统调用。基本上,在 POSIX 机器上,为了获得更多的内存,一个进程只是告诉内核将“ hole”和“ stack”之间的分隔器移动到更远的“ hole”区域。没有办法将内存返回给操作系统,操作系统本身也不管理堆。您的 C 运行时库必须提供(通过 malloc)。

这对于 POSIX 二进制文件中实际使用的代码类型也有影响。POSIX 框(几乎普遍)使用 ELF 文件格式。在这种格式中,操作系统负责不同 ELF 文件中的库之间的通信。因此,所有的库都使用地址无关代码(也就是说,代码本身可以加载到不同的内存地址并仍然可以操作) ,所有库之间的调用都通过一个查找表传递,以找出跨库函数调用的控制需要跳转的位置。这会增加一些开销,如果其中一个库更改了查找表,就可以利用这一点。

Windows 的内存模型不同,因为它使用的代码类型不同。Windows 使用 PE 文件格式,这使得代码采用与位置相关的格式。也就是说,代码取决于代码在虚拟内存中的确切加载位置。PE 规范中有一个标志,它告诉操作系统在程序运行时库或可执行文件在内存中的确切位置。如果一个程序或库不能在它的首选地址加载,Windows 加载程序必须 重新定位库/可执行文件——基本上,它移动依赖于位置的代码来指向新的位置——这不需要查找表,也不能被利用,因为没有查找表可以覆盖。不幸的是,这需要在 Windows 加载程序中非常复杂的实现,并且如果需要重新定位映像,那么开销相当大。大型商业软件包经常修改它们的库,以便在不同的地址启动,以避免重定基; Windows 自己的库(例如 ntdll.dll、 kernel32.dll、 psapi.dll 等——默认情况下都有不同的启动地址)就是这样做的

在 Windows 系统中,虚拟内存通过对 VirtualAlloc的调用从系统中获取,然后通过 虚拟免费返回给系统(好吧,技术上讲,VirtualAlloc 分配给了 ntAllocateVirtualMemory,但这只是一个实现细节)(相比之下,在 POSIX,内存是无法回收的)。这个过程很慢(而且 IIRC 要求分配物理页面大小的块; 通常为4kb 或更大)。Windows 还提供了自己的堆函数(HeapAlloc、 HeapFree 等) ,作为一个名为 RtlHeap 的库的一部分,RtlHeap 是 Windows 自身的一部分,C 运行时(即 malloc和好友)通常在这个库上实现。

Windows 还有不少遗留的内存分配 API,这些 API 都是从它不得不处理旧的80386开始的,而且这些函数现在都是在 RtlHeap 之上构建的。有关在 Windows 中控制内存管理的各种 API 的详细信息,请参阅此 MSDN 文章: http://msdn.microsoft.com/en-us/library/ms810627

另请注意,这意味着在 Windows 上,一个进程(通常也是如此)有多个堆。(通常,每个共享库创建自己的堆。)

(大部分信息来自 Robert Seacord 的“安全 C 和 C + + 编码”)

堆栈

在 X86体系结构中,CPU 使用寄存器执行操作。堆栈只是出于方便的原因而使用。您可以在调用子例程或系统函数之前将寄存器的内容保存到堆栈中,然后将它们加载回来,以继续您离开时的操作。(您可以不使用堆栈手动操作它,但它是一个经常使用的函数,因此它具有 CPU 支持)。但是你可以做几乎任何事情没有堆栈在电脑上。

例如整数乘法:

MUL BX

将 AX 寄存器与 BX 寄存器相乘。(结果将是 DX 和 AX,包含较高位的 DX)。

基于堆栈的机器(如 JAVA VM)使用堆栈进行基本操作:

DMUL

这将从堆栈顶部弹出两个值并乘以 tem,然后将结果推回堆栈。堆栈对于这种机器是必不可少的。

一些更高级的编程语言(比如 C 和 Pascal)使用这种稍后的方法将参数传递给函数: 参数按从左到右的顺序被推到堆栈中,然后由函数体弹出,返回值被推回。(这是编译器制造商做出的选择,而且有点滥用 X86使用堆栈的方式)。

堆是仅存在于编译器领域中的另一个概念。它消除了处理变量后面的内存的痛苦,但它不是 CPU 或操作系统的函数,它只是管理内存块的一种选择,这个内存块是由操作系统提供的。如果你愿意的话,你可以多来几次。

访问系统资源

操作系统有一个公共接口,您可以通过这个接口访问它的功能。在 DOS 中,参数在 CPU 的寄存器中传递。Windows 使用堆栈为 OS 函数(WindowsAPI)传递参数。