运行计算机程序会怎样?


180

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

我知道程序驻留在计算机的辅助存储器中。一旦程序开始执行,它将被完全复制到RAM中。然后,处理器一次检索几条指令(取决于总线的大小),将它们放入寄存器中并执行它们。

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

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


43
+1提出基本问题!
mkelley33 2011年

21
嗯...你知道的,他们写有关这本书的书。您是否真的想在SO的帮助下研究OS体系结构的这一部分?
安德烈

1
我根据问题的内存相关性质以及对C ++的引用添加了一些标记,尽管我认为一个不错的答案也可能来自于Java或C#的知识者!)
mkelley33 2011年

14
受到赞誉和喜爱。我一直都很害怕问...
Maxpm

2
“将它们放入寄存器”一词不太正确。在大多数处理器上,寄存器用于保存中间值,而不是可执行代码。

Answers:


161

这实际上取决于系统,但是具有虚拟内存的现代OS 倾向于加载其过程映像并分配如下所示的内存:

+---------+
|  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的更多详细信息,请参见下面的Billy O'Neal的回答)。

其他系统可以非常不同。例如,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保留在只读内存中的系统(最初由BSS设置)程序员)等。但这是一般要点。


你说:

我也知道计算机程序使用两种内存:堆栈和堆,它们也是计算机主内存的一部分。

“堆栈”和“堆”只是抽象概念,而不是(必要时)物理上不同的内存“种类”。

堆栈仅仅是后进先出的数据结构。在x86体系结构中,实际上可以使用末尾的偏移量来随机寻址,但是最常见的功能是PUSH和POP,分别用于添加和删除项。它通常用于函数局部变量(所谓的“自动存储”),函数参数,返回地址等。

一个“堆”仅仅是一个内存块,也可以根据需要分配一个昵称,并随机寻址(意思,你可以直接访问任何位置)。它通常用于在运行时分配的数据结构(在C ++中,使用newdelete,以及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上)实际上位于您的进程内存空间(在虚拟内存中),因此可以直接对其进行操作。这不是执行过程中的单独步骤(或至少与流程正交)。

仅供参考,以上是C调用约定,也由C ++使用。其他语言/系统可能以不同的顺序将参数推入堆栈,而某些语言/平台甚至不使用堆栈,而是以不同的方式进行处理。

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


4
抱歉,但是推荐好的书本会是一个更好的答案,IMO
Andrey

13
是的,“ RTFM”总是更好。
Sdaz MacSkibbons 2011年

56
@Andrey:也许您应该将该注释更改为“也,您可能希望阅读您的好书推荐 ”,我知道这类问题值得进一步调查,但是每当您必须以“对不起”开始注释时。 ……”,也许您应该真正考虑标记该帖子以引起主持人的注意,或者至少提供一个解释,说明您的观点为何仍然对任何人都重要。
mkelley33'3

2
极好的答案。它肯定为我清除了一些东西!
2011年

2
@Mikael:根据实现的不同,您可能具有强制缓存,在这种情况下,无论何时从内存中读取数据,读取整个缓存行并填充缓存。或者,可能会向缓存管理器提示仅需要一次数据,因此将其复制到缓存中无济于事。这是供阅读。对于写操作,有回写和直写式高速缓存,这会影响DMA控制器何时可以读取数据,然后还有一大堆高速缓存一致性协议,用于处理多个具有各自高速缓存的处理器。这确实值得自己的问题:
本·福格特

61

Sdaz在很短的时间内就获得了惊人的支持,但令人遗憾的是,人们对指令如何在CPU中移动的误解一直存在。

问的问题:

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

斯达兹说:

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

但这是错误的。除了自修改代码的特殊情况外,指令从不进入数据路径。而且它们不是,不能从数据路径执行。

x86的CPU寄存器是:

  • 通用寄存器EAX EBX ECX EDX

  • 段寄存器CS DS ES FS GS SS

  • 索引和指针ESI EDI EBP EIP ESP

  • 指标EFLAGS

还有一些浮点和SIMD寄存器,但是出于讨论的目的,我们将它们归类为协处理器而不是CPU。CPU内部的内存管理单元也有自己的一些寄存器,我们将再次将其视为独立的处理单元。

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

指令在CPU中与数据的路径完全不同(哈佛体系结构)。当前所有的机器都是CPU内部的哈佛体系结构。这些天大部分时间也是缓存中的哈佛体系结构。x86(您常用的台式机)是Von Neumann架构的主内存,这意味着数据和代码混合在RAM中。这不是重点,因为我们正在谈论CPU内部发生的情况。

计算机体系结构中教授的经典序列是fetch-decode-execute。存储器控制器查找存储在该地址的指令EIP。指令的位经过一些组合逻辑,以创建处理器中不同多路复用器的所有控制信号。并在某些周期后,算术逻辑单元得出结果,并将结果传送到目的地。然后提取下一条指令。

在现代处理器上,工作原理有所不同。每个传入的指令都被翻译成一系列的微码指令。之所以启用流水线,是因为以后不需要第一条微指令使用的资源,因此它们可以从下一条指令开始处理第一条微指令。

最重要的是,术语有点混乱,因为寄存器是D触发器集合的电气工程术语。指令(特别是微指令)可能很好地临时存储在此类D触发器中。但这不是计算机科学家或软件工程师或常规开发人员使用术语注册的意思。它们表示上面列出的数据路径寄存器,并且这些寄存器不用于传输代码。

数据路径寄存器的名称和数量因其他CPU体系结构(例如ARM,MIPS,Alpha,PowerPC)而异,但是它们全部执行指令而不会通过ALU。


感谢您的澄清。我犹豫不决地补充一点,因为我并不十分熟悉它,但是是应别人的要求做了的。
Sdaz MacSkibbons 2011年

s / ARM / RAM /中的“意味着数据和代码在ARM中混合在一起”。对?
Bjarke Freund-Hansen'3

@bjarkef:是第一次,但是不是第二次。我会解决的。
Ben Voigt

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盒子上,它会说:

堆栈在堆之上

@Sdaz MacSkibbons在这里很好地解释了UNIX内存模型,因此在此我不再重申。但这不是唯一的内存模型。POSIX需要此模型的原因是sbrk系统调用。基本上,在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相比,在该内存中内存不能被回收)。此过程很慢(并且IIRC要求您分配物理页面大小的块;通常为4kb或更大)。Windows还提供了自己的堆函数(HeapAlloc,HeapFree等),作为称为RtlHeap的库的一部分,该库包含在Windows本身的一部分中,通常在该库上实现C运行时(即malloc和朋友)。

从不得不处理旧的80386以来,Windows还具有许多旧式内存分配API,并且这些功能现在基于RtlHeap构建。有关在Windows中控制内存管理的各种API的更多信息,请参见以下MSDN文章:http : //msdn.microsoft.com/zh-cn/library/ms810627

还要注意,这意味着在Windows上,一个进程(通常确实)有多个堆。(通常,每个共享库都会创建自己的堆。)

(大多数信息来自Robert Seacord的“ C和C ++中的安全编码”)


很好的信息,谢谢!希望“ user487117”最终真正回来。:-)
Sdaz MacSkibbons 2011年

5

堆栈

在X86体系结构中,CPU使用寄存器执行操作。仅出于方便起见使用堆栈。您可以在调用子例程或系统函数之前将寄存器的内容保存到堆栈中,然后将其加载回以继续您所处的位置。(您可以手动使用它而无需堆栈,但是它是一个常用功能,因此具有CPU支持)。但是,无需PC上的堆栈,您几乎可以做任何事情。

例如整数乘法:

MUL BX

将AX寄存器与BX寄存器相乘。(结果将出现在DX和AX中,其中DX包含较高的位)。

基于堆栈的计算机(例如JAVA VM)将堆栈用于其基本操作。上面的乘法:

DMUL

这将从堆栈的顶部弹出两个值并乘以tem,然后将结果推回堆栈。堆栈对于此类机器至关重要。

一些高级编程语言(例如C和Pascal)使用此后一种方法将参数传递给函数:参数以从左到右的顺序推入堆栈,并由函数体弹出,然后将返回值推回。(这是编译器制造商的选择,并且在某种程度上滥用X86使用堆栈的方式)。

堆是仅存在于编译器领域中的另一个概念。它消除了处理变量后面的内存的痛苦,但这不是CPU或OS的功能,它只是内部存储由OS给出的内存块的一种选择。您可以根据需要进行很多操作。

访问系统资源

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.