为了更好地理解发生了什么,让我们假设我们只有一个非常原始的操作系统,它运行在一个 16 位处理器上,一次只能运行一个进程。这就是说:一次只能运行一个程序。此外,让我们假设所有中断都被禁用。
我们的处理器中有一个叫做栈的结构。堆栈是强加在物理内存上的逻辑结构。假设我们的 RAM 存在于地址 E000 到 FFFF 中。这意味着我们正在运行的程序可以以任何我们想要的方式使用这个内存。假设我们的操作系统说 E000 到 EFFF 是栈,F000 到 FFFF 是堆。
堆栈由硬件和机器指令维护。我们真的不需要做太多的事情来维护它。我们(或我们的操作系统)需要做的就是确保为堆栈的开始设置正确的地址。堆栈指针是一个物理实体,位于硬件(处理器)中,由处理器指令管理。在这种情况下,我们的堆栈指针将设置为 EFFF(假设堆栈向后增长,这很常见,-)。对于像 C 这样的编译语言,当你调用一个函数时,它会将你传入的任何参数推送到堆栈上的函数中。每个参数都有一定的大小。 int 通常是 16 或 32 位,char 通常是 8 位,等等。假设在我们的系统上,int 和 int* 是 16 位。对于每个参数,堆栈指针按 sizeof(argument) 递减 (--),并将参数复制到堆栈上。然后,您在作用域中声明的所有变量都以相同的方式压入堆栈,但它们的值并未初始化。
让我们重新考虑两个与您的两个示例相似的示例。
int hello(int eeep)
{
int i;
int *p;
}
在我们的 16 位系统上发生的情况如下:
1)将eeep推入堆栈。这意味着我们将堆栈指针递减到 EFFD(因为 sizeof(int) 为 2),然后实际上将 eeep 复制到地址 EFFE(我们的堆栈指针的当前值,减 1,因为我们的堆栈指针指向第一个可用点分配后)。有时有些指令可以一举完成(假设您要复制适合寄存器的数据。否则,您必须手动将数据类型的每个元素复制到堆栈上的适当位置——顺序很重要! )。
2) 为 i 创造空间。这可能意味着只是递减指向 EFFB 的堆栈指针。
3) 为 p 创造空间。这可能意味着只是将堆栈指针递减到 EFF9。
然后我们的程序运行,记住我们的变量所在的位置(eeep 从 EFFE 开始,i 在 EFFC,p 在 EFFA)。要记住的重要一点是,即使堆栈计数 BACKWARDS,变量仍会向前运行(这实际上取决于字节顺序,但重点是 &eeep == EFFE,而不是 EFFF)。
当函数关闭时,我们只需将堆栈指针增加 (++) 6,(因为 3 个大小为 2 的“对象”,而不是 c++ 类型,已被压入堆栈。
现在,您的第二种情况更难以解释,因为实现它的方法太多,几乎不可能在互联网上解释。
int hello(int eeep)
{
int *p = malloc(sizeof(int));//C's pseudo-equivalent of new
free(p);//C's pseudo-equivalent of delete
}
eeep 和 p 仍然像前面的例子一样被压入并分配到堆栈上。然而,在这种情况下,我们将 p 初始化为函数调用的结果。 malloc (或 new,但 new 在 c++ 中做得更多。它在适当的时候调用构造函数,以及其他所有东西。)所做的就是进入这个称为 HEAP 的黑盒并获取空闲内存的地址。我们的操作系统将为我们管理堆,但我们必须让它知道我们何时需要内存以及何时使用完。
在示例中,当我们调用 malloc() 时,操作系统将通过给我们这些字节的起始地址来返回一个 2 字节的块(我们系统上的 sizeof(int) 为 2)。假设第一次调用给了我们地址 F000。然后,操作系统会跟踪当前正在使用的地址 F000 和 F001。当我们调用 free(p) 时,操作系统会找到 p 指向的内存块,并将 2 个字节标记为未使用(因为 sizeof(star p) 为 2)。相反,如果我们分配更多内存,则地址 F002 可能会作为新内存的起始块返回。请注意, malloc() 本身就是一个函数。当 p 被压入堆栈以进行 malloc() 的调用时,p 被再次复制到堆栈中第一个打开的地址,该地址在堆栈上有足够的空间以适应 p 的大小(可能是 EFFB,因为我们只压入了 2这次堆栈上的东西大小为 2,并且 sizeof(p) 为 2),堆栈指针再次递减到 EFF9,malloc() 将从该位置开始将其局部变量放在堆栈上。当 malloc 完成时,它将所有项目从堆栈中弹出,并将堆栈指针设置为调用之前的值。 malloc() 的返回值,一个空星,可能会被放置在某个寄存器中(在许多系统上通常是累加器)供我们使用。
在实施过程中,这两个例子都没有这么简单。当您为新函数调用分配堆栈内存时,您必须确保保存状态(保存所有寄存器),以便新函数不会永久擦除值。这通常也涉及将它们推入堆栈。同样的方法,你通常会保存程序计数器寄存器,以便子程序返回后可以返回正确的位置。内存管理器会用尽自己的内存,以便“记住”哪些内存已经发出,哪些没有。虚拟内存和内存分段使这个过程更加复杂,内存管理算法必须不断移动块(并保护它们)以防止内存碎片(它自己的整个主题),这与虚拟内存有关也是。与第一个示例相比,第二个示例确实是一大罐蠕虫。此外,运行多个进程使这一切变得更加复杂,因为每个进程都有自己的堆栈,并且堆可以被多个进程访问(这意味着它必须保护自己)。此外,每种处理器架构都不同。有些架构会希望您将堆栈指针设置为堆栈上的第一个空闲地址,而其他架构会希望您将其指向第一个非空闲地址。
我希望这会有所帮助。请告诉我。
请注意,以上所有示例都是针对过度简化的虚构机器。在真正的硬件上,这会有点麻烦。
编辑:星号没有出现。我用“明星”这个词代替了它们
不管怎样,如果我们在示例中使用(大部分)相同的代码,分别将“hello”替换为“example1”和“example2”,我们会在 wndows 上得到以下 intel 的汇编输出。
.文件“test1.c”
。文本
.globl _example1
.def _example1; .scl 2; . 类型 32; .endef
_example1:
推%ebp
movl %esp, %ebp
低于 8 美元,%esp
离开
ret
.globl _example2
.def _example2; .scl 2; . 类型 32; .endef
_example2:
推%ebp
movl %esp, %ebp
低于 8 美元,%esp
movl $4, (%esp)
调用 _malloc
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
movl %eax, (%esp)
拨打_free
离开
ret
.def _free; .scl 3; . 类型 32; .endef
.def _malloc; .scl 3; . 类型 32; .endef