9

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

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

一些指向我所追求的具体目标:

  • 一个函数的概要,它是如何被调用的?
  • 每个函数都有一个堆栈帧,它包含什么以及它在内存中是如何排列的?
  • 函数参数和返回值
  • 全局变量和局部变量?
  • 常量静态变量?
  • 线程本地存储..

欢迎链接到类似教程的材料等,但如果您了解汇编程序等,请不要参考风格的材料。

4

6 回答 6

12

这可能是您正在寻找的东西:

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 and David A. Patterson. Computer Architecture: A Quantitative Approach”或“Andrew Tanenbaum, Structured Computer Organization”。

看完这篇,你应该明白什么是栈,以及它与堆的区别。堆栈指针和基指针是什么,返回地址是什么,有多少个寄存器等。

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

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() 的内存中。SomeFunction 只会在内存中存在一次,无论您创建了多少个 SimpleClass 实例。

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

    1. 调用函数还意味着传递所有参数,通常这是在堆栈上完成的。SomeFunction 有一个(!)参数,this 指针,即指向我们刚刚写入值“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() 的结果。

我建议,您自己构建一个这样简单的示例并逐步进行反汇编。在调试构建中,代码将很容易理解,Visual Studio 在反汇编视图中显示变量名称。查看寄存器 esp、ebp 和 eip 的作用、对象在内存中的分配位置、代码的位置等。

于 2009-11-20T10:43:13.550 回答
4

多么大的问题!

首先你要了解虚拟内存。没有它,其他任何事情都没有意义。简而言之,C/C++ 指针不是物理内存地址。指针是虚拟地址。有一个特殊的 CPU 功能(MMU,内存管理单元)可以透明地将它们映射到物理内存。仅允许操作系统配置 MMU。

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

进程的地址空间(又名虚拟地址空间,又名可寻址内存)包含:

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

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

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

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

现在通常一个进程让 C 运行时库或 Win32 库完成大部分超低级内存管理,包括设置:

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

  • 一个堆,如果进程调用malloc或执行,则在其中分配内存new X

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

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

进程也可以直接从操作系统请求页面,使用 APIVirtualAllocMapViewOfFile.

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

于 2009-11-20T11:49:17.657 回答
1

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

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

于 2009-11-20T12:04:58.733 回答
1

另一个很好的插图 http://www.cs.uleth.ca/~holzmann/C/system/memorylayout.pdf

于 2009-11-21T05:34:58.480 回答
0

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

我还偶然发现了这个 PDF,它在一个漂亮的图表中总结了上述一些信息。

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

于 2009-11-20T11:09:25.043 回答
0

实际上 - 至少对汇编程序有一点了解,你不会在这件事上走得太远。我会推荐一个反向(教程)站点,例如 OpenRCE.org。

于 2009-11-20T11:37:33.183 回答