`
waret
  • 浏览: 130922 次
  • 性别: Icon_minigender_1
  • 来自: 天津
文章分类
社区版块
存档分类
最新评论

windows进程中的内存结构

阅读更多

    接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。

    首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码:

#include <stdio.h>
int g1=0, g2=0, g3=0;
int main()
{
    static int s1=0, s2=0, s3=0;
    int v1=0, v2=0, v3=0;

    //打印出各个变量的内存地址
    printf("0x%08x\n",&v1); //打印各本地变量的内存地址
    printf("0x%08x\n",&v2);
    printf("0x%08x\n\n",&v3);
    printf("0x%08x\n",&g1); //打印各全局变量的内存地址
    printf("0x%08x\n",&g2);
    printf("0x%08x\n\n",&g3);
    printf("0x%08x\n",&s1); //打印各静态变量的内存地址
    printf("0x%08x\n",&s2);
    printf("0x%08x\n\n",&s3);
    return 0;
}

编译后的执行结果是:


0x0012ff78
0x0012ff7c
0x0012ff80

0x004068d0
0x004068d4
0x004068d8

0x004068dc
0x004068e0
0x004068e4

    输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。

├———————┤低端内存区域
│ …… │
├———————┤
│ 动态数据区 │
├———————┤
│ …… │
├———————┤
│ 代码区 │
├———————┤
│ 静态数据区 │
├———————┤
│ …… │
├———————┤高端内存区域

    堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码:

#include <stdio.h>

void __stdcall func(int param1,int param2,int param3)
{
    int var1=param1;
    int var2=param2;
    int var3=param3;
    printf("0x%08x\n",?m1); //打印出各个变量的内存地址
    printf("0x%08x\n",?m2);
    printf("0x%08x\n\n",?m3);
    printf("0x%08x\n",&var1);
    printf("0x%08x\n",&var2);
    printf("0x%08x\n\n",&var3);
    return;
}

int main()
{
    func(1,2,3);
    return 0;
}

编译后的执行结果是:


0x0012ff78
0x0012ff7c
0x0012ff80

0x0012ff68
0x0012ff6c
0x0012ff70

├———————┤<—函数执行时的栈顶(ESP)、低端内存区域
│ …… │
├———————┤
│ var 1 │
├———————┤
│ var 2 │
├———————┤
│ var 3 │
├———————┤
│ RET │
├———————┤<—“__cdecl”函数返回后的栈顶(ESP)
│ parameter 1 │
├———————┤
│ parameter 2 │
├———————┤
│ parameter 3 │
├———————┤<—“__stdcall”函数返回后的栈顶(ESP)
│ …… │
├———————┤<—栈底(基地址 EBP)、高端内存区域

    上图就是函数调用过程中堆栈的样子了。首先,三个参数以从又到左的次序压入堆栈,先压“param3”,再压“param2”,最后压入“param1”;然后压入函数的返回地址(RET),接着跳转到函数地址接着执行(这里要补充一点,介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后,继续压入当前EBP,然后用当前ESP代替EBP。然而,有一篇介绍windows下函数调用的文章中说,在 windows下的函数调用也有这一步骤,但根据我的实际调试,并未发现这一步,这还可以从param3和var1之间只有4字节的间隙这点看出来);第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,上例中是减去12字节(ESP=ESP-3*4,每个int变量占用4个字节);接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈,所以在函数返回前要恢复堆栈,先回收本地变量占用的内存 (ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前压入参数占用的内存(ESP=ESP+3*4),继续执行调用者的代码。参见下列汇编代码:


;--------------func 函数的汇编代码-------------------

:00401000 83EC0C sub esp, 0000000C //创建本地变量的内存空间
:00401003 8B442410 mov eax, dword ptr [esp+10]
:00401007 8B4C2414 mov ecx, dword ptr [esp+14]
:0040100B 8B542418 mov edx, dword ptr [esp+18]
:0040100F 89442400 mov dword ptr [esp], eax
:00401013 8D442410 lea eax, dword ptr [esp+10]
:00401017 894C2404 mov dword ptr [esp+04], ecx

……………………(省略若干代码)

:00401075 83C43C add esp, 0000003C ;恢复堆栈,回收本地变量的内存空间
:00401078 C3 ret 000C ;函数返回,恢复参数占用的内存空间
;如果是“__cdecl”的话,这里是“ret”,堆栈将由调用者恢复

;-------------------函数结束-------------------------


;--------------主程序调用func函数的代码--------------

:00401080 6A03 push 00000003 //压入参数param3
:00401082 6A02 push 00000002 //压入参数param2
:00401084 6A01 push 00000001 //压入参数param1
:00401086 E875FFFFFF call 00401000 //调用func函数
;如果是“__cdecl”的话,将在这里恢复堆栈,“add esp, 0000000C”


聪明的读者看到这里,差不多就明白缓冲溢出的原理了。先来看下面的代码:

#include <stdio.h>
#include <string.h>

void __stdcall func()
{
    char lpBuff[8]="\0";
    strcat(lpBuff,"AAAAAAAAAAA");
    return;
}

int main()
{
    func();
    return 0;
}

    编译后执行一下回怎么样?哈,“"0x00414141"指令引用的"0x00000000"内存。该内存不能为"read"。”,“非法操作”喽!"41"就是"A"的16进制的ASCII码了,那明显就是strcat这句出的问题了。"lpBuff"的大小只有8字节,算进结尾的\0,那strcat最多只能写入7个"A",但程序实际写入了11个"A"外加1个\0。再来看看上面那幅图,多出来的4个字节正好覆盖了RET的所在的内存空间,导致函数返回到一个错误的内存地址,执行了错误的指令。如果能精心构造这个字符串,使它分成三部分,前一部份仅仅是填充的无意义数据以达到溢出的目的,接着是一个覆盖RET的数据,紧接着是一段shellcode,那只要着个RET地址能指向这段shellcode的第一个指令,那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置,那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令,使得exploit有更强的通用性。

├———————┤<—低端内存区域
│ …… │
├———————┤<—由exploit填入数据的开始
│ │
│ buffer │<—填入无用的数据
│ │
├———————┤
│ RET │<—指向shellcode,或NOP指令的范围
├———————┤
│ NOP │
│ …… │<—填入的NOP指令,是RET可指向的范围
│ NOP │
├———————┤
│ │
│ shellcode │
│ │
├———————┤<—由exploit填入数据的结束
│ …… │
├———————┤<—高端内存区域

    windows下的动态数据除了可存放在栈中,还可以存放在堆中。了解C++的朋友都知道,C++可以使用new关键字来动态分配内存。来看下面的C++代码:

#include <stdio.h>
#include <iostream.h>
#include <windows.h>

void func()
{
    char *buffer=new char[128];
    char bufflocal[128];
    static char buffstatic[128];
    printf("0x%08x\n",buffer); //打印堆中变量的内存地址
    printf("0x%08x\n",bufflocal); //打印本地变量的内存地址
    printf("0x%08x\n",buffstatic); //打印静态变量的内存地址
}

void main()
{
    func();
    return;
}

程序执行结果为:


0x004107d0
0x0012ff04
0x004068c0

    可以发现用new关键字分配的内存即不在栈中,也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前,先来了解一下和“堆”有关的几个API函数:

HeapAlloc 在堆中申请内存空间
HeapCreate 创建一个新的堆对象
HeapDestroy 销毁一个堆对象
HeapFree 释放申请的内存
HeapWalk 枚举堆对象的所有内存块
GetProcessHeap 取得进程的默认堆对象
GetProcessHeaps 取得进程所有的堆对象
LocalAlloc
GlobalAlloc

    当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为1M。堆对象由系统进行管理,它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间:

HANDLE hHeap=GetProcessHeap();
char *buff=HeapAlloc(hHeap,0,8);

    其中hHeap是堆对象的句柄,buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢?它的值有什么意义吗?看看下面这段代码吧:

#pragma comment(linker,"/entry:main") //定义程序的入口
#include <windows.h>

_CRTIMP int (__cdecl *printf)(const char *, ...); //定义STL函数printf
/*---------------------------------------------------------------------------
printf函数是C语言的标准函数库中函数,VC的标准函数库由msvcrt.dll模块实现。
由函数定义可见,printf的参数个数是可变的,函数内部无法预先知道调用者压入的参数个数,函数只能通过分析第一个参数字符串的格式来获得压入参数的信息,由于这里参数的个数是动态的,所以必须由调用者来平衡堆栈,这里便使用了__cdecl调用规则。BTW,Windows系统的API函数基本上是 __stdcall调用形式,只有一个API例外,那就是wsprintf,它使用__cdecl调用规则,同printf函数一样,这是由于它的参数个数是可变的缘故。
---------------------------------------------------------------------------*/
void main()
{
    HANDLE hHeap=GetProcessHeap();
    char *buff=HeapAlloc(hHeap,0,0x10);
    char *buff2=HeapAlloc(hHeap,0,0x10);
    HMODULE hMsvcrt=LoadLibrary("msvcrt.dll");
    printf=(void *)GetProcAddress(hMsvcrt,"printf");
    printf("0x%08x\n",hHeap);
    printf("0x%08x\n",buff);
    printf("0x%08x\n\n",buff2);
}

执行结果为:


0x00130000
0x00133100
0x00133118

    hHeap的值怎么和那个buff的值那么接近呢?其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构,这个结构中存放着一些有关进程的重要信息,其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址,而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据,如 windows 2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的,同一时刻只能有一个线程访问堆中的数据,当多个线程同时有访问要求时,只能排队等待,这样便造成程序执行效率下降。

分享到:
评论

相关推荐

    Windows 2000内存结构

    Windows 2000是32位的...Windows在幕后将虚拟内存 (virtual memory,VM) 地址映射到了各进程的物理内存地址上。而所谓物理内存是指计算机的RAM和由Windows分配到用户驱动器根目录上的换页文件。物理内存完全由系统管理

    实验五、Windows 2000内存结构.doc

    实验五、Windows2000内存结构1背景知识Windows2000是32位的操作系统,它使计算机CPU可以用32位地址对32位内存块进行操作。内存中的每一个字节都可以用一个32位的指针来寻址,这样,最大的存储空间就是232字节或4000...

    windowsnt 技术内幕

    理解登录验证过程 理解访问令牌(Access Token) Windows NT目录服务简介 理解Windows NT如何构造用户帐号数据库 使用Windows NT中的Ctrl+Alt+Del组合键 把Windows NT计算机设置成自动登录 改变Windows NT口令 用拨号...

    深入解析Windows操作系统中文.part2.rar

    本书是著名的操作系统内核专家Mark Russinovich和David Solomon撰写的Windows操作系统原理的最新版著作,全面和深入地阐述了Windows操作系统的整体结构以及内部工作细节。本书针对Windows Server 2003、Windows XP...

    WINDOWS 内部原理 (八)

    在这个系列课程中,来自微软的权威技术专家将向您解释Windows操作系统的内部工作原理,从系统架构的大局观出发,逐步展示进程、线程、安全机制、内存管理和存储管理等子系统的工作方式。通过对底层原理的揭示,使您...

    内存管理技术内幕

    Windows NT所面对的最复杂和困难的任务之一就是要管理好计算机上有限的物理内存。NT必须为可能同时运行的多个进程划分物理内存以提供给每个进程适当的一份的事实更加强了这个挑战。此外,NT还必须能在一个从少到16MB...

    深入解析WINDOWS操作系统(第4版).pdf

    无论你是开发人员还是系统管理员,你都可以在本书中找到一些关键的、有关体系结构方面的知识,通过这些知识你可以更好地做系统设计、调试,以及性能优化。 《深入解析:Windows操作系统》(第4版)全书内容丰富、信息...

    深入解析Windows操作系统 中文版 第四版 part 4

    13.1 Windows的网络总体结构 13.2 网络API 13.3 多重定向器支持 13.4 名称解析 13.5 协议驱动程序 13.6 NDIS驱动程序 13.7 绑定 13.8 分层的网络服务 13.9 本章总结 第14章 崩溃转储分析 14.1 Windows为什么会崩溃 ...

    精通Windows.API-函数、接口、编程实例.pdf

    2.1.4 Windows中的数据结构 15 2.2 Windows API的功能分类 15 2.2.1 系统基本服务 15 2.2.2 系统管理 17 2.2.3 用户界面 17 2.2.4 图像和多媒体 20 2.2.5 网络 20 2.2.6 系统安全 20 2.2.7 其他...

    linux下检测内存泄漏

    在 windows 下使用 VC 编程时,我们通常需要 DEBUG 模式下运行程序,而后调试器将在退出程序时,打印出程序运行过程中在堆上分配而没有释放的内存信息,其中包括代码文件名、行号以及内存大小。该功能是 MFC ...

    内存管理内存管理内存管理

    中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。 ...

    《Windows安全原理与技术》简介与教学大纲

    本课程内容: WINDOWS系统的内存管理,虚拟内存访问,文件的内存映射;PE文件结构;进程和线程的管理、Win 32消息处理、堆栈、动态链接库;活动目录、身份验证、访问控制、文件系统安全、网络传输安全、应用服务安全...

    windows 内部原理(一)

    在这个系列课程中,来自微软的权威技术专家将向您解释Windows操作系统的内部工作原理,从系统架构的大局观出发,逐步展示进程、线程、安全机制、内存管理和存储管理等子系统的工作方式。通过对底层原理的揭示,使您...

    精通WindowsAPI 函数 接口 编程实例

    2.1.4 Windows中的数据结构 15 2.2 Windows API的功能分类 15 2.2.1 系统基本服务 15 2.2.2 系统管理 17 2.2.3 用户界面 17 2.2.4 图像和多媒体 20 2.2.5 网络 20 2.2.6 系统安全 20 2.2.7 其他...

    WINDOWS 内部原理(九)

    在这个系列课程中,来自微软的权威技术专家将向您解释Windows操作系统的内部工作原理,从系统架构的大局观出发,逐步展示进程、线程、安全机制、内存管理和存储管理等子系统的工作方式。通过对底层原理的揭示,使您...

    WINDOWS 内部原理(四)

    在这个系列课程中,来自微软的权威技术专家将向您解释Windows操作系统的内部工作原理,从系统架构的大局观出发,逐步展示进程、线程、安全机制、内存管理和存储管理等子系统的工作方式。通过对底层原理的揭示,使您...

    《windows安全与技术》课程实验教学大纲

    第一部分是关于Windows系统内核,包括WINDOWS系统的内存管理,虚拟内存访问,文件的内存映射;PE文件结构;进程和线程的管理、Win 32消息处理、堆栈、动态链接库。对Windows系统内核的深入理解是掌握windows安全技术...

Global site tag (gtag.js) - Google Analytics