从内存释放软件的原理到虚拟内存

本文已发于《黑客X档案》2010.11期

某日,群里某人提问如何实现内存释放功能,便引起了我的兴趣。从网上下载了一款内存释放软件进行逆向分析。由于逆向细节不符合本文主题,所以省略之。

逆向后发现,该程序核心部分——即内存释放功能,是调用了SetProcessWorkingSetSize()函数。此函数在MSDN描述如下:

“Sets the minimum and maximum working set sizes for the specified process”,中文意思是:设置进程的最大最小工作空间。此话什么意思?这里涉及到虚拟内存的概念,只要掌握到这个概念后,就能明白这句话的意思了。

虚拟内存,是现代操作系统中一个比较重要的概念。现在的物理内存大小(即内存条本身容量)已经不能满足应用软件的需求了。为了解决物理内存空间紧张的问题,便引入了虚拟内存的概念,将外部存储设备用来当作内存的一部分,通常用的是硬盘。

一个程序运行时,其实并不是所有代码都在内存中,有一部分暂时不执行的代码将会存入硬盘中,并以“页”为单位存储,留下重要部分在内存中执行。操作系统已经将内存分割成一块一块的装入或者换出内存了,这个叫“分页”,换出和装入是以“页”为单位进行的。可以这样理解“页”,一个练习小子的小字本每页有若干个方格子,但是每页的格子数是一样的。在Windows系统中,每页的大小默认是4KB,有若干“页”。但是页面的装入和换出是CPU和硬盘之间的操作,大家都知道CPU直接读取硬盘的操作是非常慢的,所以装入页面时也会较慢,为了不频繁装入和换出,操作系统需要一系列的算法进行调度,关于此细节比较复杂,不作叙述。

大家应该有过这样的经验,当在一台配置不是很高的机器上打游戏时,将游戏从全屏缩小后继续恢复全屏,会有一小段时间的卡死状态,而且某些游戏缩小后就没有了背景音乐。这两点很好解释,其实游戏缩小时,已经将不需要执行的代码换出到硬盘空间中了,所以有些游戏缩小后听不见背景音乐;而重新恢复到游戏状态时,需要将那些换出的代码再次装入内存,前面说了,CPU和硬盘交互是很慢的,便出现了暂时性的缓慢状态。我们可以做个实验来观察一下Windows的这个机制。我以写本稿的WORD为例,在缩小前,它的内存占用约47M,如图1:

1.png

我将WORD缩小后,再观察它的内存大小,如图2,一下就变来只有1M多了:

2.png

其余的哪里去了?便正是交换到硬盘空间里了。这便是程序的后台运行。

所以,我们可以得出这样的结论,减少程序内存占用的大小,可以让它把不必要的代码交换到硬盘空间里,只将运行该程序的关键代码留在内存中,但是一旦程序激活,它们又将会换入内存。而SetProcessWorkingSetSize()函数正是这样的功能,应该明白了“设置进程的最大最小工作空间”这句话了吧?就是设置进程占用内存的空间大小。

这个函数的原型如下,以下涉及到写代码部分,倘若看不懂,可以略过代码,我会写下思路。

BOOL SetProcessWorkingSetSize (
			       HANDLE hProcess,
			       SIZE_T dwMinimumWorkingSetSize,
			       SIZE_T dwMaximumWorkingSetSize
			       );

一个个来解释参数,第一个参数hProcess,它的类型是HANDLE,即指定一个进程的句柄;第二个参数dwMinimumWorkingSetSize和第三个参数dwMaximumWorkingSetSize分别是设置程序运行空间的最小和最大空间。我们再仔细看MSDN,有这样一句话“If both dwMinimumWorkingSetSize and dwMaximumWorkingSetSize have the value (SIZET)–1, the function removes as many pages as possible from the working set of the specified process.”,大概中文意思是如果将dwMinimumWorkingSetSize和dwMaximumWorkingSetSize设置成-1,就保留必要的一部分代码,其余的交换出去。这正是我们需要的。

了解了这个函数后,我们来编写一个程序,程序的功能就是将指定进程的内存释放出去。代码如下:

#include <stdio.h>

#include <windows.h>

#include <tlhelp32.h>

//////////////////////////////////////////////////////////////////////

/////by  :    乱雪

/////email:    lx#shellcodes.org

/////功能:SetProcessWorkingSetSize函数演示

//////////////////////////////////////////////////////////////////////

int main()

{

  //定义一个PROCESSENTRY3结构体,并填充结构的大小

  PROCESSENTRY32 pentry = {sizeof(pentry)};

  //用CreateToolhelp32Snapshot建立一个进程快照

  HANDLE hPSnap =CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);

  //得到首个进程

  BOOL bMore = Process32First(hPSnap,&pentry);



  //循环搜索所有进程,找到WINWORD.exe这个进程

  while(bMore)

    {

      if(strcmp("WINWORD.EXE",pentry.szExeFile) == 0)

	{

	  //如果找到,就用OpenProcess获得它的句柄

	  //根据MSDN对SetProcessWorkingSetSize的描述,进程必须有PROCESS_SET_QUOTA权限

	  HANDLE hProcess = OpenProcess(PROCESS_SET_QUOTA,

					FALSE,

					pentry.th32ProcessID);

	  //hProcess不为空就表明获得了句柄值

	  if(hProcess != NULL)

	    {

	      //调用SetProcessWorkingSetSize函数

	      SetProcessWorkingSetSize(hProcess, -1, -1);

	      CloseHandle(hProcess);

	    }

	  else

	    break;

	}

      bMore = Process32Next(hPSnap,&pentry); //获得下一个进程

    }

  CloseHandle(hPSnap);   //关闭句柄

  return 0;

}

以上代码在VC6.0编译通过。还是以写本稿的WORD进程WINWORD.EXE为例子,编译程序后,我们先看该进程在任务管理器中所显示的内存占用大小,如图3:

3.png

然后运行刚才编译好的程序,内存占用大小瞬间减下去了,如图4:

4.png

这个程序的思路是,首先使用CreateToolhelp32Snapshot函数建立一个进程快照,然后Process32First函数获得第一个进程,接着再用一个循环进行搜索,只要还有进程存在,就一直搜索直到最后一个,每循环一次,就判断当前进程是不是我们需要搜索的那个,如果是,就调用SetProcessWorkingSetSize函数;否则就继续循环到下一个进程。

在这里大概讲述了一下虚拟内存的概念,相信大家应该对内存释放软件有了一个大概的了解了,将部分不需要的页面换出去是可以腾出一些内存空间给其他新进程使用,但频繁交换页面,会影响系统速度,因为前面说了,CPU和硬盘之间交互操作是很慢的。并且Windows本身就有了这个功能,当你将应用程序缩小时,已经把部分不重要的代码交换出去,留下关键代码在内存中执行。

其实内存管理、进程这些的内容远远不止本文所述,涉及到操作系统原理,较为复杂,本文只是给了一个合理的描述,起到一个引导性的作用,倘若有读者对操作系统原理感兴趣的,在此推荐一些书:《深入理解计算机系统》、《操作系统精髓与设计原理》、《深入解析Windows操作系统》、《Windows内核原与实现》。至于Linux内核方面的,个人觉得大部分都还不错,大家可以适当阅读。当然看这些书除了需要金钱上的代价,还需要基本功的。