MessageBox深入研究

MessageBox深入研究

文章目录

MessageBox系列函数基本信息几个API的调用关系第一条路:一般的消息框(待完善)第二条路:硬错误消息框如果Session ID不相同如果Session ID相同CSRSS做了什么

MessageBox系列函数基本信息

MessageBox是最简单的图形界面交互API之一,只需要指定标题、正文、样式就可以弹出一个简单的对话框,而不需要指定消息处理例程,也不需要消息循环。然而Windows是一个复杂的操作系统,绝大多数的API的功能不可能简单实现,MessageBox也不例外。实际上,这个API内部大有文章。

MessageBox有以下几个版本:

MessageBoxA, MessageBoxW;MessageBoxExA, MessageBoxExW;MessageIndirectA, MessageBoxIndirectW;MessageBoxTimeoutA, MessageBoxTimeoutW.

以A系列为例,它们的原型如下:

int MessageBoxA(

IN HWND hWnd,

IN LPCWSTR lpText,

IN LPCWSTR lpCaption,

IN UINT uType

);

int MessageBoxExA(

IN HWND hWnd,

IN LPCWSTR lpText,

IN LPCWSTR lpCaption,

IN UINT uType,

IN WORD uLanguageId

);

int MessageBoxIndirectA(

IN const MSGBOXPARAMSA *lpmbp

);

int MessageBoxTimeoutA(

IN HWND hWnd,

IN LPCWSTR lpText,

IN LPCWSTR lpCaption,

IN UINT uType,

IN WORD uLanguageId,

IN DWORD dwMilliseconds

);

// 其中MessageBoxIndirect的MSGBOXPARAMSA结构如下

typedef struct tagMSGBOXPARAMSA {

UINT cbSize; // sizeof(MSGBOXPARAM)

HWND hwndOwner; // 对应其他函数的hWnd参数

HINSTANCE hInstance; // 含有图标的模块句柄(基址)

LPCSTR lpszText; // 对应其他函数的lpText

LPCSTR lpszCaption; // 对应其他函数的lpCaption

DWORD dwStyle; // 对应其他函数的uType

LPCSTR lpszIcon; // 消息框的图标

DWORD_PTR dwContextHelpId; // 帮助ID(作为参数在回调函数中使用)

MSGBOXCALLBACK lpfnMsgBoxCallback; // 帮助按钮的回调函数

DWORD dwLanguageId; // 对应其他函数的uLanguageId

} MSGBOXPARAMSA, *PMSGBOXPARAMSA, *LPMSGBOXPARAMSA;

还有ShellMessageBoxA和ShellMessageBoxW在shell32.dll中:

int WINCAPI ShellMessageBox(

HINSTANCE hInst,

HWND hWnd,

LPCSTR pszMsg,

LPCSTR pszTitle,

UINT fuStyle,

...

);

其中某些通用的参数含义如下:

hWnd 父窗口的句柄。可以为NULL.lpText 消息框的正文。lpCaption 消息框的标题。uType 消息框的样式,是一个Flag,有多个值可选。

关于按钮的uType:

Flag值消息框的按钮MB_OK0x0确定MB_OKCANCEL0x1确定和取消MB_ABORTRETRYIGNORE0x2终止、重试和忽略MB_YESNOCANCEL0x3是、否和取消MB_YESNO0x4是、否MB_RETRYCANCEL0x5重试和取消MB_CANCELTRYCONTINUE0x6取消、重试和继续MB_HELP0x4000帮助(增加一个按钮)MB_DEFBUTTON10x0(默认选中第一个按钮)MB_DEFBUTTON20x100(默认选中第二个按钮)MB_DEFBUTTON30x200(默认选中第三个按钮)

关于图标的uType:

Flag值消息框的图标MB_ICONSTOP0x10停止MB_ICONQUESTION0x20询问MB_ICONEXCLAMATION0x30警告MB_ICONINFORMATION0x40消息

关于模态的uType:

Flag值含义MB_APPLMODAL0x0在用户对消息框做出回应之前,父窗口和它的其它子窗口将被禁用(disabled).MB_SYSTEMMODAL0x1000效果同MB_APPMODAL,但是该消息框保持置顶。MB_TASKMODAL0x2000如果指定了父窗口,效果同MB_APPMODAL;如果没指定,在用户对消息框做出回应之前,当前线程的全体顶层窗口将被禁用。

其他uType:

Flag值含义MB_SETFOREGROUND0x10000消息框窗口前置。MB_DEFAULT_DESKTOP_ONLY0x20000如果当前桌面不是默认桌面,则在切换到默认桌面后才返回。MB_TOPMOST0x40000消息框窗口保持置顶。MB_RIGHT0x80000消息文本右对齐。MB_SERVICE_NOTIFICATION0x200000即使没有用户登陆到桌面,也能弹出窗口。

它们的返回值含义如下:

返回值值点击的按钮IDOK1确定IDCANCEL2取消IDABORT3终止IDRETRY4重试IDIGNORE5忽略IDYES6是IDNO7否IDTRYAGAIN10重试IDCONTINUE11继续

带有的-A后缀表示此函数接收的字符串为ANSI编码,而-W表示接收Unicode字符串。

绝大多数接收字符串的API都有A和W系列的,除了少数像GetProcAddress以外。Windows的内核使用Unicode字符串,所以调用W系列的函数往往会更直截了当。微软推荐使用MultiByteToWideChar函数转换字符串编码,然而user32调用MBToWCS函数,该函数多数情况下直接调用更快的RtlMultiByteToUnicodeN函数。

几个API的调用关系

根据xp源代码,可以发现MessageBoxA只是简单调用了MessageBoxExA,并把第5个参数LanguageId设为0,而MessageBoxExA又直接调用了MessageBoxTimeoutA,并把第6个参数Timeout设为0。 MessageBoxTimeout是一个可以指定消息框显示时间的函数,到了指定的时间后对话框就自动消失,并返回默认按钮的值,可以利用这个特点来进行毫秒级的延时。 MessageBoxTimeoutA把字符串一转,便去调用MessageBoxTimeoutW。相反,如果直接调用MessageBoxW,那么就会一路很顺畅地调用到MessageBoxTimeoutW。此外,MessageBoxIndirectA也是靠MessageBoxIndirectW实现的。

到了vista以后,调用关系发生了变化,MessageBox直接调用MessageBoxTimeout,在Windows10 1909上,调用关系如下:

顺便提一个细节,xp源代码中MessageBoxIndirect需要检查MSGBOXPARAMS.cbSize是否为sizeof(MSGBOXPARAMS),但是到了win10 1909就没有检查的代码了

继续追踪下去,MessageBoxTimeoutW和MessageBoxIndirect将有关的数据填入一个名为MSGBOXDATA的结构中,然后调用MessageBoxWorker函数。MessageBoxWorker函数位于user32.dll中,这个函数没有导出。

int MessageBoxWorker(LPMSGBOXDATA pMsgBoxParams);

typedef struct _MSGBOXDATA { // size = 160

MSGBOXPARAMS; // size = 80

PWND pwndOwner; // 所有者窗口对象(似乎没用到)

WORD wLanguageId; // 相当于其他函数的uLanguage参数

INT * pidButton; // 按钮的ID

LPWSTR * ppszButtonText; // 各个按钮的文字

UINT cButtons; // 按钮数量

UINT DefButton; // 默认选中第几个按钮

UINT CancelId; // 点击右上角的×号相当于点击哪个按钮

/* win2k 新增 */

DWORD dwTimeout; // 相当于MessageBoxTimeout的dwMilliseconds参数

/* XP 新增 */

HWND * phwndList; // 没用到

/* vista 新增,以下的成员名称根据IDA猜测而来 */

CHAR unknown[16];

WORD cxMsgFontChar; // 单个系统字符的平均宽度(用处见下)

WORD cyMsgFontChar; // 单个系统字符的平均高度(用处见下)

CHAR unknown2[2];

} MSGBOXDATA, *PMSGBOXDATA, *LPMSGBOXDATA;

MessageBoxWorker之前的函数都只是简单地调用下一个函数,正剧从这里开始,参数将得到真正的处理。 这个函数首先非常快速地检查参数是否合法,如果消息框没有标题,就命为“错误”两个字。重点在dwStyle的MB_SERVICE_NOTIFICATION标志,这个标志存在与否将控制流走向分成了两条路,接下来分别详细讨论。

第一条路:一般的消息框(待完善)

在Win10 1909中,MessageBoxWorker首先根据dwStyle参数获取以下两个数据:

按钮数量 以MsgBoxData.dwStyle的低4位索引一个名为mpTypeCcmd的全局数组。这个数组的名称是xp源代码中的,win10的符号文件并没有提供,以下出现的几个全局数组的名称也是这样。每个按钮上的文字 首先以MsgBoxData.dwStyle的低4位索引一个名mpTypeIich的全局数组,再以此为索引访问SEBbuttons的全局数组,获取按钮文字的资源ID。如果LanguageId==0,那么以该资源ID作为索引直接从gpsi->MBString数组中获取,否则使用LoadStringBaseExW函数从user32中加载。 user32、gdi32、win32k这些模块的开发者将缩写用到了极致,gpsi是Global Pointer of Server Information的缩写,存放着来自win32k的各种数据。

至于消息框的图标和弹框时播放的声音,这是接下来的事。

接着调用NtUserModifyUserStartupInfoFlags这个未导出函数,这个函数直接开始系统调用,进入内核查Shadow SSDT表以后调用win32k模块中的同名函数,修改进程的STARTUPINFO结构,将其dwFlags修改为STARTF_USESHOWWINDOW,防止有一些程序在初始化过程中遇到错误弹不出消息框。最后,MessageBoxWorker调用SoftModalMessageBox函数来弹出消息框:

int SoftModalMessageBox(LPMSGBOXDATA lpmb);

SoftModalMessageBox是一个导出函数,它可以创建任意个数按钮、任意图标、可延时、任意按钮文字的消息框。按道理,这么强大又简洁的函数是不应该被导出的,但是有一个原因使它被迫导出,接下来会说明(为另一条大路埋下伏笔)。这个函数主要做以下几件事:

计算消息框的尺寸 消息框的尺寸需要考虑几个部分:

按钮的个数 按钮个数在MSGBOXDATA中给出,标题和正文的长度屏幕的宽度边框的厚度是否有图标

补充:对话框基本单元(dialog base unit)和对话框模板单元(dialog template unit) 对话框基本单元是一个方形区域(或者说是面积单位),和对话框所用字体有关,宽和高等于该字体单个字符宽和高的平均值(舍入到整数,以像素计)。对话框模板单元的宽度等于基本单元的1/4,高度等于基本单元的1/8. 消息框所使用的字体各种信息都存储在gpsi中,SoftModalMessageBox直接就拿来用了。而对于一般的应用程序,可使用GetDialogBaseUnits这个API获取系统对话框基本单元的大小(这个API直接使用gpsi->cxSysFontChar和gpsi->cySysFontChar)。

确定消息框的图标

针对dwStyle参数播放声音

创建对话框模板

这个函数根据消息框的标题文字长度、正文的长度与行数对消息框窗口的长和宽进行计算,同时算清消息框的坐标(把消息框放到屏幕中心)。就像我们用SDK制作一个POPUP样式的窗口,这个函数也采用相同的套路:用NtUserGetDCEx(和GetDcEx是同一个函数,只不过user32.dll内部使用不同名称)获取DC,然后用DrawText绘制文字等。最后,它指定MB_DlgProcW为消息处理函数,调用InternalDialogBox(即DialogBoxParam的内部实现)来创建一个弹出式的窗口,其指定的资源在user32.dll初始化时已经准备好了。

顺便提一下,百度上说DialogBoxParam最终使用CreateWindowEx来创建窗口。其实,详细的过程为:DialogBoxParam使用FindResource、LoadResource、LockResource查找、加载并锁定资源,再利用资源的内存指针调用DialogBoxParamIndirectAorW->InternalDialogBox,InternalDialogBox调用InternalCreateDialog,在这其中才调用VerNtUserCreateWindowsEx->NtUserCreateWindowEx(起这么长的函数名打起来真是心累),此函数和CreateWindowEx还是有一点差别的。

第二条路:硬错误消息框

来说说第二条大路,即指定了MB_SERVICE_NOTIFICATION标志,这条路和操作系统内核就有比较密切的关系了。走了这条路,所弹出的Box会有非常神奇的效果:

弹出一个消息窗口在当前的活动桌面上,即使没有用户登录到该桌面上消息窗口永远保持最置前的状态(Override Mode),置前的程度高于MB_TOPMOST,或者其他设置了HWND_TOPMOST标志的窗口在这个消息窗口关闭之前,不能再有其他设置了此标志的消息窗口弹出,如果尝试弹出,则在第一个窗口得到响应之后才会出现消息窗口的所属进程为CSRSS.exe,而不是调用MessageBox的进程

MessageBoxWorker简单地调用 ServiceMessageBox:

int ServiceMessageBox(

IN LPCWSTR pText,

IN LPCWSTR pCaption,

IN UINT wType,

IN DWORD dwTimeout

);

很简单,就四个最基本的参数。ServiceMessageBox首先判断当前线程是否运行在与当前进程不同的 Session 上(即线程是否拥有其他 Session 的模拟令牌)。实现步骤是用NtOpenThreadToken(OpenThreadToken的实现)打开当前线程的令牌,再用NtQueryInformationToken(GetTokenInformation的实现)查询令牌的 Session ID ,与当前进程的 Session ID 比较。

当前进程的Session ID其实就在进程环境块PEB中,一行代码NtCurrentPeb()->SessionId即可搞定,但PEB结构没有文档化,所以还是调WTSGetActiveConsoleSessionId算了,这个函数也就相当于:USER_SHARED_DATA->ActiveConsoleId;

在NT5.0以前,不讨论跨session的情况。ServiceMessageBox函数简单填充HARDERROR_MSG结构,就调用CsrClientCallServer函数把请求通过LPC发到CSRSS去了。ntdll中有一个未导出的全局变量CsrPortHandle,是CSR LPC端口的句柄,CsrClientCallServer用的就是这个句柄。

如果Session ID不相同

如果Session ID不相同,则调用winsta.dll中的WinStationSendMessage(当然要导出啦,不然怎么调用,它同时也是WTSSendMessage的实现)向当前线程模拟令牌指定的Session弹出消息窗口。这个函数使用了RPC通知CSRSS,效率比较低,所以才进行之前的Session判断。

BOOLEAN

WINAPI

WinStationSendMessageW(

IN HANDLE hServer, //在win10中是一个CSmartBinding的类指针,不理他,填0

IN ULONG SessionId, //模拟令牌的Session ID

IN PWSTR Title, //消息窗口标题

IN ULONG TitleLength, //标题长度(以字节计)

IN PWSTR Message, //消息正文

IN ULONG MessageLength, //正文长度(以字节计)

IN ULONG Style, //窗口样式,同之前的dwStyle参数

IN ULONG Timeout, //消息框自动消失时间(以秒计)

OUT PULONG Response, //消息返回值,同MessageBox的返回值

IN BOOLEAN DoNotWait //是否等待消息窗口返回,填TRUE的话Response的结果就是未定义的

);

这个函数我们可以自己调着玩,示例代码如下。注意,编译时别忘了包含静态库,对于gcc,需要包含libwinsta.a,对于msvc,需要包含winsta.lib. 当然也可以不包含,只要用GetModuleHandle获取winsta.dll的模块地址,然后用GetProcAddress获取函数地址即可。

PWSTR text = L"text";

PWSTR caption = L"caption";

ULONG ret;

WinStationSendMessageW(

NULL, // 填NULL

WTSGetActiveConsoleSessionId(), // 当前进程的会话ID

caption, // 消息框标题

wcslen(caption) * sizeof(WCHAR), // 标题字符串长度(以字节计)

text, // 消息框正文

wcslen(text) * sizeof(WCHAR), // 正文字符串长度(以字节计)

MB_ICONWARNING, // 同上文的dwStyle

0, // 自动消失时间(以秒计),不想自动消失就填0或-1

&ret, // 消息返回值,同MessageBox的返回值

FALSE // 是否等待消息窗口返回

);

WinStationSendMessage也有A和W两个版本,这个时候字符串早就已经是unicode了,所以只用到W版本。这个函数内部

如果Session ID相同

如果Session ID一致,则调用系统服务NtRaiseHardError,这是个很好用的系统服务,原型和使用方法如下:

//原型

NTSYSAPI

NTSTATUS

NTAPI

NtRaiseHardError(

IN NTSTATUS ErrorStatus,

IN ULONG NumberOfParameters,

IN ULONG UnicodeStringParameterMask,

IN PULONG_PTR Parameters,

IN ULONG ValidResponseOptions,

OUT PULONG Response //对应HARDERROR_RESPONSE

);

// 其中*Response的可能值如下

typedef enum _HARDERROR_RESPONSE {

ResponseReturnToCaller,

ResponseNotHandled,

ResponseAbort, //意思同IDABORT

ResponseCancel, //IDCANCEL

ResponseIgnore, //IDIGNORE

ResponseNo, //IDNO

ResponseOk, //IDOK

ResponseRetry, //IDRETRY

ResponseYes, //IDYES

ResponseTryAgain, //IDTRYAGAIN

ResponseContinue //IDCONTINUE

} HARDERROR_RESPONSE;

这个函数我们也可以自己调着玩,示例代码如下。注意,编译时别忘了包含静态库,对于gcc,需要包含libntdll.a,对于msvc,需要包含ntdll.lib或ntdllp.lib. 当然也可以不包含,只要用GetModuleHandle获取ntdll.dll的模块地址,然后用GetProcAddress获取函数地址即可。 另外这段代码在内核中也可以用,前提是将NtRaiseHardError改成ExRaiseHardError,两个函数的参数完全相同。

//RtlInitUnicodeString MSDN有相关文档

#define STATUS_SERVICE_NOTIFICATION 0x40000018L

#define HARDERROR_OVERRIDE_ERRORMODE 0x10000000L

ULONG_PTR Parameters[4];

PWSTR pText = L"消息正文";

PWSTR pCaption = L"标题";

ULONG Response;

UNICODE_STRING Text, Caption;

RtlInitUnicodeString(&Text, pText);

RtlInitUnicodeString(&Caption, pCaption);

Parameters[0] = (ULONG_PTR)&Text;

Parameters[1] = (ULONG_PTR)&Caption;

Parameters[2] = MB_YESNO; //同MessageBox的uType

Parameters[3] = 0; //同MessageBoxTimeout的Timeout,单位为毫秒

NtRaiseHardError(

STATUS_SERVICE_NOTIFICATION | HARDERROR_OVERRIDE_ERRORMODE,

4,

3,

Parameters,

OptionOk,

&Response

);

NtRaiseHardError进入内核后调用内核模块中的同名函数NtRaiseHardError->ExpRaiseHardError。后者的处理根据操作系统版本的不同而不同。

Windows 2000 (NT 5.0) 至 Windows Server 2003 (NT 5.1) ExpRaiseHardError调用 LPC函数LpcRequestWaitReplyPortEx通知CSRSS,并传入HARDERROR_MSG结构,这个结构从xp到win11都没有改变过。目标LPC端口是本进程的EPROCESS.ExceptionPortWindows Vista (NT 6.0) 以后 Vista的内核是一次史诗级更新,整个LPC的代码都删光了,换上了新的ALPC。为了保持兼容性,内核导出的LPC API依然存在,但它们仅简单地调用ALPC的相关函数。ExpRaiseHardError调用的是LpcSendWaitReceivePort函数,这个函数只是进入临界区之后调用ALPC组件的AlpcpProcessSynchronousRequest函数而已。目标端口保持不变。

上文提到的HARDERROR_MSG结构如下:

typedef struct _HARDERROR_MSG {

PORT_MESSAGE h; //LPC端口消息的必要头部

NTSTATUS Status; //STATUS_SERVICE_NOTIFICATION

LARGE_INTEGER ErrorTime; //当前时间,由ExpRaiseHardError调用KeGetCurrentTime()产生

ULONG ValidResponseOptions; //同NtRaiseHardError的同名参数

ULONG Response; //同NtRaiseHardError的同名参数

ULONG NumberOfParameters; //同NtRaiseHardError的同名参数(=4)

ULONG UnicodeStringParameterMask; //同NtRaiseHardError的同名参数(=3)

ULONG_PTR Parameters[5]; //同NtRaiseHardError的同名参数

} HARDERROR_MSG, *PHARDERROR_MSG;

我们可以更进一步地,在内核中通过LPC通知CSRSS,示例代码如下,该程序在win10 1909上成功运行。

// 获取当前进程session id

token = PsReferencePrimaryToken(IoGetCurrentProcess());

Status = SeQuerySessionIdToken(token, &sessionId);

PsDereferencePrimaryToken(token);

if (!NT_SUCCESS(Status)) return Status;

// 注意,这里的Text和Caption是宽字符串指针,而这两个宽字符串必须要在用户空间中

// 否则CSRSS无法处理这些字符串

RtlInitUnicodeString(&UText, Text);

RtlInitUnicodeString(&UCaption, Caption);

swprintf_s(portName, 250, L"\\Sessions\\%d\\Windows\\ApiPort", sessionId);

RtlInitUnicodeString(&UPort, portName);

// ObReferenceObjectByName没有文档化,去WRK中找定义吧

Status = ObReferenceObjectByName(

&UPort, OBJ_CASE_INSENSITIVE, NULL, 0,

*LpcPortObjectType, KernelMode, NULL, &Port

);

if(!NT_SUCCESS(Status)) return Status;

__try {

m->h.u1.Length =

sizeof(HARDERROR_MSG) << 16 | (sizeof(HARDERROR_MSG) - sizeof(PORT_MESSAGE));

m->h.u2.ZeroInit = LPC_ERROR_EVENT; // LPC_ERROR_EVENT = 9

m->Status = STATUS_SERVICE_NOTIFICATION; // STATUS_SERVICE_NOTIFICATION = 0x40000018

m->ValidResponseOptions = 0;

m->UnicodeStringParameterMask = 3;

m->NumberOfParameters = 4;

m->Response = 0;

m->Parameters[0] = (ULONG_PTR)UText;

m->Parameters[1] = (ULONG_PTR)UCaption;

m->Parameters[2] = (ULONG_PTR)Style;

m->Parameters[3] = (ULONG_PTR)Timeout;

m->Parameters[4] = 0;

KeQuerySystemTime(&m->ErrorTime);

}__except(EXCEPTION_EXECUTE_HANDLER) {

ObDereferenceObject(Port);

return GetExceptionCode();

}

Status = LpcRequestWaitReplyPortEx(Port, (PPORT_MESSAGE)m, (PPORT_MESSAGE)m);

ObDereferenceObject(Port);

return Status;

总之,走这条大路都离不开CSRSS,那为什么要进入到内核这么麻烦呢? 因为指定了MB_SERVICE_NOTIFICATION的消息窗口主要被用于服务端对客户端的通知,在SCM看来,内核中的驱动模块也是一种服务,所以,内核中也提供了相应的函数来实现这个过程,如IoRaiseHardError、IoRaiseInformationalHardError,他们最终都是调用ExRaiseHardError -> ExpRaiseHardError来实现的。

到这里,我们可以总结一下ServiceMessageBox之后、请求到达CSRSS之前的函数调用情况

CSRSS做了什么

那么,通知CSRSS后,它做了些什么呢?

在NT4上,CSRSS的其中一个LPC端口服务线程接收到LPC_ERROR_EVENT消息后,就去调用LoadedServerDll->HardErrorRoutine,这个例程在CSR初始化时就已经设置好的了。这个例程到底在哪?网络上搜不到任何有关资料。翻翻NT4源代码,再结合IDA和调试器,发现这个例程是winsrv.dll中的一个未导出函数UserHardError->UserHardErrorEx,并传入HARDERROR_MSG结构和CSRSS自己储存的关于引起错误的线程信息,原型如下:

VOID UserHardError(

PCSR_THREAD pt,

PHARDERROR_MSG pmsg

);

VOID UserHardErrorEx(

PCSR_THREAD pt,

PHARDERROR_MSG pmsg,

PCTXHARDERRORINFO pCtxHEInfo

);

UserHardError经过一轮参数检查,从发起消息窗口的进程中复制消息正文和标题,若 HARDERRORMSG.ValidResponseOptions == OptionOkNoWait(对应NtRaiseHardError的同名参数和WinStationSendMessage的DoNotWait),则立马通知CSRSS返回,并创建一个新线程来弹出窗口。创建新线程在ProcessHardErrorRequest中实现,这个函数还会调用HardErrorHandler()(零参数),做最后的实现。

高潮来了,HardErrorHandle调用NtUserHardErrorControl,对当前线程做一些奇奇怪怪的事情[ Win32k全局变量重设置(由此决定消息窗口的唯一性)、切换桌面(由此决定消息窗口的前置性)、加入消息队列(由此决定下一个类似消息窗口的可用性)等等],确保当前线程有能力弹出MB_SERVICE_NOTIFICATION样式的窗口。

UINT NtUserHardErrorControl(

IN HARDERRORCONTROL dwCmd,

IN HANDLE handle,

OUT PDESKRESTOREDATA pdrdRestore OPTIONAL

);

设置完之后,HardErrorHandler调用SoftModalMessageBox(所以这个函数必须要导出),弹出消息窗口,回到了第一条大路。但由于 NtUserHardErrorControl的功劳,这个窗口变得唯一、最前置、不美观(Windows7之后修复了这个问题)。

所以,一个简简单单的MessageBox,却要牵涉到模态、窗口、消息、RPC/LPC、桌面、会话、系统服务等机制,需要user32.dll,ntdll,内核,winsta.dll,csrss.exe,winsrv.dll,win32k.sys等模块的参与。这似乎印证了一个道理:Windows中,使用越方便的API,背后的原理越复杂。

相关推荐

bt365备用网站 钱塘江大潮的7种类型,最后一个光听名字都害怕!

钱塘江大潮的7种类型,最后一个光听名字都害怕!

bt365备用网站 迷你世界忘记迷你号了怎么找回2025年的账号

迷你世界忘记迷你号了怎么找回2025年的账号

bt365备用网站 微信来消息没有红点显示为什么?

微信来消息没有红点显示为什么?