Win32__宽字符

1.字符编码

在win32编程中,最基础的就是字符编码相关知识

ASCII编码

由于计算机设计之初仅仅供美国使用,所以一开始的编码设计只考虑到英文字符,也就是ASCII编码:

image2021-5-17_0-3-42-1759559963275-1

虽然之后又有GB2312的ASCII拓展,但是仍然无法满足全世界的编码需求,中文这样的象形文字更是难以满足。

images/download/attachments/1015833/image2021-5-17_0-7-45.png

想要显示中文,仅仅单字节8bit的表示方式肯定不行,于是最开始尝试使用2个ASCII表值拼接起来表示一个汉字,比如”中”表示为:0xD0 0XD6。

但是,这种编码仍然存在问题,比如不够国际化,如果将编码为D0 D6的“中”字发到国外的电脑上,由于没有相应的编码表,就会显示为两个ASCII码表对应的字符,而不是中文。另外,像日文、韩文这样的象形文字也会采用这样的方式,那么就会出现一个编码对应多种文字的情况。

Unicode编码

为了解决ASCII的缺陷,Unicode编码就诞生了,Unicode编码创建了一张包含世界上绝大部分文字的编码表,只要世界上存在的文字符号,都会赋予一个唯一的编码。

Unicode编码的范围是:0x0-0x10FFFF,其可以容纳100多万个符号,但是Unicode本身也存在问题,因为Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何去存储。

假设中这个字以Unicode方式表示占2个字节,而国这个字却占4个,应该如何储存呢?

UTF-16

UTF-16/UTF-8是Unicode存储的实现方式;UTF-16编码是以16个无符号整数位单位,注意是16位为一个单位,但不表示一个字符就只有16位,具体的要看字符的Unicode编码所在范围,有可能是2字节,有可能是4字节,现在机器上的Unicode编码一般指的就是UTF-16

举个例子(虚构):

中(Unicode编码):0x1234

国(Unicode编码):0x12345

UTF-16存储的时候,“中”这个字肯定是存储的0x1234,但是“国”这个字就不一样, 我们知道UTF-16是16位(2字节)为一个单位,所以国这个字拆下来存储应该是0x0001 0x2345。

UTF-16的优点一看便知:计算、拆分、解析非常方便,2个字节为一个单位,一个一个来。

UTF-16是否是最优解呢?其实不然,我们通过如上的例子中可以看到一个很明显的缺点,那就是UTF-16会存在浪费空间的情况,因为其16位(2字节)为一个单位,它需要字节对齐,例如字母A只需要一个字节就可以表示,而使用UTF-16时就会变成2个字节,所以很浪费,而这时候UTF-8横空出世。(UTF-16在本地存储是没有什么问题的,顶多就是浪费一点硬盘空间,但是如果在网络中传输,那就太过于浪费了)

UTF-8

UTF-8称之为可变长存储方案,其存储根据字符大小来分配,例如字母A就分配一个字节,汉字“中”就分配两个字节。

优点:节省空间;缺点:解析很麻烦。

UTF-8存储的方式是有对应表的:

images/download/attachments/1015833/image2021-5-17_0-46-57.png

例如字母A,在0x000000 - 0x00007F范围之间,则采用0XXXXXX的方式进行存储,也就是按照一个字节的方式来不会改变什么,而汉字”中“则不一样了。

中(Unicode编码):0x4E 0x2D,它属于0x000800 - 0x00FFFF范围之间。

0x4E 0x2D = 0100 1110 0010 1101,其以UTF-8的方式存储就是1110 (0100) 10(11 1000) 10(10 1101),括号包裹起来的就是汉字“中”的Unicode编码。

BOM(Byte Order Mark)

字节顺序标记,其就是用来插入到文本文件起始位置开头的,用于识别Unicode文件的编码类型。对应关系如下:

images/download/attachments/1015833/image2021-5-17_1-1-28.png

2.C语言中的宽字符

本章主要是讲解在C语言中如何使用上一章所述的编码格式表示字符串。

#include <stdio.h>

void main() {
    //ASCII编码
	char str[] = "中国"; 
    d6 d0 b9 fa 00
	//Unicode编码
    wchar_t str1[] = L"中国";//此处加L是为了区别编码类型,如果不加即使是宽字符,也会默认使用ASCII编码
    2d 4e fd 56 00 00
}

image-20251004151928810

ASCII编码和Unicode编码在内存中的存储方式不一样,所以我们使用相关函数的时候也要注意,如下图所示,ASCII编码使用左边的,而Unicode则是右边的(事实上,二者并无太大区别,只是一个以单字节为单位处理字节流,另一个以双字节为单位处理字节流。这也是对于宽字节有时也可以使用左侧部分方法且效果相同的原因):

images/download/attachments/1015833/image2021-5-17_1-12-0.png

例如,在控制台打印一个宽字符的字符串:

#include <stdio.h>
#include <locale.h>

void main() {
	setlocale(LC_ALL, ""); //设置地域
	wchar_t str1[] = L"中国";
	wprintf_s(str1); //如果不设置地域的话,默认只输出英文,对于unicode的中文来说就输出为空
}

或者,输出字符串长度

#include <stdio.h>
#include <locale.h>

void main() {
	setlocale(LC_ALL, "");
	char str[] = "中国";
	wchar_t str1[] = L"中国";
	int n1 = wcslen(str1);
	int n2 = strlen(str);
	printf_s("%d %d", n1, n2);//2 4
}

复制字符串

#include <stdio.h>
#include <locale.h>

void main() {
	setlocale(LC_ALL, "");
	wchar_t str[3];
	wchar_t str1[] = L"中国";
	wcscpy(str, str1);
	wprintf(str);
}

字符串拼接

#include <stdio.h>
#include <locale.h>

void main() {
	setlocale(LC_ALL, "");
	wchar_t str[] = L"人";
	wchar_t str1[4] = L"中国";
	wcscat(str1, str);
	wprintf(str1);
}

字符串比较

#include <stdio.h>
#include <locale.h>

void main() {
	setlocale(LC_ALL, "");
	wchar_t str[] = L"中国";
	wchar_t str1[4] = L"中国人";
	printf("%d", wcscmp(str, str1));
	wprintf(L"%d", wcscmp(str, str1));//不加L会乱码
}

3.Win32API

Win32 API就是Windows操作系统提供给我们的函数(应用程序接口),其主要存放在C:\Windows\System32(存储的DLL是64位)、C:\Windows\SysWOW64(存储的DLL是32位)[没错,虽然名字上会让人产生疑惑,但的确如此] 下面的所有DLL文件(几千个)。

重要的DLL文件:

  1. Kernel32.dll:最核心的功能模块,例如内存管理、进程线程相关的函数等;
  2. User32.dll:Windows用户界面相关的应用程序接口,例如创建窗口、发送信息等;
  3. GDI32.dll:全称是Graphical Device Interface(图形设备接口),包含用于画图和显示文本的函数。

在C语言中我们想要使用Win32 API的话直接在代码中包含windows.h这个头文件即可。

Win32中的宽字节

以字符为例

| 字符类型 | 字符串指针 | 字符数组赋值 | 字符串指针赋值 | 字符类型 | | :----------- | ----------- | ------------------- | -------------------- | | | char == CHAR | PSTR(LPSTR) | CHAR cha[] = “中国” | PSTR pcha = “chaina” |多字节字符| | wchar_t == WCHAR | PWSTR(LPWSTR) | WCHAR cha[] = L”中国” | PWSTR pcha = L”china” |宽字节字符| | TCHAR | PTSTR(LPTSTR) | TCHAR cha[] = TEXT(“中国”) | PTSTR pcha = TEXT(“china”) |宏兼容|

事实上,Windows完全是由C语言编写的,所以,任何看上去”新”的数据结构都本质是C语言数据结构的宏定义出来的。比如LPCTSTR如果跟进头文件,发现其实本质就是const char*类型。一法通,万法通,之后碰见宏定义的数据结构只需要跟进头文件就知道其本质类型了。

以之前了解过的MessageBox为例,事实上,并没有一个名为MessageBox的函数,如果跟进,会发现,其定义为:

#ifdef UNICODE
#define MessageBox  MessageBoxW
#else
#define MessageBox  MessageBoxA
#endif // !UNICODE

真正存在的是MessageBoxA(ASCII编码使用)和MessageBoxW(Unicode编码使用),之所以进行宏定义,是为了兼容性,会根据不同的编码环境调用不同函数,但是在开发的过程中可以减少不必要的麻烦。

包括之前的宏兼容字符类型也是,在开发的过程中,直接使用就避免了兼容问题。

4.Win32的入口程序

以下是MSDN文档的信息:

int __clrcall WinMain(
  [in]           HINSTANCE hInstance,
  [in, optional] HINSTANCE hPrevInstance,
  [in]           LPSTR     lpCmdLine,
  [in]           int       nShowCmd
);

参数

[in] hInstance

类型:HINSTANCE

应用程序的当前实例的句柄(地址)。

[in, optional] hPrevInstance

类型:HINSTANCE

应用程序的上一个实例的句柄。 此参数始终 NULL。 如果需要检测另一个实例是否已存在,请使用 CreateMutex 函数创建唯一命名的互斥体。 即使互斥体已存在,CreateMutex 也会成功,但该函数将返回 ERROR_ALREADY_EXISTS。 这表示应用程序的另一个实例存在,因为它首先创建了互斥体。 但是,恶意用户可以在执行此操作之前创建此互斥体,并阻止应用程序启动。 为防止这种情况,请创建一个随机命名的互斥体并存储该名称,以便它只能由授权用户获取。 或者,可以将文件用于此目的。 若要将应用程序限制为每个用户的一个实例,请在用户的配置文件目录中创建锁定的文件。

[in] lpCmdLine

类型:LPSTR

应用程序的命令行,不包括程序名称。 若要检索整个命令行,请使用 GetCommandLine 函数。说白了,允许在命令行启动时向程序传入一段字符串,仅此而已,可以用于判断是谁调用的程序。

[in] nShowCmd

类型:int

控制窗口的显示方式。 此参数可以是 ShowWindow 函数的 nCmdShow 参数中指定的任何值。

返回值

类型:int

如果函数成功,在收到 WM_QUIT 消息时终止,它应返回该消息 wParam 参数中包含的退出值。 如果函数在输入消息循环之前终止,则应返回零。

查看WinBase.h头文件,发现其定义为:

#pragma region Desktop Family
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)

int
#if !defined(_MAC)
#if defined(_M_CEE_PURE)
__clrcall
#else
WINAPI
#endif
#else
CALLBACK
#endif
WinMain (
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd
    );

int
#if defined(_M_CEE_PURE)
__clrcall
#else
WINAPI
#endif
wWinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR lpCmdLine,
    _In_ int nShowCmd
    );

#endif /* WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) */
#pragma endregion

这段宏定义的意思是,当平台为原生环境(Native)时使用__stdcall(WINAPI)调用方式,当平台为CLR托管平台(比如.NET)时使用__clrcall,这是因为CLR托管平台不支持__stdcall调用约定。至于原生C++和托管C++的区别,对于Win32学习来说不是必要的,暂且在这里按下不表。

5.在Win32程序中Debug

下面以我写的Win32程序为例:

#include <stdio.h>
#include <locale.h>

#include <Windows.h>
int WINAPI WinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nShowCmd) {
    OutputDebugString(TEXT("hello"));
    return MessageBox(NULL, TEXT("hello, world"), TEXT("caption"), 0);
}

这里的MessageBox不多说了,着重说关于OutputDebugString,这个其实类似于printf,但是无法像printf那样使用占位符,并且不显示在控制台窗口中,而且自身也不创建窗口,只会显示在debug窗口中。在这里可以对这个函数进行一些改变,使其可以使用占位符进行操作。

Tools.h

#pragma once
void __cdecl OutputDebugStringF(const TCHAR* format, ...);

// 宏定义部分,实现条件编译
#ifdef _DEBUG
#define DbgPrintF OutputDebugStringF // 在 Debug 版本中,启用调试输出
#else
#define DbgPrintF 0 && (void) // 在 Release 版本中,彻底移除代码,并确保编译安全
#endif

Tools.cpp

#include "StdAfx.h"
#include "Tools.h"

// 实现格式化输出函数


// 通用调试输出函数,支持 Unicode / ANSI
void __cdecl OutputDebugStringF(const TCHAR* format, ...)
{
    va_list vlArgs;

    // 分配 4KB 缓冲区
    TCHAR* strBuffer = (TCHAR*)GlobalAlloc(GPTR, 4096 * sizeof(TCHAR));
    if (!strBuffer) return;

    // 格式化字符串
    va_start(vlArgs, format);
    _vstprintf_s(strBuffer, 4096, format, vlArgs);
    va_end(vlArgs);

    // 追加换行符
    _tcscat_s(strBuffer, 4096, _T("\n"));

#ifdef UNICODE
    OutputDebugStringW(strBuffer);
#else
    OutputDebugStringA(strBuffer);
#endif

    GlobalFree(strBuffer);
}

更改过后的WinMain函数:

#include "StdAfx.h"
#include "Tools.h"
int WINAPI WinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nShowCmd) {
    int x = 1;
    DbgPrintF("hello %d", x);
    return MessageBox(NULL, TEXT("hello, world"), TEXT("caption"), 0);
}

GetLastError

字面意思,就是获取Win32API中上一个出现的错误,并返回错误代码,由这个错误代码在MSDN中就可以找到错误原因。和Dbgprintf结合起来进行调试很有用。

Win32__事件_消息_消息函数

1.事件 消息

Windows中的事件是一个”动作”,这个动作可能是用户操作应用程序产生的,也可能是Windows自己产生的。而消息则是用于描述这些”动作”的,比如,一个动作是什么时候产生的,由哪个应用程序产生的,在什么位置产生的

Windows为了能够准确描述这些信息,提供了一个结构体MSG

typedef struct tagMSG {
    HWND        hwnd;
    UINT        message;
    WPARAM      wParam;
    LPARAM      lParam;
    DWORD       time;
    POINT       pt;
#ifdef _MAC
    DWORD       lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

以下是根据MSDN总结的各个成员的数据类型以及含义:

成员名数据类型含义
hwndHWND (Handle to Window) HWND (窗口句柄)接收该消息的窗口句柄。它指明了消息的目标窗口。
messageUINT (Unsigned Integer) UINT (无符号整数)消息的标识符。这是一个唯一的数值,指示了消息的类型(例如:WM_LBUTTONDOWN 表示鼠标左键按下,WM_PAINT 表示需要重绘窗口)。
wParamWPARAM (Word Parameter) WPARAM (字参数)消息的第一个参数。其具体含义取决于 message 的类型,通常包含一些较小的、额外的数据(如按下的键码、鼠标键状态等)。
lParamLPARAM (Long Parameter) LPARAM (长参数)消息的第二个参数。其具体含义也取决于 message 的类型,通常包含一些较大的、更复杂的数据(如鼠标光标的 x 和 y 坐标,或者一个指向结构体的指针)。
timeDWORD (Double Word) DWORD (双词)消息被投递到消息队列时的时间(通常是自系统启动以来的毫秒数)。
ptPOINT 结构体消息被发布时鼠标光标的屏幕坐标(包含 x 和 y 坐标)。
lPrivateDWORD仅用于 macOS 平台 (#ifdef _MAC),是私有数据。在标准的 Windows/Win32/UWP 编程中通常忽略。

Windows的消息类型可以分为两类,一种是微软定义的消息类型,我在网络上并没有查找到具体的信息,只是说粗略估计有几百种,并且随着后续的更新还会增加。另一种是微软允许的应用程序自定义的消息类型。

2.一个完整的消息流程

系统消息队列与应用程序消息队列:

image-20251006074337683

注:此处的”队列”指的就是先进先出的队列数据结构。

用户输入视为一个”事件”,消息对事件进行描述,表现为系统将事件相关信息储存到MSG结构体中,消息进入系统队列,系统根据句柄的不同将其分配到不同窗口的应用程序队列,让后应用程序循环调用MSG,是应用程序需要处理的消息就进行处理,比如记事本记事时鼠标点击”关闭文件”,如果不是应用程序关心的消息就交由Windows处理,比如鼠标拖动记事本窗口。

3.创建一个图形化窗口

创建一个图形化窗口在Win32中本质是创建一个WNDCLASS类。以下是WinUser.h中关于WNDCLASS的定义:

typedef struct tagWNDCLASSW {
    UINT        style;
    WNDPROC     lpfnWndProc;	//消息处理函数,也就是上述进行消息循环并判断的函数
    int         cbClsExtra;
    int         cbWndExtra;
    HINSTANCE   hInstance;		//窗口的归属,属于哪一个应用程序
    HICON       hIcon;			//图片标识
    HCURSOR     hCursor;		//鼠标样式
    HBRUSH      hbrBackground;	//窗口背景色
    LPCWSTR     lpszMenuName;	//菜单名字
    LPCWSTR     lpszClassName; 	//窗口名字
} WNDCLASSW, *PWNDCLASSW, NEAR *NPWNDCLASSW, FAR *LPWNDCLASSW;

以下是我写的WinMain函数:

#include "StdAfx.h"
#include "Tools.h"
LRESULT WindowsProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine, _In_ int nShowCmd) {
    //创建窗口类及其对象
    TCHAR className[] = TEXT("MyFirstWindow");
    WNDCLASS wndClass = {0};  //将所有成员赋值
    wndClass.lpszClassName = className;
    wndClass.lpfnWndProc = WindowsProc;
    wndClass.hInstance = hInstance;
    wndClass.hbrBackground = (HBRUSH)COLOR_MENU;

    //1.注册窗口类
    RegisterClass(&wndClass); //这个函数要求wndClass成员全部赋值

    //2.创建窗口
    HWND hwnd = CreateWindow(
        className,                      //类名
        TEXT("我的第一个窗口"),          //窗口标题
        WS_OVERLAPPEDWINDOW,            // 窗口外观样式
        10,// 相对于父窗口的×坐标
        10,// 相对于交窗口的称
        600,//窗口的宽度
        300,//窗口的高度
        NULL,// 父窗口司病,为NULL
        NULL,// 莱单句柄,为NULL
        hInstance,//当前应用程序的句柄
        NULL);
    
    //检查是否创建成功
    if (hwnd == NULL) return 0;

    //显示窗口
    ShowWindow(hwnd, SW_SHOW);

    //3.消息循环
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) { //这里的msg是返回参数

        TranslateMessage(&msg);     //翻译加工消息
        DispatchMessage(&msg);      //分发消息到系统,系统调用消息处理函数WindowsProc
    }

    return 0;
}

//4.消息处理
LRESULT WindowsProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

    switch (uMsg)
    {
    case WM_CREATE:
        MessageBoxW(hWnd, L"窗口创建了", L"提示", MB_OK);
        break;
    case WM_CLOSE:
        MessageBoxW(hWnd, L"窗口关闭了", L"提示", MB_OK);
        DestroyWindow(hWnd);
        PostQuitMessage(0);//向系统请求线程已终止(退出)
        break;
    }//这里只是举两个例子,如果需要查询更多的消息及其信息可以查询在线MSDN文档。
    return DefWindowProc(hWnd, uMsg, wParam, lParam); //除去获取的消息,其他都交由系统处理
}

其实一个Win32程序必备的三个要点就是

  1. 窗口类实例化并注册窗口类
  2. 创建窗口
  3. 消息循环&&消息处理

Win32__ESP寻址_定位回调函数

1.Win32应用程序入口识别

Win32程序由WinMainCRTStartup函数调用win32.__scrt_common_main函数再调用WinMain,于是识别Win32应用程序入口就很简单,只需要找到win32.__scrt_common_main函数,然后找到传入4个参数,并调用的函数就是WinMain了。

image-20251007160911751

如果是在动态调试器中识别原理同上,只不过需要注意__stdcall调用约定的特点,参数从右至左压栈,像是压入四个栈,最后一个压一个特定地址(句柄)入栈的就很有可能下一步调用WinMain,又由于__stdcall是内平栈,可以跟进函数看看是否retn 0x10。

或者,可以在x64dbg的命令行中直接根据名字下断点:

bp Main

2.ESP寻址特点

在讲ESP寻址之前,先理解EBP寻址:

; 函数开始
push ebp        ; 1. 保存前一个函数的 EBP (堆栈帧链接)
mov ebp, esp    ; 2. EBP 指向当前 ESP,建立新的堆栈帧
sub esp, X      ; 3. 为局部变量分配空间 (X 字节)

; ... 访问参数:[EBP + 8], [EBP + 12] ...
; ... 访问变量:[EBP - 4], [EBP - 8] ...

; 函数结束
mov esp, ebp    ; 4. 释放所有局部变量空间 (将 ESP 恢复到 EBP 处)
pop ebp         ; 5. 恢复前一个函数的 EBP
ret             ; 6. 返回调用者

EBP寻址的特点是EBP指针始终不变,正偏移量访问函数参数,负偏移量访问局部变量

在 64 位 (x64) 编程中,由于寄存器数量增加,并且出于性能考虑,EBP 寻址经常被禁用或用作通用寄存器(启用“省略帧指针”优化)。

转而使用ESP寻址,ESP寻址不需要执行push ebp / mov ebp, esp等操作,直接执行

sub esp 0x...

分配空间,通过正偏移量访问参数和变量,但是这样寻址会有点麻烦,因为push/pop的操作会导致ESP”上移”,所以每当堆栈变化的时候,就需要略微更改寻址偏移,但这个不需要人工操作,其实无伤大雅。

特性EBP 寻址 (Base Pointer) EBP 寻址(基指针)ESP 寻址 (Stack Pointer) ESP 寻址 (栈指针)
主要功能稳定地访问参数和局部变量(创建堆栈帧)。动态地管理堆栈顶部,用于 push/pop 等操作。
稳定性和用途稳定。用于建立调试信息和保证函数调用一致性。不稳定。用于临时操作,其值会随堆栈操作变化。
访问方向参数用正偏移;局部变量用负偏移局部变量和数据通常用正偏移(相对于当前栈顶)。
使用现状x86 传统标准。在 x64 中常被优化掉(省略帧指针)。x64 现代标准。是 x64 堆栈管理的主要方式。
x64 名称RBP (64位) RBP (64 位)RSP (64位) RSP (64 位)

3.窗口回调函数定位

wndclass.lpfnWndProc定义了回调函数的名字,然后又使用RegisterClass()注册了这个回调函数,所以我们现在需要去找RegisterClass() 1706234279080-7dda44f9-ddb5-473e-8d45-348fe5ee47a8 可以看到在调用RegisterClass函数时push了eax进去,说明eax是传递进去的参数RegisterClass(&wndclass);`这是注册窗口类的代码,可以看出eax就是这个&wndclass的结构体指针,我们进堆栈看看这个结构体指针,根据wndclass结构体的结构可以知道第二个就是回调函数的地址。

4.具体事件处理的定位(条件断点)

断点下到回调函数地址第一行 image.png 由于这个地方是一直在循环很多个消息id的,所以断点在这会触发很多个事件,所以我们需要给断点设置个条件,让他只关注点击左键时。至于为什么是esp+8,其实就是回调函数传入的第二个参数,一个参数占四字节,仅此而已。 image.png 所以我们点右键点空格他都会执行,不会断,当我们点左键时他就断点了 image.png 断点后直接再执行几行代码就到了点击左键时的代码了 image.png

5.课后作业

找出哪些事件会被WndProc函数处理。

课后作业链接

关键在于找到回调函数,按上面的步骤走就行,回调函数如下:

image-20251007175939965

大概可以确定的是按下“A”,“F”,“g”和左键右键以及窗口销毁消息会被处理,其余交由Windows处理。

使用IDA查看反汇编也可以印证答案:

LRESULT __stdcall sub_4010F0(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
  int n65; // [esp-4h] [ebp-28h]
  CHAR Text[32]; // [esp+4h] [ebp-20h] BYREF

  if ( Msg > 0x201 )
  {
    if ( Msg == 516 )
    {
      MessageBoxA(0, a2, a2, 0);                // "2"
      return 0;
    }
    return DefWindowProcA(hWnd, Msg, wParam, lParam);
  }
  if ( Msg == 513 )
  {
    MessageBoxA(0, a1, a1, 0);                  // "1"
    return 0;
  }
  if ( Msg == 2 )
  {
    PostQuitMessage(0);
    return 0;
  }
  if ( Msg != 256 )
    return DefWindowProcA(hWnd, Msg, wParam, lParam);
  switch ( wParam )
  {
    case 'A':
      n65 = 65;
      memset(Text, 0, sizeof(Text));
      break;
    case 'F':
      n65 = 70;
      memset(Text, 0, sizeof(Text));
      break;
    case 'g':
      n65 = 103;
      memset(Text, 0, sizeof(Text));
      break;
    default:
      MessageBoxA(0, ::Text, Caption, 0);       // "ERROR"
      return 0;
  }
  sprintf(Text, "%d\n", n65);
  MessageBoxA(0, Text, Text, 0);
  return 0;
}

Win32__子窗口_消息处理函数

什么都是窗口

1.按钮的本质

void CreateButton(HWND hwnd, HINSTANCE hAppInstance)
{
    HWND hwndPushButton;
    HWND hwndCheckBox;
    HWND hwndRadio;

    hwndPushButton = CreateWindow(
        TEXT("button"),		
        TEXT("普通按钮"),
        //WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_DEFPUSHBUTTON,							
        WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_DEFPUSHBUTTON,
        10, 10,
        80, 20,
        hwnd,               //父窗口句柄
        (HMENU)1001,		//子窗口ID					
        hAppInstance,		//虽然很多时候子窗口的hInstance参数并不会被严格使用(因为父窗口的实例句柄已足够),但如果处理不当这是一个潜在的隐患。
        NULL);

    hwndCheckBox = CreateWindow(
        TEXT("button"),
        TEXT("复选框"),
        //WS_CHILD | WS_VISIBLE | BS_CHECKBOX | BS_AUTOCHECKBOX,							
        WS_CHILD | WS_VISIBLE | BS_CHECKBOX | BS_AUTOCHECKBOX,
        10, 40,
        80, 20,
        hwnd,
        (HMENU)1002,		//子窗口ID					
        hAppInstance,
        NULL);

    hwndRadio = CreateWindow(
        TEXT("button"),
        TEXT("单选按钮"),
        //WS_CHILD | WS_VISIBLE | BS_RADIOBUTTON | BS_AUTORADIOBUTTON,							
        WS_CHILD | WS_VISIBLE | BS_RADIOBUTTON,
        10, 70,
        80, 20,
        hwnd,
        (HMENU)1003,		//子窗口ID					
        hAppInstance,
        NULL);
}

事实上,按钮本质也是窗口。

2.按钮事件的处理

回顾创建按钮时的CreateWindow函数参数:

hwndPushButton = CreateWindow(
        TEXT("button"),		
        TEXT("普通按钮"),
		...
    }

这里直接写了一个类名,但是并没有人为定义一个按钮类。这是因为按钮的WNDCLASS不是我们定义的,是系统预定义好的。

利用OutputDebugStringF在调试窗口查看:

TCHAR szBuffer[0x20];
GetClassName(hwndPushButton, szBuffer, 0x20);

WNDCLASS wc;
GetClassInfo(hAppInstance, szBuffer, &wc);
OutputDebugStringF(TEXT("-->%s\n"), szBuffer);
OutputDebugStringF(TEXT("-->%x\n"), wc.lpfnWndProc);

发现:

image-20251010202657635

按钮类实际上是系统定义好的名为Button的窗口类。

如果在主窗口的回调函数中获取鼠标左键点击事件,我们会发现,在按钮的窗口中点击左键是没有用的,那么怎么处理按钮窗口中的事件呢?事实上,子窗口和父窗口的回调函数是分开的且类似按钮的系统子窗口的回调函数是系统写好的。

image-20251010203038188

所以,如果要在父窗口的回调函数中获取按钮类的事件,需要先获取WM_COMMAND。

case WM_COMMAND:
{
    switch (LOWORD(wParam))
    {
    case 1001:		//子窗口菜单,也就是编号				
        MessageBox(hWnd, TEXT("Hello Button 1"), TEXT("Demo"), MB_OK);
        return 0;
    case 1002:
        MessageBox(hWnd, TEXT("Hello Button 2"), TEXT("Demo"), MB_OK);
        return 0;
    case 1003:
        MessageBox(hWnd, TEXT("Hello Button 3"), TEXT("Demo"), MB_OK);
        return 0;
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}				

顺带一提,如果是自定义的子窗口,系统不会获取子窗口消息并向父窗口发送WM_COMMAND,需要像主窗口那样,定义窗口类,定义回调函数,注册类。

比如:

WNDCLASS wc = { 0 };
wc.lpfnWndProc = MyChildProc;
wc.hInstance = hInstance;
wc.lpszClassName = TEXT("MyChildClass");
RegisterClass(&wc);
HWND hwndChild = CreateWindow(
    TEXT("MyChildClass"), TEXT("子窗口"),
    WS_CHILD | WS_VISIBLE | WS_BORDER,
    50, 50, 200, 100,
    hwndParent, (HMENU)2001, hInstance, NULL);

LRESULT CALLBACK MyChildProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_LBUTTONDOWN:
    {
        MessageBox(hWnd, TEXT("Hello Child 1"), TEXT("Demo"), MB_OK);
    }
    break;
    }
    return DefWindowProc(hWnd, msg, wParam, lParam);
}

当然,也可以宏定义子窗口消息,并将消息发送至主窗口处理。

#define WM_CHILD_NOTIFY (WM_USER + 100)
HWND hwndParent = GetParent(hwnd);
OutputDebugStringF(_T("[Child] 点击触发 -> 向父窗口发送 WM_CHILD_NOTIFY"));
SendMessage(hwndParent, WM_CHILD_NOTIFY, (WPARAM)hwnd, 1);

3.消息堆栈

回顾回调函数的结构:

LRESULT CALLBACK WindowProc(  			
	IN  HWND hwnd,  		
	IN  UINT uMsg,     //消息代码,比如WM_COMMAND是0x111
	IN  WPARAM wParam,  		
	IN  LPARAM lParam  		
	);  		

那么,由此可知在调用回调函数时的堆栈状况:

image-20251010212017645

4.按钮事件处理逻辑定位

其实很简单了,只需要在消息接收处下条件断点:

[esp+0x8] == WM_COMMAND && [esp+0xC] == 按钮id

5.课后作业

[课后作业](/public/stduy_note/win32/Win32 子窗口_消息处理函数.rar)

找到按钮的消息处理函数,看看这个消息处理函数除了三个MessageBox还做了哪些事情。

按照上节课的知识定位回调函数,发现: image-20251010214050601

其实就是多做了一个如果接收WM_DESTROY消息就摧毁进程的事。

Win32__资源文件_消息断点

1.资源文件、创建对话框

在VisualStudio中创建一个项目,可以通过创建.rc资源文件和resource.h来管理一些控件。大致流程是

[ resource.h ]        ← 定义 ID 宏

[ MenuDemo.rc ]       ← 使用这些 ID 定义菜单、图标资源

[ MenuDemo.cpp ]      ← 使用 LoadMenu / LoadIcon + 这些 ID 加载资源

在之前的程序中,如果想创建一个窗口,需要实例化class->注册class->CreateWindow->提供消息处理函数->消息循环。而如果使用.rc资源文件管理资源,资源文件会帮我们完成实例化class,注册class和消息循环的工作。(其实严格上来讲,只有dialog资源会自动注册,其他的仍需手动注册)

项目使用 .rc 资源文件不使用 .rc 文件(纯代码)
窗口类图标、菜单在资源编辑器中添加图标/菜单,然后在 .rc 中定义并通过 LoadIcon()LoadMenu() 加载需要自己写代码调用 CreateMenu()AppendMenu()LoadImage() 等函数手动创建
对话框、按钮、控件可以用可视化编辑器拖拽控件(生成 .rc 文件),然后用 DialogBox() 加载需要用 CreateWindow()CreateWindowEx() 手动创建每个控件
字符串 / 文本资源放在 .rcSTRINGTABLE 中,方便多语言支持文本全部硬编码在源码中,修改麻烦
资源 ID 管理统一集中在 resource.h 自动编号、自动维护自己定义一堆 #define ID_XXX,容易重复或冲突
项目结构代码简洁,资源独立,便于修改所有资源和界面逻辑混在一起,可读性差
运行速度一样(最终编译结果相同)一样(都是编译到 .exe 里)

对于dialog资源来说,创建只需要DialogBox()并提供回调函数即可。

void DialogBoxW(
  [in, optional] hInstance,
  [in]           lpTemplate,
  [in, optional] hWndParent,
  [in, optional] lpDialogFunc
);
[in, optional] hInstance

类型:HINSTANCE

包含对话框模板的模块句柄。 如果此参数为 NULL,则使用当前可执行文件。

[in] lpTemplate

类型:LPCTSTR

对话框模板。 此参数是指向以 null 结尾的字符串的指针,该字符串指定对话框模板的名称或指定对话框模板的资源标识符的整数值。 如果参数指定资源标识符,则其高序单词必须为零,其低序单词必须包含标识符。 可以使用 MAKEINTRESOURCE 宏创建此值。

[in, optional] hWndParent

类型:HWND

拥有对话框的窗口的句柄。

[in, optional] lpDialogFunc

类型:DLGPROC

指向对话框过程的指针。 有关对话框过程的详细信息,请参阅 DialogProc

以下是一个示例:

// Win32_test2.cpp : 定义应用程序的入口点。
//

#include "framework.h"
#include "Win32_test2.h"
#include "Tools.h"

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{

	DialogBox(hInstance,MAKEINTRESOURCE(IDD_DIALOG1),NULL,DialogProc);
	return 0;
}

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	OutputDebugStringF(TEXT("uMsg: 0x%X"), uMsg);

	switch (uMsg)
	{
	case  WM_INITDIALOG:

		MessageBox(hwndDlg, TEXT("WM_INITDIALOG"), TEXT("INIT"), MB_OK);

		return TRUE;

	case  WM_COMMAND:

		switch (LOWORD(wParam))
		{
		case   IDOK:

			MessageBox(NULL, TEXT("IDC_BUTTON_OK"), TEXT("OK"), MB_OK);

			return TRUE;

		case   IDCANCEL:

			MessageBox(NULL, TEXT("IDC_BUTTON_OUT"), TEXT("OUT"), MB_OK);

			EndDialog(hwndDlg, 0);

			return TRUE;
		}
		break;

	}

	
	return FALSE;
}

2.获取文本框内容

当我们在对话框中创建一个文本框控件,如果我们需要获取文本框中的内容,需要:

//1.获取文本框窗口句柄
HWND hEditUser = GetDlgItem(父窗口句柄,控件ID);
//2.通过文本框句柄获取里面的内容
TCHAR szUserBuff[0x50];
GetWindowText(文本框句柄,szUserBuff,0x50);

3.定位对话框回调函数

当程序比较简单时,之前的定位回调函数方法还算迅速。但是,如果碰到复杂的程序,或者对导出表做了混淆的程序,就难以定位回调函数。这时,我们可以使用消息断点。以下以之前写的程序为例。

  1. 当我们运行程序到出现对话框后,查看窗口信息,x64dbg会将Windows程序的窗口过程(实际上就是回调函数地址)及句柄放在”句柄“视图中

    image-20251015192506452

    这样我们就可以在视图中对Button类窗口的回调函数下消息断点。

  2. 这样一来,当我们点击完”确认“消息按钮的时候,就会在Button类窗口的回调函数处停止,但这并不是我们的对话框的回调函数。还记得之前讲过的按钮回调函数处理机制吗:

    image-20251010203038188

    此时停止的地方,实际上是”系统提供Winproc“,如何转到我们的对话框回调函数呢?从上图可以知道,”系统提供Winproc“最终会调用”父窗口的Winproc“,所以一步步跟进,总会调用对话框Winproc的。但是这样效率极低,我们可以在主程序的代码段中下内存访问断点,这样,一旦调用到主程序的对话框Winproc,就会暂停到对话框回调函数

    image-20251015194736765

4.课后作业

[课后作业](/public/stduy_note/win32/Win32 资源文件_消息断点.rar)

  1. 对于ReverseTraining_3.exe,需要找到对话框回调函数,并查看三个按钮都做了什么工作。
  2. 对于ReverseTraining_4.exe,需要找到正确的账号密码,并成功登录。

作业1:根据上述定位回调函数的方法,找到回调函数,发现点击对话框和三个按钮分别会弹出MessageBox,仅此而已。

image-20251016101322627

作业2:回调函数如下 image-20251016111628240

设条件断点[esp+8] == 0x0111 && ([esp+C] & 0xFFFF) == 0x3EA,因为根据之前的调试发现登录按钮的编码为0x3EA。不难发现reversetraining_4.401000函数就是对比函数,跟进看看:

image-20251016120919915

其实这段代码仅仅是判断账号长度是否为3,密码长度是否为5,是则返回1,否则返回0。也就是说,只要输入的账号长度为3,密码长度为5就可以成功登录。

image-20251016121154066

Win32__提取图标_修改标题

1.在程序中使用图标

首先在资源文件中导入.ico图标

//1.加载图标
HICON hBigIcon;
HICON hSmallIcon;
hBigIcon = LoadIcon(hAppInstance, MAKEINTRESOURCE(IDI_BIGICON));
hSmallIcon = LoadIcon(hAppInstance, MAKEINTRESOURCE(IDI_SMALLICON));

//2.设置图标
SendMessage(hwndDlg, WM_SETICON, ICON_BIG, (LPARAM)hBigIcon);
SendMessage(hwndDlg, WM_SETICON, ICON_SMALL, (LPARAM)hSmallIcon);

关于SendMessage函数:

LRESULT SendMessage(
  [in] HWND   hWnd,
  [in] UINT   Msg,
  [in] WPARAM wParam,
  [in] LPARAM lParam
);
[in] hWnd

类型:HWND

窗口的句柄,其窗口过程将接收消息。 如果此参数 HWND_BROADCAST ( (HWND) 0xffff) ,则消息将发送到系统中的所有顶级窗口,包括禁用或不可见的无所有者窗口、重叠窗口和弹出窗口;但消息不会发送到子窗口。

消息发送受 UIPI 约束。 进程线程只能将消息发送到完整性级别较低或相等进程的线程的消息队列。

[in] Msg

类型: UINT

要发送的消息。

有关系统提供的消息的列表,请参阅 系统定义的消息

[in] wParam

类型:WPARAM

其他的消息特定信息。

[in] lParam

类型:LPARAM

其他的消息特定信息。

此处SendMessage函数的作用就是将图标及句柄以WM_SETICON传给对话框的回调函数。

2.资源表

用ResourceHacker打开我们的测试程序,查看其中的资源: image-20251016184755640

需要说明的一些事情:

  1. Icon图标资源会根据不同分辨率在编译的时候创建一个资源的多个分辨率版本,比如这个测试程序中只创建了两个资源,但是最终的.exe中存在18个图标资源。

  2. Icon Group资源存放着资源的信息

    image-20251016185117121

尽管现在的ResourceHacker可以直接帮我们按照一定格式进行导出资源,使得资源是完整的,但我们仍需知道资源存放在哪,如何导出二进制,以及如何修复为正常资源。

image-20251016185503290

想要知道怎么提取资源,就要知道如何定位资源文件存储位置。

资源目录:

typedef struct _IMAGE_RESOURCE_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    WORD    NumberOfNamedEntries;
    WORD    NumberOfIdEntries;
//  IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

资源目录项:

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
    //这个union告诉我们资源的名字或者ID(第一层中表示类型,第二层中表示编号,第三层是语言标识)
    union {
        struct {
            DWORD NameOffset:31;
            DWORD NameIsString:1;
        };//告诉我们资源以ID还是名字储存
        DWORD   Name;
        WORD    Id;
    };
    //这个union告诉我们资源的下一层级在哪
    union {
        DWORD   OffsetToData;
        struct {
            DWORD   OffsetToDirectory:31;
            DWORD   DataIsDirectory:1;
        };
    };
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

数据项:

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD OffsetToData;   // RVA,指向资源数据
    DWORD Size;           // 资源数据大小
    DWORD CodePage;
    DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

image-20251020190040293

特别说明:

  1. 资源目录项第一个union中当最高位是1时,低31位是一个UNICODE指针,指向一个结构:

    typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
        WORD    Length;
        WCHAR   NameString[ 1 ];
    } IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

    注:这里的NameString并不以\0\0结尾,判断时依据Length成员的值。

    • 当最高位是0时,表示字段的值作为 ID 使用

    • 如何判断第一位的值?

      printf(“%x\n”,(pResourceEntry[i].Name & 0x80000000) == 0x80000000);

      printf(“%x\n”,pResourceEntry[i].NameIsString == 1);

  2. 资源目录项第二个union中OffsetToData的含义:

    • 最高位如果为1,低31位 + 资源地址 == 下一层目录节点的起始位置

      第一层、第二层全为1.

    • 最高位如果为0,指向 IMAGE_RESOURCE_DATA_ENTRY

      第三层为0

3.课后作业

提取PE文件的资源文件

这里附上我的一个练手小项目PETools

Win__通用控件_WM_NOTIFY

1.标准控件与通用控件

Windows标准控件,标准控件总是可用的:

Static

Group Box

Button

Check Box

Radio Button

Edit

ComboBox

ListBox

Windows通用控件,代码包含在Comctrl32.dll

使用前:

#include <commctrl.h>

#pragma comment(lib,“comctl32.lib”)

Animation

ComboBoxEx

Date_and_Time_Picker

Drag_List_Box

Flat_Scroll_Bar

Header

HotKey

ImageList

IPAddress

List_View

Month_Calendar

Pager

Progress_Bar

Property_Sheets

Rebar

Status Bars

SysLink

Tab

Toolbar

ToolTip

Trackbar

TreeView

Up_and_Down

特别说明:

通用控件在使用前,需要通过INITCOMMONCONTROLSEX进行初始化

只要在您的程序中的任意地方引用了该函数就、会使得WINDOWS的程序加载器PE Loader加载该库

INITCOMMONCONTROLSEX icex;

icex.dwSize = sizeof(INITCOMMONCONTROLSEX);

icex.dwICC = ICC_WIN95_CLASSES;

InitCommonControlsEx(&icex);

2.ListView的使用

  • 初始化列表使用

    LV_COLUMN lv;
    HWND hListProcess;
    
    //初始化
    memset(&lv,0,sizeof(LV_COLUMN));
    //获取IDC_LIST_PROCESS句柄
    hListProcess = GetDlgItem(hDlg,IDC_LIST_PROCESS);
    //设置整行选中
    SendMessage(hListProcess,LVM_SETEXTENDEDLISTVIEWSTYLE,LVS_EX_FULLROWSELECT,LVS_EX_FULLROWSELECT);
    
    //第一列
    lv.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_SUBITEM;
    lv.pszText = TEXT("进程");
    lv.cx = 200;
    lv.iSubItem = 0;
    //ListView_InsertColumn(hListProcess, 0, &lv);
    SendMessage(hListProcess,LVM_INSERTCOLUMN,0,(LPARAM)&lv);
    //第二列
    lv.pszText = TEXT("PID");
    lv.cx = 100;
    lv.iSubItem = 1;
    //ListView_InsertColumn(hListProcess, 1, &lv);
    SendMessage(hListProcess,LVM_INSERTCOLUMN,1,(LPARAM)&lv);
    //第三列
    lv.pszText = TEXT("镜像基址");
    lv.cx = 100;
    lv.iSubItem = 2;
    ListView_InsertColumn(hListProcess, 2, &lv);
    //第四列
    lv.pszText = TEXT("镜像大小");
    lv.cx = 100;
    lv.iSubItem = 3;
    ListView_InsertColumn(hListProcess, 3, &lv);
  • 向表中新增数据

    LV_ITEM vitem;
    
    //初始化
    memset(&vitem,0,sizeof(LV_ITEM));
    vitem.mask = LVIF_TEXT;
    
    vitem.pszText = "csrss.exe";
    vitem.iItem = 0;
    vitem.iSubItem = 0;
    //ListView_InsertItem(hListProcess, &vitem);
    SendMessage(hListProcess, LVM_INSERTITEM,0,(DWORD)&vitem);
    
    vitem.pszText = TEXT("448");
    vitem.iItem = 0;
    vitem.iSubItem = 1;
    ListView_SetItem(hListProcess, &vitem);
    
    vitem.pszText = TEXT("56590000");
    vitem.iItem = 0;
    vitem.iSubItem = 2;
    ListView_SetItem(hListProcess, &vitem);
    
    vitem.pszText = TEXT("000F0000");
    vitem.iItem = 0;
    vitem.iSubItem = 3;
    ListView_SetItem(hListProcess, &vitem);
    
    vitem.pszText = TEXT("winlogon.exe");
    vitem.iItem = 1;
    vitem.iSubItem = 0;
    //ListView_InsertItem(hListProcess, &vitem);
    SendMessage(hListProcess, LVM_INSERTITEM,0,(DWORD)&vitem);
    
    vitem.pszText = TEXT("456");
    vitem.iSubItem = 1;
    ListView_SetItem(hListProcess, &vitem);
    
    vitem.pszText = TEXT("10000000");
    vitem.iSubItem = 2;
    ListView_SetItem(hListProcess, &vitem);
    
    vitem.pszText = TEXT("000045800");
    vitem.iSubItem = 3;
    ListView_SetItem(hListProcess, &vitem);

3.WM_NOTIFY

WM_NOTIFY的使用:

该消息类型与WM_COMMAND类型相似,都是由子窗口向父窗口发送的消息。

WM_NOTIFY可以包含比WM_COMMAND更丰富的信息

Windows通用组件中有很多消息,都是通过WM_NOTIFY来描述的.

WM_NOTIFY消息中的参数如下:

wParam:控件ID

lParam:指向一个结构tagNMHDR

typedef struct tagNMHDR { 				

        HWND hwndFrom; //发送通知消息的控制窗口句柄				

        UINT idFrom;   //发送通知消息的控制ID值				

        UINT code;     //通知码,如LVM_SELCHANGED				

} NMHDR; 			

这个结构体能满足一般的要求,但能描述的信息还是有限的

解决方案:对每种不同用途的通知消息都定义另一种结构来表示

typedef struct tagNMLVCACHEHINT {				
    NMHDR   hdr;		//tagNMHDR		
    int     iFrom;				
    int     iTo;				
} NMLVCACHEHINT, *PNMLVCACHEHINT;				

typedef struct tagLVDISPINFO {				
    NMHDR hdr;				
    LVITEM item;				
} NMLVDISPINFO, FAR *LPNMLVDISPINFO;				

typedef struct _NMLVFINDITEM {				
	NMHDR hdr;				
    int iStart;				
    LVFINDINFO lvfi;				
} NMLVFINDITEM, *PNMLVFINDITEM;		

4.课后作业

写一个Win32程序,实现遍历当前进程与模块的功能

Win32__PE查看器

PE查看器

Win32__创建线程

1.程序、进程与线程

  • 程序就是在硬盘里还没跑起来的二进制文件,进程就是已经运行中的程序,一个进程至少有一个线程,比如一个正在举行的活动需要几十个人帮忙干活,进程就是那个活动,线程就是那几十个人
  • 一个线程启动是需要占用一个cpu的
  • 一个新线程也会创建一个新堆栈
  • 进程就是一个4GB容器,线程就是EIP

2.创建线程

HANDLE CreateThread(				
  LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性 通常为NULL				
  SIZE_T dwStackSize,                       // 参数用于设定线程可以将多少地址空间用于它自己的堆栈				
				        					// 每个线程拥有它自己的堆栈
  LPTHREAD_START_ROUTINE lpStartAddress,    // 参数用于指明想要新线程执行的线程函数的地址				
  LPVOID lpParameter,                       // 线程函数的参数				
				        					// 在线程启动执行时将该参数传递给线程函数
				        					// 既可以是数字,也可以是指向包含其他信息的一个数据结构的指针
  DWORD dwCreationFlags,                    // 0 创建完毕立即调度  CREATE_SUSPENDED创建后挂起				
  LPDWORD lpThreadId                        // 线程ID,lpThreadId是个out类型,相当于也是一个返回值,会把线程id写进这里面

关闭句柄 				
);				
				        					// 返回值:线程句柄

线程句柄与线程ID

  • 线程是由Windows内核在0环负责创建与管理的,正常我们是访问不了0环内核层的,句柄相当于一个令牌,有这个令牌就可以使用线程对象
  • 线程ID是身份证,唯一的,系统进行线程调度的时候要使用的.

以下是一个创建线程的示例:

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	int x = (int)lpParameter;
	for (int i = 0; i < 1000; i++) {
		printf("子线程 %d\n", x);
		Sleep(1000);
	}
	return 0;
}

void mytest()
{
	int x = 2;
	HANDLE hThread =  ::CreateThread(NULL, 0, ThreadProc, (void*)x, 0, NULL);
    ::CloseHandle(hThread);  //这里只是关闭线程句柄,而非关闭线程本身
}
void main()
{
	mytest();
	for (int i = 0; i < 1000; i++) {
		printf("主线程\n");
		Sleep(1000);
	}
}

关于向线程函数传参,第一种是创建全局变量,第二种是通过线程参数LPVOID lpParameter进行传参。需要注意的是,由于lpParameter是void*类型,我们需要格外关注堆栈的建立与销毁情况,比如说:

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	int x = (int)lpParameter;
	for (int i = 0; i < 1000; i++) {
		printf("子线程 %d\n", x);
		Sleep(1000);
	}
	return 0;
}

void mytest()
{
	int x = 2;
	HANDLE hThread =  ::CreateThread(NULL, 0, ThreadProc, &x, 0, NULL);
    ::CloseHandle(hThread);  //这里只是关闭线程句柄,而非关闭线程本身
}
void main()
{
	mytest();
	for (int i = 0; i < 1000; i++) {
		printf("主线程\n");
		Sleep(1000);
	}
}

上面这个代码就会出错,因为int x是在mytest中定义的局部变量,当mytest函数执行完后销毁堆栈,会导致&x取到的地址上原先存放的2已经被销毁。

3.倒计时程序

#include "main.h"
#include "resource.h"

HWND hEDIT;
VOID Timer()
{
	//获取子窗口数值
	TCHAR szBuffer[10];
	DWORD dwTimer;
	memset(szBuffer, 0, sizeof(szBuffer));
	GetWindowText(hEDIT, szBuffer, 10);

	//转成整型
	_stscanf_s(szBuffer, _T("%d"), &dwTimer);
	while (dwTimer > 0)
	{
		Sleep(1000);
		_stprintf_s(szBuffer, 10, _T("%d"), --dwTimer);
		SetWindowText(hEDIT, szBuffer);
	}
}
INT_PTR CALLBACK DialogProc(
	HWND hwndDlg,  // handle to dialog box			
	UINT uMsg,     // message			
	WPARAM wParam, // first message parameter			
	LPARAM lParam  // second message parameter			
)
{

	switch (uMsg)
	{
	case  WM_INITDIALOG:
	{

		//TCHAR szBuffer[5];

		hEDIT = GetDlgItem(hwndDlg, IDC_EDIT1);

		SetWindowText(hEDIT, TEXT("1000"));

		return TRUE;
	}
	case  WM_CLOSE:

		EndDialog(hwndDlg, 0);

		return TRUE;

	case  WM_COMMAND:

		switch (LOWORD(wParam))
		{
		case   IDC_BUTTON1:

			Timer();
			return TRUE;

		}
		break;
	}
	return FALSE;
}

int CALLBACK WinMain(
	_In_  HINSTANCE hInstance,
	_In_  HINSTANCE hPrevInstance,
	_In_  LPSTR lpCmdLine,
	_In_  int nCmdShow
)
{
	DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc);
	return 0;
}

这个程序是跑不通的,因为其线程只有默认的一个,当执行Timer时会占据仅有的一个线程,从而导致消息无法被获取,正确的做法是为Timer单独创建一个线程,以免消息循环被挤占。

#include "main.h"
#include "resource.h"

HWND hEDIT;
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	//获取子窗口数值
	TCHAR szBuffer[10];
	DWORD dwTimer;
	memset(szBuffer, 0, sizeof(szBuffer));
	GetWindowText(hEDIT, szBuffer, 10);

	//转成整型
	_stscanf_s(szBuffer, _T("%d"), &dwTimer);
	while (dwTimer > 0)
	{
		Sleep(1000);
		_stprintf_s(szBuffer, 10, _T("%d"), --dwTimer);
		SetWindowText(hEDIT, szBuffer);
	}

	return 0;
}
INT_PTR CALLBACK DialogProc(
	HWND hwndDlg,  // handle to dialog box			
	UINT uMsg,     // message			
	WPARAM wParam, // first message parameter			
	LPARAM lParam  // second message parameter			
)
{

	switch (uMsg)
	{
	case  WM_INITDIALOG:
	{

		//TCHAR szBuffer[5];

		hEDIT = GetDlgItem(hwndDlg, IDC_EDIT1);

		SetWindowText(hEDIT, TEXT("1000"));

		return TRUE;
	}
	case  WM_CLOSE:

		EndDialog(hwndDlg, 0);

		return TRUE;

	case  WM_COMMAND:

		switch (LOWORD(wParam))
		{
		case   IDC_BUTTON1:

			HANDLE hThread = ::CreateThread(NULL, 0, ThreadProc, 0, 0, NULL);
			return TRUE;

		}
		break;
	}
	return FALSE;
}

int CALLBACK WinMain(
	_In_  HINSTANCE hInstance,
	_In_  HINSTANCE hPrevInstance,
	_In_  LPSTR lpCmdLine,
	_In_  int nCmdShow
)
{
	DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc);
	return 0;
}

这个就没问题了

4.课后作业

image-20251029164641509

其实很简单,在倒计时程序的基础上再创建一个线程即可

#include "main.h"
#include "resource.h"

HWND hEDIT1;
HWND hEDIT2;
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	//获取子窗口数值
	TCHAR szBuffer[10];
	DWORD dwTimer;
	memset(szBuffer, 0, sizeof(szBuffer));
	GetWindowText(hEDIT1, szBuffer, 10);

	//转成整型
	_stscanf_s(szBuffer, _T("%d"), &dwTimer);
	while (dwTimer > 0)
	{
		Sleep(1000);
		_stprintf_s(szBuffer, 10, _T("%d"), --dwTimer);
		SetWindowText(hEDIT1, szBuffer);
	}

	return 0;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
	//获取子窗口数值
	TCHAR szBuffer[10];
	DWORD dwTimer;
	memset(szBuffer, 0, sizeof(szBuffer));
	GetWindowText(hEDIT2, szBuffer, 10);

	//转成整型
	_stscanf_s(szBuffer, _T("%d"), &dwTimer);
	while (dwTimer < 1000)
	{
		Sleep(1000);
		_stprintf_s(szBuffer, 10, _T("%d"), ++dwTimer);
		SetWindowText(hEDIT2, szBuffer);
	}

	return 0;
}

INT_PTR CALLBACK DialogProc(
	HWND hwndDlg,  // handle to dialog box			
	UINT uMsg,     // message			
	WPARAM wParam, // first message parameter			
	LPARAM lParam  // second message parameter			
)
{

	switch (uMsg)
	{
	case  WM_INITDIALOG:
	{

		//TCHAR szBuffer[5];

		hEDIT1 = GetDlgItem(hwndDlg, IDC_EDIT1);
		hEDIT2 = GetDlgItem(hwndDlg, IDC_EDIT2);

		SetWindowText(hEDIT1, TEXT("1000"));
		SetWindowText(hEDIT2, TEXT("0"));

		return TRUE;
	}
	case  WM_CLOSE:

		EndDialog(hwndDlg, 0);

		return TRUE;

	case  WM_COMMAND:

		switch (LOWORD(wParam))
		{
		case   IDC_BUTTON1:

			HANDLE hThread = ::CreateThread(NULL, 0, ThreadProc, 0, 0, NULL);
			HANDLE hThread1 = ::CreateThread(NULL, 0, ThreadProc1, 0, 0, NULL);
			return TRUE;

		}
		break;
	}
	return FALSE;
}

int CALLBACK WinMain(
	_In_  HINSTANCE hInstance,
	_In_  HINSTANCE hPrevInstance,
	_In_  LPSTR lpCmdLine,
	_In_  int nCmdShow
)
{
	DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc);
	return 0;
}