调试应用程序内存中的神秘问题

IBM
i 堆内存调试助手

无论在哪种平台上,调试应用程序内的堆内存问题都极为困难。幸运的是,IBM i 6.1 及更高版本提供了相关支持,能帮助用户在 ILE 环境内调试堆内存问题。本文介绍了堆内存是什么,还展示了通过多种 ILE 语言使用堆内存的正确方法,这些语言包括 C、C++、RPG、COBOL 和 CL。此外,本文还描述了使用堆内存时可能出现的一些常见问题,以及如何利用 IBM i 中的支持功能调试这些问题。

1 评论:

Scott
Hanson
, 软件工程师, IBM

2013 年 8 月 08 日

  • 内容

在 IBM Bluemix 云平台上开发并部署您的下一个应用。

现在就开始免费试用

堆内存是什么?

堆内存是在应用程序内动态分配内存时常用的可用内存池。之所以使用 “堆内存” 这个术语,是因为动态内存分配的大多数实现均使用称为堆 的一种二进制树型数据结构。动态内存分配是在应用程序运行时显式分配和取消分配内存(或存储)的一种方法。在本文中,堆管理器 这个术语用于描述处理应用程序动态内存分配的软件。

堆管理器执行两种主要操作。第一种操作是分配。这种操作将保留给定大小的一个存储块,并返回所保留存储块的指针。存储块的惟一所有权将赋予应用程序,应用程序可以使用该存储块来实现它所需要的任何目的。第二种操作是取消分配。此操作会将之前已分配好的存储块返回给堆管理器。取消分配一个存储块时,块的所有权也将返还给堆管理器。此后,堆管理器可以使用该存储块继续分配。

堆管理器经常会提供的第三种操作是重新分配。这种操作会重新调整给定存储块的大小,并返回存储块的指针(可能经过更新)。重新分配操作并不属于严格需要的操作(因为可以通过基本的分配和取消分配操作予以实现),但堆管理器通常会提供这种操作。

在 IBM i? 上,共有两种不同类型的堆存储:单层存储和 teraspace 存储。应用程序使用的存储类型由创建程序时指定的程序属性决定。默认情况下使用的是单层存储。如果需要使用 teraspace 存储,必须使用特殊编译选项和程序创建选项。除此之外,也可以使用应用程序编程接口 (API) 在单层存储应用程序内分配和取消分配 teraspace 存储。如需进一步了解这两种类型的存储,请参阅 ILE
概念
 手册中的 “Teraspace 和单层存储” 部分。

两种类型的堆存储之间存在某些重大差异。从堆中分配单层存储时,最多只能分配 16 MB 的内存。从堆中分配 teraspace 存储时,可以分配数 TB 大小的存储。必须使用十六字节的指针来寻址单层存储。对于支持八字节指针的语言(例如 C 和 C++),可以使用八字节指针寻址 teraspace 存储。单层存储堆的总大小限制为每作业 4 GB。teraspace 堆不存在这类限制,teraspace 存储堆的总大小仅受限于可用的系统存储数量。

如何分配或取消分配堆存储

尽管每种语言均有不同的分配和取消分配方法,但所有集成语言环境 (ILE) 语言均可使用堆内存。C 语言中使用的是 malloc() 和 free() 函数,C++
中使用的是 new 和 delete 操作符,而
RPG 中使用的是 %ALLOC 内置函数和 DEALLOC 操作。尽管
COBOL 和 CL 没有管理堆内存的内置函数,但 ILE 模型允许从任意 ILE 语言调用函数,因此这些语言可以调用 malloc() 和 free() 函数来管理堆内存。实际上,所有
ILE 语言均可调用 malloc() 和 free() 函数来管理堆内存。下面的几个示例展示了所有这些语言中对堆内存的分配、使用和取消分配。

示例 1 是使用 C 语言编写的。

示例 1 – C 中的堆内存

/* To compile: CRTBNDC PGM(EXAMPLE1) */

#include <stdlib.h>
#include <string.h>

int main (int argc, char *argv[])
{
  /* allocate 16 bytes of storage from the heap */
  char *ptr = (char*)malloc(16);

  /* set the allocated storage */
  memcpy(ptr, "abcdefghijklmnop", 16);

  /* deallocate storage */
  free(ptr);

  return 0;
}

示例 2 是使用 C++ 语言编写的。

示例 2 – C++ 中的堆内存

/* To compile: CRTBNDCPP PGM(EXAMPLE2) */
#include <string.h>

int main (int argc, char *argv[])
{
  /* allocate 16 bytes of storage from the heap */
  char *ptr = new char[16];

  /* set the allocated storage */
  memcpy(ptr, "abcdefghijklmnop", 16);

  /* deallocate storage */
  delete [] ptr;

  return 0;
}

示例 3 是使用 RPG 语言编写的。

示例 3 – RPG 中的堆内存

      * To compile: CRTBNDRPG PGM(EXAMPLE3)
     H dftactgrp(*no)
     D [email protected]            S               *
     D based_data      S             16A   BASED([email protected])
     D sz              S              5  0 INZ(16)
      /free
        // allocate 16 bytes of storage from the heap
        [email protected] = %ALLOC(sz);

        // set the allocated storage
        based_data = ‘abcdefghijklmnop‘;

        // deallocate storage
        DEALLOC [email protected];

        *inlr = ‘1‘;

示例 4 是使用 COBOL 语言编写的。

示例 4 – COBOL 中的堆内存

       PROCESS NOMONOPRC.
       IDENTIFICATION DIVISION.
      * To compile: CRTBNDCBL PGM(EXAMPLE4) BNDDIR(QC2LE)
       PROGRAM-ID. EXAMPLE4.
       ENVIRONMENT DIVISION.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  ptr                POINTER.
       01  sz                 PIC S9(9) BINARY.
       LINKAGE SECTION.
       77  based-data PIC X(16).
       PROCEDURE DIVISION.
       MAIN-LINE SECTION.
       001-MAIN-FLOW.
           MOVE 16 TO sz.
      *    allocate 16 bytes of storage from the heap
           CALL LINKAGE PRC "malloc" USING BY VALUE sz
                                     RETURNING ptr.
      *    set the allocated storage
           SET ADDRESS OF based-data TO ptr.
           MOVE "abcdefghijklmnop" TO based-data.
      *    deallocate storage
           CALL LINKAGE PRC "free" USING BY VALUE ptr.
           STOP RUN.

示例 5 是使用 CL 语言编写的。

示例 5 – CL 中的堆内存

/* To compile: CRTBNDCL PGM(EXAMPLE5) */
PGM
  DCL VAR(&PTR) TYPE(*PTR)
  DCL VAR(&SZ) TYPE(*INT) VALUE(16)
DCL VAR(&BASED_DATA) TYPE(*CHAR) LEN(16) STG(*BASED) +
    BASPTR(&PTR)

  /* allocate 16 bytes of storage from the heap */
  CALLPRC PRC(‘malloc‘) PARM( (&SZ *BYVAL) ) RTNVAL(&PTR)

  /* set the allocated storage */
  CHGVAR VAR(&BASED_DATA) VALUE(‘abcdefghijklmnop‘)

  /* deallocate storage */
  CALLPRC PRC(‘free‘) PARM( (&PTR *BYVAL) )
ENDPGM

这些示例仅分配了极少的堆存储(16 字节)。通常情况下,所分配的存储块要比这大得多。举例来说,考虑这样一个事实,CL 变量的最大大小为 32767 字节。利用堆存储和 *PTR 变量,可以在 CL 中管理更大的存储块。

堆内存的常见问题

堆分配和取消分配必须由应用程序显式执行,因此可能会出现这些操作使用不当的问题。堆内存使用不当的常见场景包括:写入的数据量超过了所分配的存储量(内存写入越界),读取的数据量超过了已分配内存的大小(内存读取越界),写入存储或读取存储时使用的是此前已经取消分配的存储(重用已取消分配的内存),取消分配存储超过一次(重复取消分配),在内存不再使用时未能取消分配内存(内存泄漏)。

下面的示例程序展示了这些堆内存问题。

前两个场景涉及到了堆分配的大小。分配时将为堆管理器提供所需的存储大小,它将返回足够大的存储块,以满足所请求的大小。如果应用程序未能正确计算堆大小,那么有时会意外地读取或写入不属于当前分配存储的一部分堆存储。这可能会给应用程序带来问题。

示例 6 展示了 C++ 中的内存写入越界问题。堆仅分配了 16 字节的大小,但总计写入了 17 字节的数据。strcpy() 函数不仅会复制字符串的
16 个字节,而且还会追加一个零字节后缀(NULL 字符)。

示例 6 – C++ 中的内存写入越界

/* To compile: CRTBNDCPP PGM(EXAMPLE6) */
#include <string.h>

int main (int argc, char *argv[])
{
  char *ptr = new char[16];

  /* strcpy() copies the trailing NULL character */
  strcpy(ptr, "abcdefghijklmnop");  /* memory overwrite */
  delete [] ptr;
  return 0;
}

示例 7 展示了 C 语言中的内存写入越界。堆仅分配了 13 字节的大小,但总计写入了 14 字节的数据。strcpy() 函数不仅会复制字符串的
13 个字节,而且还会追加一个零字节后缀(NULL 字符)。

示例 7 – C 中的内存写入越界

/* To compile: CRTBNDC PGM(EXAMPLE7) */
#include <stdlib.h>
#include <string.h>

int main (int argc, char *argv[])
{
  char *ptr = (char*)malloc(13);

  /* strcpy() copies the trailing NULL character */
  strcpy(ptr, "abcdefghijklm");  /* memory overwrite */
  free(ptr);
  return 0;
}

示例 8 展示了 RPG 中的内存写入越界问题。堆仅分配了 13 字节的大小,但总计写入了 14 字节的数据。基本变量的大小是 14 个字节。

示例 8 – RPG 中的内存写入越界

      * To compile: CRTBNDRPG PGM(EXAMPLE8)
     H dftactgrp(*no)
     D [email protected]            S               *
     D message         S             14A   BASED([email protected])
      /free
        [email protected] = %ALLOC(13);

        // the entire 14-byte message variable is
        // updated, including trailing blanks
        message = ‘hello‘;   // memory overwrite
        DEALLOC [email protected];

        *inlr = ‘1‘;

下一个场景涉及过早地取消分配应用程序仍在使用的堆存储。逻辑错误可能允许应用程序引用已经取消分配并返回给堆管理器进行重用的堆存储。此类分配中引用的数据可能是所需数据,也可能不是所需数据,可能会导致应用程序中出现间歇性错误。

示例 9 是使用 C++ 编写的,展示了写入不再属于已分配存储的堆存储的情况。

示例 9 - 取消分配的内存重用

/* To compile: CRTBNDCPP PGM(EXAMPLE9) */
#include <string.h>

int main (int argc, char *argv[])
{
  char *ptr = new char[16];

  delete [] ptr;
  strcpy(ptr, "abc");  /* reuse of deallocated memory */
  return 0;
}

下一个场景涉及到多次取消分配相同的存储。

示例 10 是使用 C 语言编写的,展示了取消分配内存的重复调用。

示例 10 – 取消分配内存的重复调用

/* To compile: CRTBNDC PGM(EXAMPLE10) */
#include <stdlib.h>

int main (int argc, char *argv[])
{
  char *ptr = (char*)malloc(16);

  free(ptr);
  free(ptr);  /* duplicate deallocation */
  return 0;
}

最后一个场景是未能对某些已经分配的堆内存执行取消分配。这就叫做内存泄露,因为内存泄漏 到了堆以外的地方,无法再供应用程序使用。尽管内存不再被引用,但堆管理器并不了解该情况,也无法重用内存。内存泄漏会造成严重的问题。大量内存泄漏会导致性能问题,还有可能导致应用程序耗尽内存。在长期运行的应用程序中,这种问题尤为严重。在某些时候,应用程序结束之后(在激活组终止时),操作系统会回收该应用程序的所有堆存储,因此内存泄漏不再成为问题。

示例 11 是使用 RPG 编写的,展示了内存泄漏的情况。尽管应用程序尝试取消分配存储,但传递给取消分配函数的指针值不是分配函数返回的原始指针,因此未能取消对内存的分配。

示例 11 – RPG 中的内存泄漏

      * To compile: CRTBNDRPG PGM(EXAMPLE11)
     H dftactgrp(*no)
     D [email protected]            S               *
     D name            S             10A   BASED([email protected])
     D i               S              5  0
      /free
        // allocate enough storage for 100 names
        [email protected] = %ALLOC(%SIZE(name) * 100);

        // move along the storage one name at a time
        for i = 1 to 100;
          name = ‘something‘;
          [email protected] += %SIZE(name);
        endfor;

        // deallocate storage
        DEALLOC [email protected];        // memory leak

        *inlr = ‘1‘;

不正确的堆使用会导致间歇性的应用程序故障、不当的应用程序行为,甚至损坏的数据。致使堆问题难以调试的一个特征是:堆错误往往不会造成直接后果。举例来说,在缓冲区写入越界的情况下,写入的数据超出了已分配内存缓冲区的结尾处。当引用写入越界、不再包含所需数据的内存时,问题征兆要到很晚的时候才会显现在应用程序中。

IBM i 堆内存管理器

IBM i 6.1 及更新版本中提供了三种堆内存管理器:默认内存管理器、Quick Pool 内存管理器和调试内存管理器。对给定应用程序有效的内存管理器由应用程序运行时的 QIBM_MALLOC_TYPE 环境变量的设置控制。6.1
版本中的 PTF 5761SS1-SI33945 提供了访问其他堆内存管理器的环境变量,IBM i 发布版的后续版本也包含此类环境变量。在添加环境变量访问的同时,还添加了调试内存管理器。在版本 5.4 和 6.1 中,还可以调用 API 进行支持设置,从而使用 Quick Pool 内存管理器。如需查看堆内存管理器支持的完整文档,请参阅 ILE
C/C++ 运行时库函数
 手册。

默认内存管理器

默认内存管理器是一种通用的内存管理器,它会尝试平衡性能和内存需求。它为绝大多数应用程序提供了充足的性能,同时会尝试最大程度地减少开销所需的额外内存量。默认内存管理器是大多数应用程序的最佳选择,默认情况下,内存管理器是启用的。如果 IBM_MALLOC_TYPE 环境变量尚未设置,或者被设置为无法识别的值,则会使用默认的内存管理器。

Quick Pool 内存管理器

Quick Pool 内存管理器会将内存拆分为一系列池,以便提高发出大量较小的分配请求的应用程序的性能。在启用 Quick Pool 内存管理器时,将为处于给定分配大小范围内的分配请求分配池中固定大小的单元。这些请求的处理速度将快于大小超出此范围的请求。超出此范围的分配请求将按照与默认内存管理器相同的方式处理。

默认情况下不会启用 Quick Pool 内存管理器,但可以通过设置以下环境变量来启用它:

 QIBM_MALLOC_TYPE=QUICKPOOL

也可以在应用程序中使用 API 调用启用 Quick Pool 内存管理器。如需了解有关的更多信息,请参阅 ILE
C/C++ 运行时库函数
 手册。

调试内存管理器

调试内存管理器主要用于查找应用程序没有正确使用堆的情况。它并未针对性能而优化,可能会对应用程序的性能造成负面影响。然而,它对于确定不当的堆使用情况很有价值。

调试内存管理器检测到的内存问题会导致以下两种行为之一:

  • 如果在发生不当使用之时检测到问题,那么将会生成一条机器检查句柄 (MCH) 异常消息(通常是 MCH0601、MCH3402 或者 MCH6801)。在这种情况下,错误消息通常会停止应用程序。
  • 如果在不当使用已经发生之后才检测到问题,则会生成一条 C2M1212 消息。在这种情况下,消息通常不会停止应用程序。

调试内存管理器会通过两种方式检测内存问题:

  • 首先,使用限制访问内存页面。在每次分配之前和之后使用一个限制访问权限的内存页面。让每个内存块都与 16 字节的边界对齐,并尽可能地将它们放置在页面结尾处。由于仅允许在页面边界处保护内存,所以这样的对齐对于内存写入越界和内存读取越界的检测效果最好。在一个限制访问权限的内存页面中执行任何读取或写入操作时,会立即生成一条 MCH 异常。
  • 第二,它会在每次分配前后使用一个填充字节。在分配时,紧邻每次分配的内存之前的几个字节会初始化为预设的字节模式。在分配之时,如果分配的大小需要限制为 16 字节的倍数,那么紧邻所分配内存之后的填充字节也会初始化为预设的字节模式。在所分配的内存取消分配时,将验证填充字节,确保其仍然包含预期的预设字节模式。如果任何填充字节被修改,那么调试内存管理器会生成一条 C2M1212 消息,原因代码为 X‘80000000‘。

默认情况下不会启用调试内存管理器,但可以通过设置以下环境变量来启用它:

 QIBM_MALLOC_TYPE=DEBUG

调试堆内存的常见问题

上文列出了使用堆内存时的几种常见问题,还给出了一些示例程序,展示了各种堆问题。调试内存管理器允许检测多种堆内存常见问题,包括:内存写入越界、内存读取越界、重用已取消分配的内存和重复的取消分配。调试内存管理器不会检测内存泄漏问题。ILE 应用程序内的内存泄漏检测将在未来的文章中加以介绍。

在使用调试内存管理器运行程序时,将描述展示堆问题的每个示例程序的行为。

示例 6 展示了一个内存写入越界问题。其中展示了尝试超越大小为 16 字节的倍数的数据项末尾的一项写入操作。将该示例编译为一个单层存储程序,并使用调试内存管理器运行此程序,在发生内存写入越界时,这会生成一条 MCH0601 消息。将该示例编译为一个 teraspace 存储程序,并使用调试内存管理器运行此程序,在发生内存写入越界时,这会生成一条 MCH6801 消息。无论出现哪种情况,错误消息的细节都会指向执行内存写入越界的语句。示例展示了内存写入越界,但内存读取越界也会得到相同的结果。

示例 7 展示了一个内存写入越界问题。其中演示了未超越 16 字节边界的内存写入操作。将该示例被编译为一个单层存储程序或 teraspace 程序,并使用调试内存管理器运行该程序,这会得到一条 C2M1212 消息,原因代码为 X‘80000000‘。消息的细节将指向调用 free() 的语句。调试内存管理器无法检测到未超越
16 字节边界的内存读取越界。

示例 8 展示了一个内存写入越界问题。由于在默认情况下 RPG 应用程序不会启用调试内存管理器,因此未能检测到内存写入越界。IBM i 7.1 的一项增强为 RPG 添加了 ALLOC(*TERASPACE) 控制规范关键字。该关键字将通知 RPG 运行时为内存分配和取消分配使用 C 运行时 teraspace 堆函数,允许对 RPG 应用程序使用调试内存管理器。完成上述操作之后,该示例将具备与示例 7 相同的行为。如需进一步了解 ALLOC 关键字的其他细节,请参阅 ILE
RPG 语言参考 手册。

示例 9 展示了写入不再属于已分配存储的堆存储的情况。将该示例编译为一个单层存储程序,并使用调试内存管理器运行此程序,在发生内存写入越界时,这会生成一条 MCH3402 消息。将该示例编译为一个 teraspace 存储程序,并使用调试内存管理器运行此程序,在发生内存写入时,这会生成一条 MCH6801 消息。无论出现哪种情况,错误消息的细节都会指向执行内存写入的语句。示例展示了内存写入,但内存读取也会得到相同的结果。

示例 10 展示了取消分配内存的重复调用。将该示例编译为一个单层存储程序或 teraspace 程序,并使用调试内存管理器运行该程序,这会得到一条 C2M1212 消息。消息的细节将指向调用 free() 的语句。

示例 11 展示了一个内存泄漏问题。如前文所述,调试内存管理器不会检测内存泄漏问题。

如果未使用调试内存管理器,则无法检测到这些内存问题,同时还有可能导致间歇性的应用程序问题、不当的应用程序行为或损坏的数据。

破解堆的谜题

对于编写和维护 ILE 应用程序而言,了解堆内存是什么以及如何正确使用堆内存的能力极为重要。在明确认识常见堆问题的同时,利用调试内存管理器即可轻松检测到应用程序内的堆问题,迅速解决 IBM i ILE 应用程序内的堆内存问题。

本文内容最初是在 iProDeveloper 2010 年 8 月刊中发布的。

参考资料

学习

讨论

时间: 2024-10-16 18:18:11

调试应用程序内存中的神秘问题的相关文章

大页内存(HugePages)在通用程序优化中的应用

今天给大家介绍一种比较新奇的程序性能优化方法-大页内存(HugePages),简单来说就是通过增大操作系统页的大小来减小页表,从而避免快表缺失.这方面的资料比较贫乏,而且网上绝大多数资料都是介绍它在Oracle数据库中的应用,这会让人产生一种错觉:这种技术只能在Oracle数据库中应用.但其实,大页内存可以算是一种非常通用的优化技术,应用范围很广,针对不同的应用程序,最多可能会带来50%的性能提升,优化效果还是非常明显的.在本博客中,将通过一个具体的例子来介绍大页内存的使用方法. 在介绍之前需要

IDA远程调试 在内存中dump Dex文件

1. 首先使用调试JNI_OnLoad函数的方法,先将apk以调试状态挂起,使用IDA附加上去. 2. 然后在libdvm.so中的dvmDexFileOpenPartial函数上下一个断点 3. 然后我们点击继续运行,程序就会在dvmDexFileOpenPartial()这个函数处暂停,R0寄存器指向的地址就是dex文件在内存中的地址,R1寄存器就是dex文件的大小 4. 然后我们就可以使用ida的script command去dump内存中的dex文件了. static main(void

内存管理--程序在内存中的分布

在多任务操作系统中的每一个进程都运行在一个属于它自己的内存沙盘中.这个沙盘就是虚拟地址空间(virtual address space). 1 32位虚拟内存布局 在32位模式下虚拟地址空间总是一个4GB的内存地址块.这些虚拟地址通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用.每一个进程拥有一套属于它自己的页表,但是还有一个隐情.只要虚拟地址被使用,那么它就会作用于这台机器上运行的所有软件,包括内核本身.因此一部分虚拟地址必须保留给内核使用: 图 1 这并不意味

程序在内存中的分布

转载,原文地址:http://blog.csdn.net/hackbuteer1/article/details/6786811 在现代的操作系统中,当我们说到内存,往往需要分两部分来讲:物理内存和虚拟内存.从硬件上讲,虚拟空间是CPU内部的寻址空间,位于MMU之前,物理空间是总线上的寻址空间,是经过MMU转换之后的空间. 一般我们所说的程序在内存中的分布指的就是程序在虚拟内存中的存储方式. 从低地址到高地址,可分为下面几段: 预留内存地址(操作系统维护的内存地址,不可访问) 程序代码区(只读,

Java程序开发中的简单内存分析

首先说明内存总体分为了4个部分, 包括 1.stack segment (栈区存储基本数据类型的局部变量,对象的引用名) 2.heap segment(堆区,一般用于存储java中new 出来的对象) 3.code segment (代码段) 4.data segment (数据段,静态数据常量) 其中我们程序中用关键字new出来的东西都是存放在heap segment: 程序中的局部变量存放在stack segment,这些局部变量是在具体方法执行结束之后,系统自动释放内存资源(而heap s

在Eclipse中使用MAT分析Android程序内存使用状况(转)

对于Android这种手持设备来说,通常不会带有太大的内存,而且一般用户都是长时间不重启手机,所以编写程序的时候必须要非常小心的使用内存,尽量避免有内存泄露的问题出现.通常分析程序中潜在内存泄露的问题是一件很有难度的工作,一般都是由团队中的资深工程师负责,而且随着程序代码量的提高,难度还会逐步加大. 今天要介绍一个在Eclipse中使用的内存分析工具——MAT(Eclipse Memory Analyzer,主页在http://www.eclipse.org/mat/).它是一个功能非常丰富的J

程序在内存中的分配方式

1 桟区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值,其操作方式类似于数据结构中的栈. 若申请的内存小于系统所剩内存则成功,否则就会发生栈溢出错误. 栈的增长方向是向下的,通常栈的初始化指针指向内存的最高地址,它是连续的.系统自动分配,因此速度很快. 在函数调用时,第一个进栈的是主函数的下一条指令的地址,然后是各个函数的参数,参数从右向左进栈,然后是函数中的局部变量.静态变量是不入栈的. 2 堆区(heap):一般有程序员分配和释放,若不释放,最后由操作系统回收. 它与

如何让程序只在内存中执行一个

在WinForm或者Console程序中,可以通过可以遍历所有现在正在执行的进程,如果有同名的进程存在,那么说明这个程序已经启动了一个了,此处就不再启动了.如果没有同名进程的存在,说明这个程序还没有启动过,此时我们可以启动一个. public static void Main() { string strModuleName = System.Diagnostics.Process.GetCurrentProcess().MainModule.ModuleName; string strProc

【翻译】Anatomy of a Program in Memory—剖析内存中的一个程序(进程的虚拟存储器映像布局详解)

[翻译]Anatomy of a Program in Memory—剖析内存中的一个程序(进程的虚拟存储器映像布局详解) . . .