代码之家  ›  专栏  ›  技术社区  ›  sharkin

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

  •  9
  • sharkin  · 技术社区  · 16 年前

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

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

    我特别想要的是:

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

    欢迎链接到类似教程的材料等,但请不要参考样式的材料假设组装知识等。

    7 回复  |  直到 16 年前
        1
  •  10
  •   Sebastian    16 年前

    也许这就是你想要的:

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

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

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

    编辑:

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

    首先,您必须了解有关计算机体系结构的基础知识,其中包括学习汇编代码的一般基础知识。任何“计算机体系结构导论”的大学课程都可以。文献包括“约翰·L·亨尼西和大卫·A·帕特森”。计算机体系结构:一种定量方法“或“安德鲁·坦纳鲍姆,结构化计算机组织”。

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

    一旦你理解了这一点,就可以相对容易地将各个部分组合起来:

    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. 执行某个函数的实际二进制代码,该代码将调用机器指令,该指令将m\ninteger转换为double并将其添加到m\u fdouble。在堆栈上的ebp-x字节处可以找到m\u-ninteger和m\u-fdouble。
      5. 加法的结果存储在寄存器中,函数返回。这意味着堆栈被丢弃,这意味着堆栈指针被设置回基指针。返回基指针(堆栈上的下一个值),然后将指令指针设置为返回地址(堆栈上的下一个值)。现在我们回到了原始状态,但在某些寄存器中隐藏了someFunction()的结果。

    我建议,您构建一个如此简单的示例,并逐步完成分解。在debug build中,代码易于理解,Visual Studio在反汇编视图中显示变量名。查看寄存器esp、ebp和eip的作用、对象在内存中的分配位置、代码在哪里等。

        2
  •  4
  •   Jason Orendorff Oliver    16 年前

    多大的问题啊!

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

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

    进程的地址空间(也称为虚拟地址空间,也称为可寻址内存)包含:

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

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

    • 组成已加载的各种模块(exe和dll文件)(每个模块都包含机器代码、字符串常量和其他数据);以及

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

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

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

    • 一个堆,如果进程调用 malloc 或做 new X .

    有关堆栈结构的更多信息,请阅读 calling conventions . 有关堆的结构的详细信息,请阅读 malloc implementations . 一般来说,堆栈实际上是一个堆栈,一个后进先出的数据结构,包含参数、局部变量和偶尔出现的临时结果,而且不多。因为程序很容易直接写入堆栈的结尾(这个站点被命名为常见的C/C++错误),所以系统库通常确保有一个与堆栈相邻的未映射的页面。这使得当发生这样的错误时,进程会立即崩溃,因此更容易调试(并且在该进程造成更多损害之前,它会被杀死)。

    从数据结构的角度来看,堆实际上不是堆。它是由CRT或Win32库维护的数据结构,当进程通过 马洛克 和朋友们。(请注意,操作系统不会对此进行微观管理;如果不喜欢CRT的方式,进程可以在很大程度上根据需要管理其地址空间。)

    进程还可以使用类似API的 VirtualAlloc MapViewOfFile .

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

        3
  •  1
  •   atv    16 年前

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

    它提供了关于调用堆栈结构、局部变量、全局变量和返回地址如何存储在调用堆栈上的信息。

        4
  •  1
  •   Tanuj    16 年前
        5
  •  0
  •   Frank Bollack    16 年前

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

    我也偶然发现 this PDF 这在一个漂亮的图表中总结了上面的一些信息。

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

        6
  •  0
  •   Tobias Langner    16 年前

    事实上,在这个问题上,您至少需要一点汇编程序方面的知识。我会重新创建一个反向(教程)站点,例如opernce.org。

        7
  •  0
  •   Rob    16 年前

    史蒂文斯的《高级Unix编程》一书有好几页,如果你能掌握的话,上面有一个确切的答案。当然,你应该拥有这本书。

    推荐文章