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

blmhpbnm  于 2023-03-05  发布在  其他
关注(0)|答案(4)|浏览(135)

我知道一般的理论,但我无法解释细节.
我知道程序驻留在计算机的辅助存储器中。一旦程序开始执行,它就被完全复制到RAM中。然后处理器一次检索几条指令(这取决于总线的大小),将它们放入寄存器中并执行它们。
我还知道计算机程序使用两种内存:堆栈和堆,它们也是计算机主内存的一部分。堆栈用于非动态内存,堆用于动态内存(例如,与C++中的new运算符相关的所有内容)
我不明白的是这两件事是怎么联系起来的,在什么时候栈被用来执行指令,指令从RAM,到栈,到寄存器?

ct2axkht

ct2axkht1#

这实际上取决于系统,但使用virtual memory的现代操作系统倾向于加载其进程映像并分配内存,如下所示:

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

这是许多常见虚拟内存系统上的一般进程地址空间,"空洞"是总内存大小减去所有其他区域占用的空间;这就为堆提供了大量的空间。2这也是"虚拟的",意味着它通过一个转换表Map到你的"实际"内存,并且可以实际存储在实际内存的任何位置。3这样做是为了保护一个进程不访问另一个进程的内存,并且使每个进程都认为它在一个完整的系统上运行。
注意,例如,堆栈和堆的位置在某些系统上可能是不同的顺序(有关Win32的更多详细信息,请参见Billy O'Neal下面的回答)。
其他系统可能会有很大的不同,例如DOS运行在real mode上,当运行程序时,它的内存分配看起来就大不相同:

+-----------+ 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允许直接访问操作系统内存,没有任何保护,这意味着用户空间程序通常可以直接访问或覆盖任何他们喜欢的东西。
然而在进程地址空间中,程序看起来往往很相似,只是它们被描述为代码段、数据段、堆、栈段等,Map方式也略有不同,但大部分通用区域仍然存在。
一旦将程序和必要的共享库加载到内存中,并将程序的各个部分分配到正确的区域,操作系统就开始在主方法所在的位置执行进程,然后程序从那里接管,在需要时进行必要的系统调用。
不同的系统(嵌入式,无论什么)可能有非常不同的体系结构,如无堆栈系统,哈佛体系结构系统(代码和数据保存在单独的物理内存中),实际上将BSS保存在只读内存中的系统(最初由程序员设置),等等。
你说:
我还知道计算机程序使用两种内存:栈和堆,它们也是计算机主内存的一部分。
"堆栈"和"堆"只是抽象的概念,而不是(必须)物理上不同的内存"种类"。
stack仅仅是一个后进先出的数据结构。在x86架构中,它实际上可以通过使用从末尾的偏移量来随机寻址,但最常见的函数是PUSH和POP,分别用于向其中添加和删除项。它通常用于函数局部变量(所谓的"自动存储")、函数参数、返回地址等。
"heap"只是一个可以按需分配的内存块的昵称,它是随机寻址的(意思是,你可以直接访问其中的任何位置),通常用于在运行时分配的数据结构(在C++中,使用newdelete,在C中使用malloc和friends,等等)。
在x86体系结构上,堆栈和堆物理上都驻留在系统内存(RAM)中,并通过虚拟内存分配Map到进程地址空间,如上所述。
registers(仍在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的更正。]

vc6uscn9

vc6uscn92#

Sdaz在很短的时间内获得了大量的赞成票,但遗憾的是,它使人们对指令如何通过CPU的误解永久化了。
问题是:
指令从RAM到堆栈再到寄存器?
斯达兹说:
还要注意的是,这些并不是实际执行的C代码行,编译器已经将它们转换成可执行文件中的机器语言指令,然后(通常)从TEXT区域复制到CPU流水线,再复制到CPU寄存器,并从那里执行。
但这是错误的,除了自修改代码的特殊情况外,指令从来不进入数据路径,也不能从数据路径执行。
x86 CPU registers包括:

  • 通用寄存器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。

hc8w905p

hc8w905p3#

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

#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 MacSkibons在这里很好地解释了UNIX内存模型,所以我不再重复。但这不是唯一的内存模型。POSIX需要此模型的原因是sbrk系统调用。基本上,在POSIX机器上,要获得更多内存,进程仅仅告诉内核将"洞"和"堆"之间的分隔符进一步移动到"洞"区域中。没有办法将存储器返回到操作系统,而且操作系统本身并不管理堆,你的C运行库必须提供(通过malloc)。
这也暗示了POSIX二进制文件中实际使用的代码类型。(几乎普遍)使用ELF文件格式。在这种格式下,操作系统负责不同ELF文件中库之间的通信。因此,所有库都使用位置无关代码(也就是说,代码本身可以被加载到不同的存储器地址中并且仍然运行),库之间的所有调用都通过一个查找表来查找控制需要跳转到何处以进行跨库函数调用。这增加了一些开销,如果其中一个库更改了查找表,则可能会被利用。
Windows的内存模型是不同的,因为它使用的代码类型是不同的。Windows使用PE文件格式,这使代码保持位置相关的格式。也就是说,代码取决于代码在虚拟内存中的确切位置。2在PE规范中有一个标志,它告诉操作系统当你的程序运行时,库或可执行文件希望被Map到内存中的确切位置。如果一个程序或库不能被加载到它的首选地址,Windows加载程序必须 * rebase * 库/可执行文件--基本上,它移动位置相关的代码以指向新的位置--这不需要查找表,也不能被利用,因为没有查找表可以覆盖。这需要在Windows加载器中非常复杂的实现,并且如果映像需要重定基,则确实需要相当多的启动时间开销。大型商业软件包经常修改它们的库,以故意在不同的地址启动,以避免重定基;windows自己用它自己的库来完成这个操作(例如ntdll.dll,kernel32.dll,psapi.dll等--所有的都有不同的默认起始地址)
在Windows上,通过调用VirtualAlloc从系统获取虚拟内存,并通过VirtualFree将其返回给系统(好吧,从技术上讲,VirtualAlloc将分配给NtAllocateVirtualMemory,但这是实现细节)(与POSIX形成对比,POSIX中的内存不能回收)。Windows还提供它自己的堆函数(HeapAlloc、HeapFree等)作为被称为RtlHeap的库的一部分,RtlHeap被包括作为Windows自身的一部分,C运行时(即,malloc和friends)通常在其上实现。
Windows也有相当多的旧内存分配API,这些API是在它必须处理旧的80386的日子里创建的,现在这些函数是在RtlHeap之上构建的。有关Windows中控制内存管理的各种API的详细信息,请参阅以下MSDN文章:http://msdn.microsoft.com/en-us/library/ms810627.
还要注意的是,这意味着在Windows上,单个进程(通常)拥有多个堆(通常,每个共享库创建自己的堆)。
(Most此信息的部分内容来自Robert Seacord的"C和C++中的安全编码")

llmtgqce

llmtgqce4#

一堆

在X86体系结构中,CPU通过寄存器执行操作。堆栈只是为了方便而使用的。在调用子程序或系统函数之前,你可以将寄存器的内容保存到堆栈中,然后再将它们加载回去,从你离开的地方继续操作。(您可以在没有堆栈的情况下手动添加,但它是一个经常使用的函数,所以它有CPU支持)。但你可以做几乎任何事情没有堆栈在个人电脑上。
例如整数乘法:

MUL BX

AX寄存器乘以BX寄存器。(结果将在DX和AX中,DX包含高位)。
基于堆栈的机器(如JAVA VM)使用堆栈进行基本操作。

DMUL

这个函数从栈顶取出两个值,乘以tem,然后将结果推回栈。栈对于这种机器来说是必不可少的。
一些高级编程语言(如C和Pascal)使用后一种方法向函数传递参数:参数按从左到右的顺序被推到堆栈中,并由函数体弹出,返回值被推回(这是编译器制造商的选择,有点滥用X86使用堆栈的方式)。

那堆

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

访问系统资源

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

相关问题