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

我知道一般理论,但我无法适应细节。

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

我也知道一个计算机程序使用两种内存:堆栈和堆,它们也是计算机主内存的一部分。 该堆栈用于非动态内存,动态内存堆(例如,与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.  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以及malloc和C中的朋友等)。

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 -S foo.c如果您有权访问GCC),并查看一下。 该程序集很容易遵循。 您可以看到堆栈用于函数局部变量,并用于调用函数,存储它们的参数和返回值。 这也是为什么当你做这样的事情时:

f( g( h( i ) ) ); 

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

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

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


Sdaz在很短的时间内获得了大量的upvotes,但令人遗憾的是延续了关于指令如何通过CPU的错误观念。

问的问题是:

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

Sdaz说:

还要注意,这些并不是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(您的普通台式机)在主内存中采用冯诺依曼体系结构,这意味着数据和代码混合在RAM中。 这是不言而喻的,因为我们正在谈论CPU内部发生的事情。

    计算机体系结构中教授的经典序列是提取 - 解码 - 执行。 存储器控制器查找存储在地址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盒子上,为了获得更多的内存,一个进程仅仅告诉内核将“洞”和“堆”之间的分隔器进一步移动到“洞”区域。 没有办法将内存返回到操作系统,操作系统本身不管理你的堆。 您的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和朋友)通常实现该malloc

    从处理旧的80386开始,Windows也有不少遗留的内存分配API,现在这些功能都建立在RtlHeap之上。 有关控制Windows内存管理的各种API的更多信息,请参阅此MSDN文章:http://msdn.microsoft.com/zh-cn/library/ms810627。

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

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

    链接地址: http://www.djcxy.com/p/13911.html

    上一篇: What happens when a computer program runs?

    下一篇: Program stack and heap, how do they work?