文章目录
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,背后的原理越复杂。