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

Message有以下几个版本: 
- MessageBoxA,MessageBoxW; 
- MessageBoxExA,MessageBoxExW; 
- MessageIndirectA,MessageBoxIndirectW; 
- MessageBoxTimeoutA,MessageBoxTimeoutW.

还有ShellMessageBoxA和ShellMessageBoxW在shell32.dll中:

int WINCAPI ShellMessageBox(
            HINSTANCE hInst, 
            HWND hWnd, 
            LPCSTR pszMsg, 
            LPCSTR pszTitle, 
            UINT fuStyle, 
            ...);
  • -A后缀的API表示接收的字符串为ANSI格式的,而-W表示接收Unicode格式的字符串。ANSI系列的函数会把字符串用RtlAnsiStringToUnicodeString函数转换为Unicode格式,然后调用Unicode系列的。所以直接调用Unicode系列的函数会更直截了当(绝大多数接收字符串的API都有A和W系列的,除了少数像GetProcAddress以外)。

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

int WINAPI MessageBoxTimeout(
           IN HWND hWnd, 
           IN LPCSTR lpText, 
           IN LPCSTR lpCaption, 
           IN UINT uType, 
           IN WORD wLanguageId, 
           IN DWORD dwMilliseconds
           );

此外,MessageBoxIndirectA也是靠MessageBoxIndirectW实现的。继续追踪下去,MessageBoxTimeoutW和MessageBoxIndirectW将有关的数据填入一个名为MSGBOXDATA的结构中。 
以下给出MSGBOXDATA的定义:

typedef struct _MSGBOXPARAMS {//公开的结构
  UINT           cbSize;
  HWND           hwndOwner;
  HINSTANCE      hInstance;
  LPCTSTR        lpszText;
  LPCTSTR        lpszCaption;
  DWORD          dwStyle;
  LPCTSTR        lpszIcon;
  DWORD_PTR      dwContextHelpId;
  MSGBOXCALLBACK lpfnMsgBoxCallback;
  DWORD          dwLanguageId;
} MSGBOXPARAMS, *PMSGBOXPARAMS;

typedef struct _MSGBOXDATA {            
    MSGBOXPARAMS;                       
    PWND     pwndOwner;                 
    WORD     wLanguageId;
    INT    * pidButton;                 
    LPWSTR * ppszButtonText;            
    UINT     cButtons;                  
    UINT     DefButton;
    UINT     CancelId;
    DWORD    Timeout;
} MSGBOXDATA, *PMSGBOXDATA, *LPMSGBOXDATA;

然后以它作为唯一的参数调用一个user32.dll未导出的函数–MessageBoxWorker:

int MessageBoxWorker(
    LPMSGBOXDATA pMsgBoxParams);

以上的函数都只是简单地调用下一个函数,而从这里开始,MessageBox的参数将得到真正的处理。MessageBoxWorker首先检查参数中的ParentHwnd是否为零,从而确认是否Disable父窗口。接下来检查窗口样式是否指定MB_SERVICE_NOTIFICTION标志,从这里开始,控制流将走上两条截然不同的大路。

若未指定MB_SERVICE_NOTIFICATION标志,MessageBoxWorker会根据user32服务初始化时定义的一个全局变量来确定消息框按钮的个数及每个按钮上的文字,同时计算消息框的图标样式(例如询问、警告、错误)。等这些都计算完以后,调用NtUserModifyUserStartupInfoFlags这个未导出函数,这个函数直接开始系统调用,进入内核查Shadow SSDT表以后调用win32k模块中的同名函数,修改进程的STARTUPINFO结构。最后,MessageBoxWorker调用SoftModalMessageBox函数来弹出对话框:

int  SoftModalMessageBox(
     LPMSGBOXDATA lpmb);

SoftModalMessageBox是一个导出函数,它可以创建任意个数按钮、任意图标、可延时、任意按钮文字的消息框。按道理,这么强大又简洁的函数是不应该被导出的,但是有一个原因使它被迫导出,接下来会说明(为另一条大路埋下伏笔)。这个函数根据消息框的标题文字长度、正文的长度与行数对消息框窗口的长和宽进行计算,同时算清消息框的坐标(把消息框放到屏幕中心)。就像我们用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(
    LPCWSTR pText,
    LPCWSTR pCaption,
    UINT wType,
    UINT Timeout
    );

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

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

BOOLEAN
WINAPI
WinStationSendMessageW(
    IN HANDLE hServer,      //填0
    IN ULONG SessionId,     //模拟令牌的Session ID
    IN PWSTR Title,         //消息窗口标题
    IN ULONG TitleLength,   //标题长度
    IN PWSTR Message,       //消息正文
    IN ULONG MessageLength, //正文长度
    IN ULONG Style,         //窗口样式,如MB_ICONEXCLAMATION
    IN ULONG Timeout,       //自动消失时间
    OUT PULONG Response,    //消息返回值,如IDCANCEL
    IN BOOLEAN DoNotWait    //是否等待消息窗口返回
    );

如果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
    );

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;
//使用方法
//RtlInitUnicodeString MSDN有相关文档
typedef LONG NTSTATUS
#define STATUS_SERVICE_NOTIFICATION ((NTSTATUS)0x40000018L)
#define HARDERROR_OVERRIDE_ERRORMODE    0x10000000

NTSTATUS Status;
ULONG_PTR Parameters[4];
PWSTR pText = L"消息正文";
PWSTR pCaption = L"标题";
ULONG Response, Timeout = 0, Type = MB_YESNO;
UNICODE_STRING Text, Caption;

RtlInitUnicodeString(&Text, pText);
RtlInitUnicodeString(&Caption, pCaption);
Parameters[0] = (ULONG_PTR)&Text;
Parameters[1] = (ULONG_PTR)&Caption;
Parameters[2] = Type;           //同MessageBox的uType
Parameters[3] = Timeout;        //同WTSSendMessage的Timeout,单位为毫秒

Status = NtRaiseHardError(STATUS_SERVICE_NOTIFICATION | HARDERROR_OVERRIDE_ERRORMODE,
                          4,
                          3,
                          Parameters,
                          OptionOk,
                          &Response);

NtRaiseHardError进入内核后调用内核模块中的同名函数NtRaiseHardError->ExpRaiseHardError,ExpRaiseHardError调用 LPC函数LpcRequestWaitReplyPortEx通知CSRSS,并传入一个结构: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;

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

那么,通知CSRSS后,它做了些什么呢?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,背后的原理越复杂