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

gai*_*nco 176 c++ memory x86 operating-system computer-architecture

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

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

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

我无法理解的是这两件事是如何联系起来的.用于执行指令的堆栈在什么时候?指令从RAM,堆栈到寄存器?

Sda*_*ons 159

它真的取决于系统,但具有虚拟内存的现代操作系统倾向于加载其过程映像并分配如下内存:

+---------+
|  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.
+---------+
Run Code Online (Sandbox Code Playgroud)

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

请注意,例如堆栈和堆的位置在某些系统上的顺序可能不同(有关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
Run Code Online (Sandbox Code Playgroud)

您可以看到DOS允许直接访问操作系统内存,没有任何保护,这意味着用户空间程序通常可以直接访问或覆盖他们喜欢的任何内容.

然而,在进程地址空间中,程序往往看起来相似,只是它们被描述为代码段,数据段,堆,堆栈段等,并且它的映射略有不同.但大多数一般领域仍然存在.

将程序和必要的共享库加载到内存中,并将程序的各个部分分配到正确的区域后,操作系统开始执行您的主程序所在的进程,并且程序从那里接管,在必要时进行系统调用.它需要它们.

不同的系统(嵌入式,无论什么)可能具有非常不同的体系结构,例如无堆栈系统,哈佛架构系统(代码和数据保存在单独的物理内存中),实际将BSS保持在只读内存中的系统(最初由程序员)等等.但这是一般的要点.


你说:

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

"堆栈"和"堆"只是抽象概念,而不是(必然)物理上不同的"种类"的内存.

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

一个"堆"仅仅是一个内存块,也可以根据需要分配一个昵称,并随机寻址(意思是,你可以在其中直接访问任何位置).它通常用于您在运行时分配的数据结构(在C++中,使用newdelete,和mallocC中的朋友等).

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.
}
Run Code Online (Sandbox Code Playgroud)

编写一个这样的简单程序,然后将其编译为程序集(gcc -S foo.c如果您可以访问GCC),并查看.组装很容易遵循.您可以看到堆栈用于函数局部变量,并且用于调用函数,存储它们的参数和返回值.这也是你做以下事情的原因:

f( g( h( i ) ) ); 
Run Code Online (Sandbox Code Playgroud)

所有这些都依次被调用.它实际上构建了一堆函数调用及其参数,执行它们,然后在它回退(或向上;)时将它们弹出.但是,如上所述,堆栈(在x86上)实际上驻留在进程内存空间(在虚拟内存中),因此可以直接操作; 它在执行过程中不是一个单独的步骤(或者至少与过程正交).

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

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

  • @Andrey:也许你应该把这个评论改为"你也可以阅读*你的好书推荐*"我明白这类问题值得进一步调查,但每当你必须开始发表评论时,对不起但是......"或许你应该考虑将主持人的注意事项标记在帖子上,或至少提供关于*为什么*你的意见对任何人都很重要的解释. (56认同)
  • 是的,"RTFM"总是更好. (13认同)
  • 对不起,但是一本好的书推荐会是一个更好的答案,IMO (4认同)
  • 很好的答案.它肯定为我清除了一些东西! (2认同)
  • @Mikael:根据实现情况,您可能具有强制缓存,在这种情况下,任何时候从内存中读取数据,读取整个缓存行并填充缓存.或者可以给缓存管理器一个提示,即只需要一次数据,因此将其复制到缓存中是没有用的.这是为了阅读.对于写入,存在回写和直写高速缓存,其影响DMA控制器何时可以读取数据,然后存在用于处理多个处理器的整个高速缓存一致性协议,每个处理器具有其自己的高速缓存.这确实值得拥有自己的Q. (2认同)

Ben*_*igt 60

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.


Bil*_*eal 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.");
    }
}
Run Code Online (Sandbox Code Playgroud)

在Windows NT(和它的孩子),这个程序通常会产生:

堆在堆栈之上

在POSIX盒子上,它会说:

堆栈位于堆上方

@Sdaz MacSkibbons在这里很好地解释了UNIX内存模型,所以我在此不再重复.但这不是唯一的记忆模型.POSIX需要此模型的原因是sbrk系统调用.基本上,在POSIX盒子上,为了获得更多内存,一个进程只是告诉内核将"hole"和"heap"之间的分隔符进一步移动到"hole"区域.无法将内存返回给操作系统,操作系统本身也无法管理堆.您的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本身的一部分包含在其中,malloc通常在其上实现C运行时(即和朋友).

从处理旧的80386时起,Windows也有相当多的遗留内存分配API,而这些功能现在建立在RtlHeap之上.有关在Windows中控制内存管理的各种API的详细信息,请参阅此MSDN文章:http://msdn.microsoft.com/en-us/library/ms810627.

另请注意,这意味着在Windows上,单个进程(通常具有多个堆)具有多个堆.(通常,每个共享库都会创建自己的堆.)

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


vbe*_*nce 5

堆栈

在X86架构中,CPU使用寄存器执行操作.堆栈仅用于方便原因.您可以在调用子例程或系统函数之前将寄存器的内容保存到堆栈中,然后将其加载回来以继续您离开的操作.(你可以手动没有堆栈,但它是一个经常使用的功能,所以它有CPU支持).但是如果没有PC中的堆栈,你几乎可以做任何事情.

例如整数乘法:

MUL BX
Run Code Online (Sandbox Code Playgroud)

将AX寄存器与BX寄存器相乘.(结果将在DX和AX中,DX包含更高的位).

基于堆栈的计算机(如JAVA VM)使用堆栈进行基本操作.以上乘法:

DMUL
Run Code Online (Sandbox Code Playgroud)

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

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

堆是另一个仅存在于编译器领域的概念.它需要处理变量后面的内存的痛苦,但它不是CPU或操作系统的功能,它只是内存管理系统给出的内存块的选择.如果你愿意,你可以多次这样做.

访问系统资源

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