计算机程序运行时会发生什么?
我知道一般的理论,但我不能适应细节。
我知道一个程序驻留在电脑的辅助存储器中。 一旦程序开始执行,它就完全复制到RAM中。 然后,处理器一次性检索几条指令(取决于总线的大小),将它们放入寄存器并执行。
我也知道一个计算机程序使用两种内存:堆栈和堆,它们也是计算机主内存的一部分。 该堆栈用于非dynamic内存,dynamic内存堆(例如,与C ++中的new
运算符相关的所有内容)
我不明白的是这两件事情是如何连接的。 用于执行指令的堆栈在什么位置? 指令从RAM到堆栈,到寄存器?
这真的取决于系统,但是具有虚拟内存的现代操作系统倾向于加载其过程映像并分配内存,如下所示:
+---------+ | 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. eg | table | interrupt 0x21 checked the address at 0x21*4 and far-jumped to that | | location to service the interrupt. +-----------+ 0x0
您可以看到,DOS允许直接访问操作系统内存,没有任何保护,这意味着用户空间程序通常可以直接访问或覆盖任何他们喜欢的东西。
然而,在进程地址空间中,程序往往看起来很相似,只是它们被描述为代码段,数据段,堆栈,堆栈段等等,而且它们映射的方式稍有不同。 但大部分地区仍然在那里。
在将程序和必要的共享库加载到内存中,并将程序的各个部分分配到正确的区域时,操作系统开始执行您的进程,无论它的主要方法在哪里,并且您的程序从那里接pipe,根据需要进行系统调用它需要他们。
不同的系统(embedded的,不pipe)可能具有非常不同的体系结构,诸如无堆栈系统,哈佛体系结构系统(代码和数据保存在单独的物理存储器中),实际上将BSS保持在只读存储器的系统程序员)等,但这是一般的要点。
你说:
我也知道一个计算机程序使用两种内存:堆栈和堆,它们也是计算机主内存的一部分。
“堆栈”和“堆”只是抽象的概念,而不是(必然)物理上不同的“种类”的记忆。
一个堆栈只是一个后进先出的数据结构。 在x86体系结构中,实际上可以通过使用从最后一个偏移量来随机地解决,但最常见的function是PUSH和POP来分别添加和删除项目。 它通常用于函数局部variables(所谓的“自动存储”),函数参数,返回地址等(下面更多)
一个“堆”只是一个可以按需分配的内存块的昵称,并且是随机的(也就是说,你可以直接访问它的任何位置)。 它通常用于在运行时分配的数据结构(在C ++中,使用new
和delete
以及malloc
和C中的朋友等)。
x86体系结构中的堆栈和堆都物理地驻留在系统内存(RAM)中,并通过虚拟内存分配映射到进程地址空间,如上所述。
寄存器 (仍然在x86上),物理上驻留在处理器内部(与RAM相对),并由处理器从TEXT区域加载(也可以从内存中的其他地方或其他地方加载,具体取决于CPU指令实际执行)。 它们本质上是非常小的,非常快的片上存储器位置,用于许多不同的目的。
寄存器布局高度依赖于架构(事实上,寄存器,指令集和存储器布局/devise,正是“架构”所指的),所以我不会详细介绍它,但build议您采取汇编语言课程更好地理解他们。
你的问题:
用于执行指令的堆栈在什么位置? 指令从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),并查看一下。 大会很容易遵循。 你可以看到栈被用于函数局部variables,并且用于调用函数,存储它们的参数和返回值。 这也是为什么当你做这样的事情:
f( g( h( i ) ) );
所有这些被依次调用。 它实际上build立了一堆函数调用和它们的参数,执行它们,然后在它们向下(或向上)时popup它们。 但是,如上所述,堆栈(在x86上)实际上驻留在进程内存空间(虚拟内存)中,因此可以直接操作它。 在执行过程中不是一个单独的步骤(或者至less与过程正交)。
仅供参考,以上是C调用约定 ,也被C ++使用。 其他语言/系统可能会以不同的顺序将参数推送到堆栈上,有些语言/平台甚至不使用堆栈,而是以不同的方式进行处理。
还要注意,这些不是C代码执行的实际行。 编译器已经将它们转换为可执行文件中的机器语言指令。 然后(通常)将它们从TEXT区域复制到CPUpipe道中,然后复制到CPU寄存器中,并从那里执行。 [这是不正确的。 见下面的本Voigt更正 。]
Sdaz在很短的时间内得到了大量的upvotes,但令人遗憾的是,对于如何通过CPU的指令有一种误解。
问题问:
指令从RAM到堆栈,到寄存器?
斯达兹说:
还要注意,这些不是C代码执行的实际行。 编译器已经将它们转换为可执行文件中的机器语言指令。 然后(通常)将它们从TEXT区域复制到CPUpipe道中,然后复制到CPU寄存器中,并从那里执行。
但这是错误的。 除了自修改代码的特殊情况外,指令不会进入数据path。 他们不是,不能从数据path执行。
x86 CPU寄存器是:
-
一般注册EAX EBX ECX EDX
-
段寄存器CS DS ES FS GS SS
-
索引和指针ESI EDI EBP EIP ESP
-
指标EFLAGS
还有一些浮点和SIMD寄存器,但为了讨论的目的,我们将这些寄存器分类为协处理器的一部分,而不是CPU。 CPU内部的内存pipe理单元也有一些自己的寄存器,我们再次把它作为一个独立的处理单元。
这些寄存器都没有用于执行代码。 EIP
包含执行指令的地址,而不是指令本身。
指令从数据(哈佛架构)经过一个完全不同的path。 目前所有的机器都是CPU内部的哈佛架构。 大多数这些日子也是哈佛架构中的高速caching。 x86(您的普通台式机)是主内存中的冯诺依曼体系结构,这意味着数据和代码混合在RAM中。 这是不言而喻的,因为我们正在讨论CPU内部发生的事情。
计算机体系结构中教授的经典序列是提取 – 解码 – 执行。 存储器控制器查找存储在地址EIP
处的指令。 指令的这些位通过一些组合逻辑来为处理器中不同的多路复用器创build所有的控制信号。 经过一些周期之后,算术逻辑单元到达一个结果,该结果被logging到目标中。 然后获取下一条指令。
在现代处理器上,情况有点不同。 每个传入的指令都被翻译成一系列的微码指令。 这使得stream水线化成为可能,因为第一条微指令所使用的资源不再需要,所以它们可以从下一条指令开始工作在第一条微指令上。
最重要的是,术语有些混淆,因为寄存器是一个D触发器集合的电子工程术语。 并且指令(或特别是微指令)可能暂时存储在这种D触发器的集合中。 但是,当计算机科学家或软件工程师或普通开发人员使用术语“ 注册”时,这不是什么意思。 它们是指上面列出的数据path寄存器,这些寄存器不用于传输代码。
其他CPU架构(如ARM,MIPS,Alpha,PowerPC)中的数据通路寄存器的名称和数量也不尽相同,但是它们都是在不通过ALU的情况下执行指令的。
内存在进程执行时的确切布局完全取决于您使用的平台。 考虑下面的testing程序:
#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框中,为了获得更多的内存,一个进程只是告诉内核把“洞”和“堆”之间的分隔线进一步移动到“洞”区域。 没有办法将内存返回到操作系统,操作系统本身不pipe理你的堆。 您的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
和friends)通常被实现。
Windows在处理旧的80386的时候也有不less遗留的内存分配API,现在这些function是build立在RtlHeap之上的。 有关在Windows中控制内存pipe理的各种API的更多信息,请参阅此MSDN文章: http : //msdn.microsoft.com/zh-cn/library/ms810627 。
还要注意,这意味着在Windows上一个进程(通常是)有多个堆。 (通常,每个共享库创build它自己的堆。)
(大部分信息来自Robert Seacord的“C和C ++安全编码”)
堆栈
在X86架构中,CPU执行寄存器操作。 堆栈仅用于方便的原因。 在调用子程序或系统函数之前,可以将寄存器的内容保存为堆栈,然后将它们加载回来,以便在离开的地方继续操作。 (你可以在没有堆栈的情况下手动执行它,但它是一个经常使用的函数,所以它支持CPU)。 但是,如果没有PC的堆栈,你可以做任何事情。
例如一个整数乘法:
MUL BX
将AX寄存器与BX寄存器相乘。 (结果将在DX和AX,DX包含更高的位)。
基于堆栈的机器(如JAVA VM)使用堆栈进行基本操作。 上述乘法:
DMUL
这会popup堆栈顶部的两个值,然后将tem相乘,然后将结果推回堆栈。 堆栈对于这种机器是必不可less的。
一些更高级别的编程语言(比如C和Pascal)使用后面的方法将parameter passing给函数:按照从左到右的顺序将参数推入堆栈,并由函数体popup,返回值被推回。 (这是编译器厂商制造的一种select,以及X86使用堆栈的方式)。
堆
堆是另一个只存在于编译器领域的概念。 它将处理variables后面的内存所带来的痛苦消除了,但它不是CPU或OS的函数,只是OS内存块的一种select。 如果你愿意的话,你可以做到这一点。
访问系统资源
操作系统有一个公共接口,你可以如何访问它的function。 在DOS中,parameter passing到CPU的寄存器中。 Windows使用堆栈来传递OS函数的参数(Windows API)。