程序在内存中的外观如何?

时间:2009-11-20 10:26:17

标签: memory winapi program-structure

程序(例如C或C ++)如何安排在计算机内存中?我对片段,变量等有一点了解,但基本上我对整个结构没有扎实的了解。

由于内存中的结构可能不同,我们假设Windows上有一个C ++控制台应用程序。

我特意指出的一些指示:

  • 功能概要,如何调用?
  • 每个函数都有一个堆栈框架,它包含什么以及它如何安排在内存中?
  • 函数参数和返回值
  • 全局和局部变量?
  • const static variables?
  • 线程本地存储..

欢迎链接到类似教程的材料,但请不要参考汇编程序等知识的参考样式材料。

7 个答案:

答案 0 :(得分:10)

这可能是你想要的:

http://en.wikipedia.org/wiki/Portable_Executable

PE文件格式是Windows二进制文件(.exe,.dll等)的二进制文件结构。基本上,它们被映射到内存中。这里描述了更多细节,并解释了你自己如何看待内存中加载的dll的二进制表示:

http://msdn.microsoft.com/en-us/magazine/cc301805.aspx

编辑:

现在我知道您想了解源代码如何与PE文件中的二进制代码相关。这是一个巨大的领域。

首先,您必须了解有关计算机体系结构的基础知识,这将涉及学习汇编代码的一般基础知识。任何“计算机体系结构简介”大学课程都可以。文献包括例如“John L. Hennessy和David A. Patterson。计算机体系结构:定量方法”或“Andrew Tanenbaum,结构化计算机组织”。

阅读完之后,您应该了解堆栈是什么以及它与堆的区别。堆栈指针和基指针是什么,返回地址是什么,有多少寄存器等。

一旦你理解了这一点,将各个部分放在一起就相对容易了:

C ++对象包含代码和数据,即成员变量。一个班级

class SimpleClass {
     int m_nInteger;
     double m_fDouble;

     double SomeFunction() { return m_nInteger + m_fDouble; }
}

将在内存中连续4 + 8个字节。当你这样做时会发生什么:

SimpleClass c1;
c1.m_nInteger = 1;
c1.m_fDouble = 5.0;
c1.SomeFunction();

首先,在堆栈上创建对象c1,即堆栈指针esp减少12个字节以腾出空间。然后将常数“1”写入存储器地址esp-12,并将常数“5.0”写入esp-8。

然后我们调用一个意味着两件事的函数。

  1. 计算机必须将二进制PE文件的一部分加载到包含函数SomeFunction()的内存中。无论你创建多少个SimpleClass实例,SomeFunction都只会在内存中一次。

  2. 计算机必须执行SomeFunction()函数。这意味着几件事:

    1. 调用该函数也意味着传递所有参数,通常这是在堆栈上完成的。 SomeFunction有一个(!)参数,这个指针,即指向堆栈上的内存地址的指针,我们刚刚写了值“1”和“5.0”
    2. 保存当前程序状态,即当前指令地址,即SomeFunction返回时将执行的代码地址。调用函数意味着按下堆栈上的返回地址并将指令指针(寄存器eip)设置为函数SomeFunction的地址。
    3. 在函数SomeFunction中,通过将旧的基指针(ebp)存储在堆栈上(push ebp)并使堆栈指针成为新的基指针(mov ebp,esp)来保存旧堆栈。
    4. 执行SomeFunction的实际二进制代码,它将调用将m_nInteger转换为double的机器指令并将其添加到m_fDouble。 m_nInteger和m_fDouble位于堆栈上,位于ebp-x字节。
    5. 添加的结果存储在寄存器中,函数返回。这意味着堆栈被丢弃,这意味着堆栈指针被设置回基本指针。基指针被设置回(堆栈上的下一个值),然后指令指针被设置为返回地址(堆栈上的下一个值)。现在我们回到原始状态,但在某些寄存器中潜伏着SomeFunction()的结果。
  3. 我建议,你自己建立一个简单的例子并逐步完成反汇编。在调试版本中,代码将易于理解,Visual Studio在反汇编视图中显示变量名称。查看寄存器esp,ebp和eip的作用,分配对象的内存位置,代码所在的位置等。

答案 1 :(得分:4)

这是一个多么大的问题!

首先,您想了解virtual memory。没有它,没有别的意义。简而言之,C / C ++指针不是物理内存地址。指针是虚拟地址。有一个特殊的CPU功能(MMU,内存管理单元)透明地将它们映射到物理内存。仅允许操作系统配置MMU。

这提供了安全性(没有C / C ++指针值可以指向另一个进程的虚拟地址空间,除非该进程是故意与你共享内存)并让操作系统做一些我们现在非常神奇的事情理所当然(比如透明地将一些进程的内存交换到磁盘,然后在进程尝试使用它时透明地加载它)。

进程的地址空间(即k.a.虚拟地址空间,a.k.a。可寻址内存)包含:

  • 为Windows内核保留的巨大内存区域,不允许该进程触及;

  • “未映射”的虚拟内存区域,即没有加载任何内容,没有物理内存分配给这些地址,如果尝试访问它们,进程将崩溃;

  • 将已加载的各种模块(EXE和DLL文件)分开(每个模块包含机器代码,字符串常量和其他数据);以及

  • 进程从系统分配的其他内存。

现在通常一个进程允许C运行时库或Win32库执行大多数超级低级内存管理,包括设置:

  • 一个堆栈(对于每个线程),其中存储了局部变量和函数参数以及返回值;以及

  • 一个堆,如果进程调用mallocnew X,则分配内存。

有关堆栈结构的更多信息,请阅读calling conventions。有关堆的结构的更多信息,请阅读malloc implementations。通常,堆栈实际上是一个堆栈,一个后进先出的数据结构,包含参数,局部变量和偶尔的临时结果,而不是更多。由于程序很容易直接写入堆栈末尾(此站点命名后的常见C / C ++错误),因此系统库通常会确保堆栈旁边有一个未映射的页面。这使得进程在发生此类错误时立即崩溃,因此调试起来要容易得多(并且该进程在可以造成更多损害之前就会被终止)。

在数据结构意义上,堆实际上不是堆。它是由CRT或Win32库维护的数据结构,它从操作系统获取内存页,并在进程通过malloc和朋友请求小块内存时将它们包装出来。 (请注意,操作系统不会对此进行微观管理;如果它不喜欢CRT的方式,则可以在很大程度上管理其所需的地址空间。)

流程还可以使用VirtualAllocMapViewOfFile等API直接从操作系统请求页面。

还有更多,但我最好停下来!

答案 2 :(得分:1)

答案 3 :(得分:1)

要了解堆栈帧结构,您可以参考 http://en.wikipedia.org/wiki/Call_stack

它为您提供有关调用堆栈结构,本地,全局,返回地址如何存储在调用堆栈中的信息

答案 4 :(得分:0)

这可能不是最准确的信息,但MS Press提供了本书Inside Microsoft® Windows® 2000, Third Edition的一些示例章节,其中包含有关过程及其创建的信息以及一些重要数据结构的图像。

我偶然发现this PDF在一张漂亮的图表中总结了一些上述信息。

但所有提供的信息更多来自操作系统的观点,而不是关于应用程序方面的详细信息。

答案 5 :(得分:0)

史蒂文斯的书“高级Unix编程”有几个页面,如果你能掌握它,就会得到确切答案。当然,你应该拥有这本书。

答案 6 :(得分:0)

实际上 - 在这个问题上,你至少可以在Assembler中获得一点点知识。我会重新建立一个倒车(教程)网站,例如OpenRCE.org。