Win32程序中的几个基本概念

0x00 前言

  Win32指的是32位的Windows系操作系统。1993年5月,Microsoft发布了具备安全性和稳定性特征的32位操作系统Windwos NT3.11,这是在Windwos系列中使用32位编程模式的第一个版本。
  当运行第一个”Hello world”的程序,发现这和日常使用Windows应用根本不一样啊,这是为什么,平时看到的窗口应用程序的代码又是怎样写的,就这样走进了Win32。
  所以,本篇简单总结Win32中最基础概念中的3个:

  • 窗口与消息循环
  • 工作模式与内存管理
  • 进程线程与程序

0x01 进程线程与程序

  进程和线程在字面上看起来颇为相近,两者又是息息相关的,所以往往给初学者造成混淆,其实从英文原文来看,进程(Process)和线程(Thread)是完全不同的。   

01. 进程

  进程是正在执行中的应用程序,磁盘上存储的可执行文件只能称之为文件而不能称为进程,内存中正在执行的文件才叫做进程。一个进程是一个执行中的文件使用资源的总和,包括虚拟地址空间、代码、数据、对象句柄、环境变量和执行单元等。当一个应用程序同时被多次执行时,产生的是多个进程,因为虽然它们由同一个文件执行而来,但是它们的地址空间等资源是互相隔离的,这与不同文件在执行的情况是一样的。

02. 线程

  进程是不“活泼”的,要使进程中的代码被真正运行起来,它必须拥有在这个环境中运行代码的“执行单元”,这就是线程,线程是操作系统分配处理器时间的基本单位,一个线程可以看成是一个执行单元,它负责执行包含在进程地址空间中的代码。当一个进程被建立的时候,操作系统会自动为它建立一个线程,这个线程从程序指定的入口地址开始执行(对于Windows编程,就是WinMain了),通常把这个线程称为主线程,当主线程执行完最后一句代码的时候,进程也就没有继续存在的理由了,这时操作系统会撤销进程拥有的地址空间和其他资源,对我们来说,这就意味着程序的终止。
  在主线程中,程序可以继续建立多个线程来“同时”执行进程地址空间中的代码,这些线程被称为子线程。操作系统为每个线程保存单独的寄存器环境和单独的堆栈,但是它们共享进程的地址空间、对象句柄、代码和数据等其他资源,它们可以执行相同的代码,可以对相同的数据进行操作,也可以使用相同的句柄。你可以把一个进程中的多个线程看成是进程范围内的“多任务”。

  

03. 进程与线程的关系

  进程和线程的关系可以看做是“容器”和“内容物”的关系,进程是线程的容器,线程总是在某个进程的环境中被创建,它不可以脱离进程单独存在,而且线程的整个生命周期都存在于进程中,如果进程被结束,其中的线程也就自然结束了。   

04. 事件

  Windows中可以创建多种类的对象,如文件、窗口和内存等对象都是看得见摸得着的实体,事件(Event)也是一种对象,对对象比较抽象,我们可以把它看成一个设置在Windows内部的标志,它的状态设置和测试工作由Windows来完成,Windows可以将这些标志的设置和测试工作和线程调度等工作在内部结合起来,这样效率会高很多。   

0x02 窗口与消息循环

00. 窗口是什么

  对于应用程序来说,它认为窗口是自己拥有的显示空间;对于Windows,因为各个程序在屏幕上不能相互干扰,窗口是帮助实现多任务”同时进行”的手段。
  窗口不等于程序,它可能只是程序的一部分。Windows窗口采用层次结构,一个窗口中可以建立多个子窗口。
  程序也不等于窗口,例如后台运行的各种程序。   

01. 窗口与事件驱动

  DOS程序与控制台程序是过程驱动,由程序运行的阶段决定用户该做什么。
  窗口程序(绝大多)为事件驱动,用户可能随时发出各种消息,例如”最小化”、”关闭”等,这就需要窗口程序需要一些措施,这就是消息处理:
  这里写图片描述

02. 一个简单的窗口程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <windows.h>
#pragma comment(lib, "winmm")
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("HelloWin");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc; //<- WndProc回调函数
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
if (!RegisterClass(&wndclass)){
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, // window class name
TEXT("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL); // creation parameters
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0)){
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message){
case WM_CREATE:
PlaySound(TEXT("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rect);
DrawText(hdc, TEXT("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}

03. 窗口程序的运行过程

WinMain()的主流程:

  • 1.得到应用程序的句柄(GetModuleHandle)
    (Win32汇编当中hInstance需要自己获取,c语言通过WinMain由系统传入)
  • 2.注册窗口类(RegisterClassEx)
  • 3.建立窗口(CreateWindowEx)
  • 4.显示窗口(ShowWindow)
  • 5.刷新窗口客户区(UpdataWindow)
  • 6.进入消息获取和处理的循环(GetMessage-> TranslateMessage-> DispatchMessage)

WndProc:
  Windows 应用程序创建的每个窗口都在系统核心注册一个相应的窗口函数,窗口函数程序代码形式上是一个巨大的switch 语句,用以处理由消息循环发送到该窗口的消息,窗口函数由Windows 采用消息驱动的形式直接调用,而不是由应用程序显示调用的,窗口函数处理完消息后又将控制权返回给Windows。

04. 消息队列与消息循环

  0.消息是什么?
  比如用户按键,鼠标移动等,这些操作都被视为消息。
 
这里写图片描述

  1.Windows处理消息的流程
  Windows在系统内部有一个系统消息队列,当输入设备(比如鼠标、键盘)有所动作的时候,Windows都会产生相应的记录放在系统消息队列里,每个记录包括消息的类型、发生的位置和发生的时间的信息。(箭头ab
  
  同时,Windows为每个程序(严格来说是每个线程)维护一个消息队列。Windows检查系统消息队列里消息的发生位置,当位置位于某个应用程序的窗口范围内时,就把这个消息派送到应用程序的消息队列里。(箭头c
  
  当应用程序还没有来去消息的时候,消息就暂时保留在消息队列里,当程序中的消息循环执行到GetMessage的时候,控制权转移到GetMessage所在的USER32.DLL中(箭头1),USER32.DLL从程序消息队列中取出一条消息(箭头2),然后把这条消息返回应用程序(箭头3)。

  应用程序可以对这条消息进行预处理,如可以用TranslateMessage把基于键盘扫描码的按键消息转换成基于ASCII码的键盘消息,以后也会用到TranslateAccelerator把键盘快捷键转换成命令消息,但这个步骤不是必需的。

  然后应用程序将处理这条消息,但方法不是自己直接调用窗口过程来完成,而是通过DispatchMessage间接调用窗口过程,Dispatch的英文含义是分派,之所以是分派,是因为一个程序可能建有不止一个窗口,不同的窗口消息必须分派给相应的窗口过程。当控制权转移到USER32.DLL中的DispatchMessage(箭头4)时,它会找出消息对应窗口的窗口过程,然后把消息的具体信息当做参数来调用它(箭头5),窗口过程根据消息找到对应的分支去处理,然后返回(箭头6),这是控制权回到DispatchMessage,最后DispatchMessage函数返回应用程序(箭头7)。这样,一个循环就结束了,程序又开始新一轮的GetMessage。
  
  2.应用互发消息的流程
  应用程序之间也可以互发消息,PostMessage是把一个消息队列放到其他程序的消息队列中(箭头d),目标程序收到了这条消息就把它放入该程序的消息队列去处理;而SendMessage则越过消息队列直接调用目标程序的窗口过程(箭头I),窗口过程返回以后才从SendMessage返回(箭头II)。
  
  3.借助Windows调用WndProc的原因

  • 1.如果由程序自己调用WndProc,当多窗口时会十分复杂,这意味着程序要维护自己所有窗口的消息。
  • 2.其他应用程序可以使用SendMessage(越过消息队列)通过Windows调用目标程序的WndProc。
  • 3.Windows并非把所有消息都放进消息队列。

05. 句柄与模块

  1.句柄
  句柄只是个数值,只是Windows用来表示各种资源的编号。
  举个例子,旅客入住酒店:他自己的需求订房间,前台找到合适的房间,把房号告诉旅客,旅客在此房间中做某些事。
  在这个例子中,旅客==应用程序,订房间==请求资源(比如建个窗口),房号==句柄值,做某些事==应用程序操作。旅客不关心房号的值,因为住到合适的房间就好;同时,旅客也不能去他人的房间。
 
  但讨论句柄的实质,它是指向结构体的指针,详见:http://blog.csdn.net/wenzhou1219/article/details/17659485
 
  Windows中几乎所有东西都是拿句柄来标识的,文件句柄、窗口句柄、线程句柄和模块句柄等。他们只是应用程序方便找到自己想要的资源的编号
 
  2.模块
  一个模块代表的是一个运行中的exe文件或dll文件,用来代表这个文件中所有的代码和资源,磁盘上的文件不是模块,装入内存后运行时就叫做模块。一个应用程序调用其他DLL中的API时,这些DLL文件被装入内存,就产生了不同的模块,为了区分地址空间中的不同模块,每个模块都有一个惟一的模块句柄来标识。
 
  ps.黑历史:当时第一次用OD逆向的时候,模块这个概念太神秘了(姿势不够啊xxd)
   

0x03 工作模式与内存管理

01. 80x86处理器的工作模式

  1. 实模式
      寻址为分段机制;物理寻址为1MB(20位);所有段可读可写可执行。

  2. 保护模式
      寻址为分页机制;物理寻址为4GB(32位);存在优先级(0级~3级)。

  3. 虚拟86模式
      支持任务切换与分页;物理寻址为1MB;采用模拟方法完成特权指令。

02. 内存寻址、分页机制与内存安排

  1. Win32的内存寻址
  在Win32实模式或DOS中,我们知道寻址方法是由段寄存器(例如CS、DS、SS)乘以16当做基地址,加上偏移地址。因为无法对内存进行分页管理,所以寻址地址就是物理地址。
这里写图片描述

  在Win32保护模式中,一个地址空间能否可以被写入、可以被多少优先级的代码写入都是个问题,于是我们需要定义一些安全上的属性,这需要64位长的数据才能表示,这些64位的数据成为段描述符

  然而,段寄存器是16位的,无法放入这些64位的数据,解决办法是把所有段的段描述符放在内存的指定位置,组成一个段描述表;而段寄存器中的16位数据用来索引信息,指定这个段的属性用段描述表中的第几个描述表来表示。这时,段寄存器中的信息不再是段地址了,而是段选择器:可以通过它在段描述表中”选择”一个项目以得到段的全部信息。
  
  80386中引入了两个新的寄存器来管理段描述符表。一个是48位的全局描述符表寄存器GDTR,一个是16位的局部描述符表寄存器LDTR。
  
  GDTR指向的描述符表为全局描述符表GDT(Global Descriptor Table)。
  它包含系统中所有任务都可用的段描述符,通常包含描述操作系统所使用的代码段、数据段和堆栈段的描述符及各任务的LDT段等;全局描述符表只有一个
  
  LDTR则指向局部描述符表LDT(Local DescriptorTable)
  80386处理器设计成每个任务都有一个独立的LDT。它包含有每个任务私有的代码段、数据段和堆栈段的描述符,也包含该任务所使用的一些门描述符,如任务门和调用门描述符等。不同任务的局部描述符表分别组成不同的内存段,描述这些内存段的描述符当做系统描述符放在全局描述符表中。
  
  和GDTR直接指向内存地址不同,LDTR和CS,DS等段选择器一样只存放索引值,指向LDTR内存段对应的描述符早GDTR中的位置。这样任务切换只改变LDTR的值即可。实际上,16位的段选择器中只有高13位表示索引值。剩下的3个数据位中,第0,1位表示程序的当前优先级RPL;第2位TI位用来表示在段描述符的位置;TI=0表示在GDT中,TI=1表示在LDT中。
  
  在保护模式下,同样以xxxx:yyyyyyyy格式表示一个逻辑地址。
  对于这个地址,首先要看xxxx的TI位是否为0,如果是的话,则先从GDTR寄存器中获取GDT的基址(图中的步骤①),然后在GDT中以段选择器xxxx的高12位当做位置索引得到段描述符(步骤②)。段描述符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(步骤③)。
  如果xxxx的TI位为1的话就更复杂了,这表示段描述符在LDT中,这时第一步的操作还是从GDTR寄存器中获取GDT的基址(步骤1’),并且要从LDTR中获取LDT所在段的位置索引(步骤2’);然后以这个位置索引在GDT中得到LDT段的位置(步骤3’);然后才是用xxxx做索引从LDT段中获得段描述符(步骤4’),再以这个段描述符得到段的基址等信息(步骤5’)。分这两种情况得到段的基址后(图中Result所示),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。
这里写图片描述

  2. Win32的分页机制与虚拟内存
  前1节中,讨论的都是分段机制的内容,不管是DOS还是Win32,明显的特征是通过寄存器附加偏移来计算一个地址,在DOS或实模式中,计算结果即可表示物理地址;在Win32中,得到的线性地址(或称虚拟地址)还需要其他步骤才能转变为物理地址,这就是分页机制。

  为什么需要分页机制呢?因为多任务的操作系统,内存碎片化是不能允许的。80386处理器吧4KB大小的一块内存当做一”页”内存,每页物理内存可以根据”页目录”和”页表”,随意映射到不同的线性地址上,有效避免内存碎片化。同时页表规定了页的访问属性,是否可读可写可执行。

  Win32中把线性地址转换为物理地址是根据页目录和页表指定的映射关系,见图

  这里写图片描述
  
  最后说到页要提到虚拟内存。Windows系统一般在硬盘上建立物理大小为物理内存两倍左右的交换文件用作虚拟内存。虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。
  只需要在真正访问到的时候将硬盘文件读入物理内存,然后重新将线性地址映射到这块物理内存。同样,被执行的可执行文件也不必真正装入内存,至于要在页表中建立映射关系,以后真正运行到某处代码的时候再将它调入物理内存。
  
  3. Win32的内存安排
  从物理内存中的层次看,Windows操作系统和DOS一样,也是所有的内容共享内存,如操作系统使用的代码和数据。
  从应用程序代码的层次看,则Windows被看作一个分时的多任务的操作系统,CPU被分成一个个的时间片,分配给不同的应用程序,在一个程序的时间片中,和这个程序执行无关的东西(如其它程序的代码和数据),并不被映射到相应的线性地址中去,这样,各个程序就独立开了,在这个应用程序的线性地址内,并不能访问别的程序所使用的线性地址空间。
  
  Windows操作系统通过切换不同的页表内容让线性地址在不同的时间片中映射到不同的内容:
这里写图片描述

  • 1.每个应用程序都有自己的4GB的寻址空间,该空间可以存放操作系统,系统DLL和用户DLL的代码,它们之中有各种函数供应用程序调用。再除去一些其它的空间,余下的就是应用程序的代码,数据和可以分配的地址空间。
  • 2.不同的应用程序的线性地址空间是隔离的,虽然它们在物理内存中同时存在,但是在某个程序的时间片中,其它应用程序的代码和数据没有被映射到相应的可寻址的线性地址中,所以是不可访问的。程序可以使用自己的4GB的寻址空间,这个空间完全是程序私有的。
  • 3.DLL程序没有自己的私有空间,它们总是被映射到其它应用程序的地址空间中,当做其它应用程序一起运行。原因很简单,如果它不和其它程序同属一个地址空间,应用程序如何才能调用得到它??

03. 逻辑地址、线性地址(虚拟地址)和物理地址

  1. DOS
  逻辑地址-> (分段机制)-> 物理地址
  
  2. Win32
  逻辑地址-> (分段机制)-> 线性地址(虚拟地址)-> (分页机制)-> 物理地址

  3. 资料搬运
  http://blog.csdn.net/newcong0123/article/details/52792070
  http://blog.sina.com.cn/s/blog_6cc1c52d0100sgqt.html
  http://blog.csdn.net/fanwenjieok/article/details/40084719
  初学时看到这么多地址名字十分混乱,幸亏这几篇博客,请相互对比参考

04. 内存管理及API

  1. 内存管理基础
  win32中的内存管理是分层次的,系统提供了几组层次不同的函数来管理内存,它们是标准内存管理函数堆管理函数虚拟内存管理函数内存映射文件函数
  windows充分利用了80X86处理器保护模式下的线性寻址机制和分页机制,这些机制是win32内存管理的基础。

  • 标准内存管理函数:总是在默认堆中分配和释放内存,这组函数就是常规意义上的内存管理函数。
  • 堆管理函数:堆的主要功能是有效地管理内存和进程的地址空间。win32中,进程可以
    使用的整个地址空间就是一个堆。并且堆的概念又被引申了一步:win32中分两种堆,一种
    进程的默认堆,默认堆只有一个,指的就是可以使用的整个地址空间;
    另一种是动态堆,也称私有堆,一个进程可以随意建立多个私有堆,也可以将它们释放,私有堆全部位于默认堆中。
  • 虚拟内存管理函数:管理虚拟内存,主要用于保留/提交/释放虚拟内存,在虚拟内存
    页上改变保护方式、锁定虚拟内存页,以及查询一个进程的虚拟内存等操作,这是一组位于
    底层的函数。
  • 内存映射文件函数:相对比较独立,它是为了文件操作的方便性而设立的。可以将一个文件直接映射
    到进程的地址空间中,这样可以通过内存指针用读写内存的方法直接存取文件内容。

这里写图片描述

  2. 标准内存管理函数
  2.1. 功能:在进程的默认堆中申请和释放内存块
  2.2. 主要的函数有:

  • 申请:GlobalAlloc
  • 释放:GlobalFree
  • 修改:GlobalReAlloc
  • 锁定:GlobalLock
  • 解锁:GlobalUnlock
  • 丢弃:GlobalDiscard
  • 其它:GlobalFlags、GlobalHandle、GlobalSize

  建议:GlobalAlloc跟LocalAlloc等函数是为兼容而保留的,Win32下两组函数完全相同,都是在底层直接调用HeapAlloc函数从默认堆分配内存,建议使用HeapAlloc函数。

  2.3. 可分配的内存有两种:固定的内存块和可移动的内存块(可进一步定义为可丢弃的内存块)
  可通过在申请时指定标志来创建不同的内存块

  • GMEM_FIXED:固定
  • GMEM_ZEROINIT:0初始化
  • GPTR:GMEM_FIXED or GMEM_ZEROINIT 组合
  • GMEM_MOVEABLE:可移动的
  • GMEM_DISCARDABLE:可丢弃的

  2.4. 调整可移动内存块的大小最好还是先将内存解锁,等调整完毕后再锁定使用

  注意:Win32下以上函数仅仅是为了兼容而存在,系统不保证这些函数在使用上不会出错,而在底层已经不再去做碎片拼合的工作。HeapAlloc等函数不支持可移动内存机制,所以在Win32下要防止内存碎片化,最好的办法还是使用内存池。
  
  3. 堆管理函数
  3.1. Windows的堆分为两种:
  默认堆:由OS自动创建,只有一个,可直接使用
  私有堆:使用前需先创建,可以有多个

  使用私有堆的好处:

  • 1.空间预留,避免同步冲突,增加运行速度
  • 2.某些情况,防止系统频繁在物理内存和交换文件之间进行数据交换,提高系统性能
  • 3.封装和保护模块化程序
  • 4.便于扫尾工作,无需像默认堆一般将内存一块块单独释放

  3.2 主要函数有:

  • 创建私有堆: HeapCreate
  • 释放私有堆: HeapDestroy
  • 在堆中分配内存:HeapAlloc
  • 释放内存: HeapFree
  • 修改内存块大小:HeapReAlloc
  • 获取默认堆:GetProcessHeap
  • 列出进程中所有堆:GetProcessHeap
  • 列出堆中所有内存块:HeapWalk
  • 验证堆的完整性或堆中某内存块的完整性:HeapValidate
  • 锁定堆:HeapLock
  • 解锁:HeapUnlock (这两个函数可用于线程同步,不过一般不采用,而是采用HEAP_NO_SERIALIZE标志)
  • 合并堆中空闲内存并释放不在使用中的内存页:HeapCompact
  • 获取堆中某块内存大小:HeapSize

  注意GetProcessHeap和GetProcessHeap的区分
  
  4. 虚拟内存管理函数
  主要函数:

  • VirtualAlloc 和 VirtualFree :进行地址空间的分配和释放工作
  • VirtualLock 和 VirtualUnlock :对内存页进行锁定和解锁
  • VirtualQuery(Ex):查询内存页状态
  • VirtualProtext(Ex):改变内存页的保护模式