我了解这个概念的基本理论,但无法理解其中的细节。
我知道程序存储在计算机的二级存储器中。一旦程序开始执行,它就被完全复制到 RAM 中。然后,处理器每次检索几条指令(取决于总线的大小),将它们放在寄存器中并执行。
我也知道计算机程序使用两种类型的内存:栈和堆,它们也是计算机主存的一部分。栈用于非动态内存,堆用于动态内存(例如C++中与new
运算符相关的所有内容)。
我不明白的是这两件事情如何联系起来。在何时使用栈来执行这些指令?指令从 RAM 到栈再到寄存器中吗?
我了解这个概念的基本理论,但无法理解其中的细节。
我知道程序存储在计算机的二级存储器中。一旦程序开始执行,它就被完全复制到 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.
+---------+
+-----------+ 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
你说:
我也知道计算机程序使用两种内存:堆栈和堆。它们也是计算机主内存的一部分。
"堆栈"和"堆"只是抽象概念,而不是(必然)物理上不同的“种类”内存。
堆栈只是一个后进先出的数据结构。在x86架构中,可以通过从结尾偏移来随机寻址,但最常见的功能是PUSH和POP,用于添加和从中删除项目。它通常用于函数本地变量(所谓的“自动存储”),函数参数,返回地址等。(更多细节在下面)
"堆"只是一个昵称,用于按需分配并随机寻址的内存块(这意味着您可以直接访问其中的任何位置)。它通常用于在运行时分配的数据结构(在C ++中使用new
和delete
,在C中使用malloc
和friends等)。
在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
),并查看它。汇编语言非常容易理解。您可以看到堆栈用于函数局部变量,并用于调用函数,存储它们的参数和返回值。这也是为什么当您执行以下操作时:f( g( h( i ) ) );
这些寄存器都不用于可执行代码。 EIP
包含正在执行的指令的地址,而不是指令本身。
指令和数据在CPU中通过完全不同的路径进行(哈佛架构)。目前所有的机器在CPU内部都是哈佛架构。现在大多数机器在缓存中也是哈佛架构。 x86(普通桌面机器)在主内存中是冯·诺伊曼架构,这意味着数据和代码在RAM中交织在一起。但这并不重要,因为我们谈论的是CPU内部发生的事情。
计算机体系结构中教授的经典序列是取指-译码-执行。内存控制器查找存储在地址EIP
处的指令。指令的位通过一些组合逻辑来创建处理器中不同复用器的所有控制信号。经过一些周期后,算术逻辑单元得出一个结果,该结果被时钟输入到目标中。然后获取下一条指令。
在现代处理器上,工作方式略有不同。每个传入的指令都会被翻译成一整个系列的微代码指令。这使得流水线成为可能,因为第一个微指令使用的资源后来不再需要,因此它们可以开始处理下一条指令的第一个微指令。
最后,术语有些混淆,因为“register”是电气工程术语,指的是D触发器集合。指令(尤其是微指令)很可能会暂时存储在这样的D触发器集合中。但当计算机科学家、软件工程师或普通开发人员使用“register”这个术语时,他们的意思并不是这个。他们指的是如上所列的数据通路寄存器,而这些寄存器不用于传输代码。当进程执行时,内存的确切布局完全取决于您使用的平台。考虑以下测试程序:
#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.");
}
}
malloc
和相关函数)通常是在其上实现的。栈
X86架构中,CPU使用寄存器执行操作。栈仅用于方便的原因。在调用子程序或系统函数之前,您可以将寄存器的内容保存到堆栈中,然后再将它们加载回来,以便在离开时继续操作。(您可以手动完成无需使用堆栈的操作,但是由于该功能经常使用,因此CPU提供了支持)。但在PC上,您几乎可以不使用堆栈进行任何操作。
例如,整数乘法:
MUL BX
将AX寄存器与BX寄存器相乘(结果将存在DX和AX中,其中DX包含高位)。
基于堆栈的机器(如JAVA VM)使用堆栈进行其基本操作。上述乘法:
DMUL
这个操作会从栈顶弹出两个值,相乘后再将结果推回栈中。对于这种类型的机器来说,栈是必不可少的。
一些高级编程语言(例如C和Pascal)使用这种方法来传递函数参数:按从左到右的顺序将参数推入栈中,并由函数体弹出,返回值再被推回。这是编译器制造商做出的选择,有点滥用X86使用栈的方式。
堆
堆是编译器领域中的概念,它帮助处理变量背后的内存管理,但它并不是CPU或操作系统的功能,只是OS给出的内存块的一种清理选择。如果你愿意,你也可以手动完成这个过程。
访问系统资源
操作系统有一个公共接口,可以让你访问它的函数。在DOS中,参数以CPU寄存器的形式传递。Windows使用栈来传递操作系统函数的参数(Windows API)。