1 映射到内存的可执行文件和DLL
系统预定一块足够大的地址空间来容纳.exe文件,待预订的地址空间区域的具体位置已经在PE文件中(这里是exe)中指定了,默认情况下,.exe文件的基地址是0x00400000(对运行在64位windows下的64位程序来说,这个地址可能会有所不同。)但是,只需要在构建exe时使用/BASE链接器开关就可以为自己的应用程序指定一个不同的地址。
系统会对地址空间区域进行标注,表明该区域的后备物理地址存储器来自磁盘上的exe文件而非来自系统的页交换文件。
默认情况下,MS的连接器将X86平台的DLL的基地址设为0x10000000,将X64平台的DLL的基地址设为0x00400000,但是只要在构建DLL时用/BASE链接器开关就可以指定一个不同的基地址,所有与windows一起发布的系统DLL都有不同的基地址,这样即使把他们载入同一个地址空间也不会发生重叠。
如果系统无法在DLL文件指定的基地址处预定区域,原因可能是:1、该区域已经被其他exe或DLL占用, 2、区域不够大。 这个时候系统会尝试在另一个地址来为DLL预定地址空间区域。如果系统无法将DLL载入指定的基地址,那么原因:DLL中不包含重定位信息(当使用/FIXED链接器开关来构建DLL时就去除了重定位信息)。
重定位不仅需要用页交换文件中额外的存储空间,而且会增加载入DLL所需的时间!
系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的DLL文件而非来自系统的页交换文件,另外如果windows不能将DLL加载到指定的基地址而必须重定位的话,那么系统还会另外进行标注,表明DLL中有一部分物理存储器被映射到了页交换文件。
2 默认情况下同一个可执行文件或DLL的多个实例不会共享静态数据
也就是说,一个可执行文件或DLL里的全局变量,对于多个该可执行文件或DLL的实例来说,他们之间的这些静态变量是相互独立的。
如果一个应用程序正在运行,那么当我们为这个打应用程序创建一个新的进程时,系统只不过是打开另一个内存映射视图(memory-mapped-view),系统创建一个新的进程对象,并(为主线程)创建一个新的线程对象。这个新打开的内存映射视图隶属一个文件映射对象(file-mapping object),后者用来标识可执行文件的映像。系统同时给进程对象和线程对象分别指定新的进程ID(process id)和新的线程ID(thread id)。 通过内存映射文件,同一个应用程序的多个实例之间可以共享内存中的代码和数据。
2.1关于数据
在exe中,数据位于代码之后, 应用程序的第二个实例开始运行时,系统只不过是把包含应用程序代码和数据的虚拟内存页面映射到第二个实例的地址空间中。
文件的内容被分为段,代码在一个段中,全局变量在一个段中,段是对齐到页面大小的整数倍。 应用程序可以通过调用GetSystemInfo来检测页面大小。 在exe或DLL中,代码段通常位于数据段前面。
如果应用程序的一个实例修改了数据页面的一些全局变量,那么应用程序的其他实例的内存都会被修改,为了避免这种情况,系统通过内存管理系统的【写时复制】(copy-on-write)来防止这种情况发生。 如果实例A要修改数据页面P2上的一个全局变量g,那么系统会重新申请一个页面newP,然后复制P2上的内容,最后让A写大新页面newP,这样其他实例的内存都不会受到影响。
2.2关于代码
除了数据,还有代码,一种情况是我们用VS调试的时候,在源码某个地方下断点,那么下断点的地方源码就被修改了,因为调试器会把一条汇编语言指令改成一条激活调试器的指令。假设此时已经有该程序的其他实例,此时同样会按照写时复制的机制来去除这种影响,系统复制一个新的代码页,把原始页面复制到新页面,然后在新页面上修改源代码。
3 如何让同一个可执行文件或DLL的多个实例共享静态数据
补充知识:每个exe或DLL文件映像由许多段组成,按照惯例,每个标准的段名都以点号开始,例如,代码段放在一个名叫.text的段中,未经初始化的数据放在.bss段中,已经初始化的数据放在.data段中。 每个段都由一些与之相关联的属性,如下:
READ 可以从该段读取数据
WRITE 可以向该段写入数据
EXECUTE 可以执行该段的内容
SHARED 该段的内容为多个实例所共享(这个属性事实上关闭了写时复制机制)
使用VS提供的命令,dumpbin可以查看各个段, dumpbin /headers XXX.exe 回车即可
下面举个例子,模仿飞秋的只能启动一个实例的功能,如下:
在某个合适的地方,例如XX.CPP文件的头部
1 #pragma data_seg("cuihaoshared") //注意这个段名,区分大小写,在用dumpbin查看的时候,发现该段名为截取为"cuihaosh",猜测可能对长度有限制 2 bool g_bOnlyOneInstance = false; //必须初始化,否则不能被放入cuihaoshared段,而被放入未初始化段 3 #pragma data_seg() //结束 4 5 #pragma comment(linker, "/section:cuihaoshared,rws") //段名区分大小写,读/写/共享
在XX.CPP所在类的析构函数中
1 g_bOnlyOneInstance = false; //这句话写在析构函数中
在应用程序类构造函数中
1 if (true == g_bOnlyOneInstance) 2 { 3 AfxMessageBox(_T("only one instance")); //提示一下 4 TerminateProcess(::GetCurrentProcess(), 1); //结束本进程 5 }
上面提到,如果#pragma data_seg("name") 和 #pragma data_seg()之间的全局变量不初始化的时候不会放入name段,而是会放入未初始化的段, 有一个写法可以不初始化也放入自己新建的段,前提是MS VC++的编译器才支持, 使用allocate声明符,如下:
1 #pragma data_seg("name") 2 int a = 0; //放入name段 3 int b; //不放入name段 4 #pragma data_seg() 5 6 __declspec(allocate("name")) int c = 2; //放入name段 7 8 __declspec(allocate("name")) int d; //放入name段
that's all, thank you!