??此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
??看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?练习做完了吗?没有的话就不要继续了。
?
?? 华丽的分割线 ??
?
??由于win32k.sys里面大量的所需符号和结构体都没有,有很多东西我目前只能浅尝辄止,UI是一个很复杂的东西,我的能力有限也只能具体讲解一下它的功能、大体流程和某些小细节。对于代码参考,是基于ReactOS 0.2.0版本和泄露的WinXP SP1代码。ReactOS是基于逆向Windows得到,所以基本类似,建议此版本,高版本的有自己的实现了,就不太一样了。
??学过Win32图形界面编程的都知道我们要显示一个窗体是十分麻烦的,如下是创建项目自己生成的,我进行略加修改,最终代码如下:
#include "stdafx.h"#include <windows.h>#define WM_MyMessage WM_USER+1HINSTANCE hInst; // 当前实例char szTitle[] = "WingSummerTest"; // 标题栏文本char szWindowClass[]= "CnBlog"; // 主窗口类名// 此代码模块中包含的函数的前向声明ATOM MyRegisterClass(HINSTANCE hInstance);BOOL InitInstance(HINSTANCE, int);LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow){ UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); MyRegisterClass(hInstance); // 执行应用程序初始化 if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } MSG msg; // 主消息循环 while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int) msg.wParam;}//// 函数: MyRegisterClass()// 目标: 注册窗口类。//ATOM MyRegisterClass(HINSTANCE hInstance){ WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = NULL; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = NULL; wcex.lpszClassName = szWindowClass; wcex.hIconSm = NULL; return RegisterClassEx(&wcex);}//// 函数: InitInstance(HINSTANCE, int)// 目标: 保存实例句柄并创建主窗口// 注释: 在此函数中,我们在全局变量中保存实例句柄并创建和显示主程序窗口。//BOOL InitInstance(HINSTANCE hInstance, int nCmdShow){ hInst = hInstance; // 将实例句柄存储在全局变量中 HWND hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) { return FALSE; } ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE;}//// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)// 目标: 处理主窗口的消息。// WM_COMMAND - 处理应用程序菜单// WM_PAINT - 绘制主窗口// WM_DESTROY - 发送退出消息并返回//LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam){ switch (message) { case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // TODO: 在此处添加使用 hdc 的任何绘图代码... EndPaint(hWnd, &ps); } break; case WM_DESTROY: PostQuitMessage(0); break; case WM_MyMessage: MessageBox(NULL,"WingSummer Test Recived!","CnBlog",MB_ICONINFORMATION); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0;}??如果我们要实现创建并显示一个窗体,首先得创建一个窗体类,并将它注册。然后调用CreateWindow创建窗体,然后调用ShowWindow显示出来,而WndProc就是我在注册的时候的窗体消息处理过程。执行完上面的代码后,我们开始进入消息处理,调用熟悉的三个函数就可以实现消息处理。
??但是,你们知道是为什么必须吗?这所谓的消息是什么?消息队列在哪里?
??如下是维基百科的解释:
There are two main senses of the word "message" in computing: messages between the human users of computer systems that are delivered by those computer systems, and messages passed between programs or between components of a single program, for their own purposes.
??在Windows中,消息可以来自键盘、鼠标等硬件,也可以来自于其他进程线程发送来的消息。我们既然了解了什么是消息,那么消息队列是什么。

??如上是老的Windows的实现方式,版本Win95、Win98和很多Linux的实现方式都是它。在3环每一个用户空间都有一个消息队列。如果捕捉消息,必然有一个专用进程进行捕获封装,因为我们编程的时候从来没有写过自己捕获消息的代码。当专用进程捕获到消息,就会往指定目标进程插入一条消息,实现方式只能是跨进程通信,但是这有不足的地方,会耗费大量的时间于通信上。那么微软的最终实现是什么,就是把消息队列搬到0环,使用GUI线程,你想要获取消息仅需要通过系统调用的方式直接获得,而不必进行跨进程通信,大大提升了效率。
??当线程刚创建的时候,都是普通线程,之前我们讲过可以通过线程结构体的ServiceTable进行判断。当线程第一次调用Win32k.sys实现的函数时时,会调用PsConvertToGuiThread,以扩充内核栈,换成64KB的大内核栈,原普通内核栈只有12KB大小;创建一个包含消息队列的结构体,并挂到KTHREAD上。ServiceTable指向的地址变为KeServiceDescriptorTableShadow;把需要的内存数据映射到本进程空间。
??那么消息队列在在哪里呢?我们看下面的结构体:
kd> dt _KTHREADntdll!_KTHREAD …… +0x12c CallbackStack : Ptr32 Void +0x130 Win32Thread : Ptr32 Void +0x134 TrapFrame : Ptr32 _KTRAP_FRAME +0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE ……??看到Win32Thread了吗,这里面存储了消息队列,但消息队列不仅仅有一个,只有图形界面才有有效值,指向一个结构体。如下所示:
typedef struct tagTHREADINFO { W32THREAD;//***************************************** begin: USER specific fields PTL ptl; // Listhead for thread lock list PPROCESSINFO ppi; // process info struct for this thread PQ pq; // keyboard and mouse input queue PKL spklActive; // active keyboard layout for this thread PCLIENTTHREADINFO pcti; // Info that must be visible from client PDESKTOP rpdesk; PDESKTOPINFO pDeskInfo; // Desktop info visible to client PCLIENTINFO pClientInfo; // Client info stored in TEB DWORD TIF_flags; // TIF_ flags go here. PUNICODE_STRING pstrAppName; // Application module name. PSMS psmsSent; // Most recent SMS this thread has sent PSMS psmsCurrent; // Received SMS this thread is currently processing PSMS psmsReceiveList; // SMSs to be processed LONG timeLast; // Time and ID of last message ULONG_PTR idLast; int exitCode; HDESK hdesk; // Desktop handle int cPaintsReady; UINT cTimersReady; PMENUSTATE pMenuState; union { PTDB ptdb; // Win16Task Schedule data for WOW thread PWINDOWSTATION pwinsta; // Window station for SYSTEM thread }; PSVR_INSTANCE_INFO psiiList; // thread DDEML instance list DWORD dwExpWinVer; DWORD dwCompatFlags; // The Win 3.1 Compat flags DWORD dwCompatFlags2; // new DWORD to extend compat flags for NT5+ features PQ pqAttach; // calculation variabled used in // zzzAttachThreadInput() PTHREADINFO ptiSibling; // pointer to sibling thread info PMOVESIZEDATA pmsd; DWORD fsHooks; // WHF_ Flags for which hooks are installed PHOOK sphkCurrent; // Hook this thread is currently processing PSBTRACK pSBTrack; HANDLE hEventQueueClient; PKEVENT pEventQueueServer; LIST_ENTRY PtiLink; // Link to other threads on desktop int iCursorLevel; // keep track of each thread's level POINT ptLast; // Position of last message PWND spwndDefaultIme; // Default IME Window for this thread PIMC spDefaultImc; // Default input context for this thread HKL hklPrev; // Previous active keyboard layout int cEnterCount; MLIST mlPost; // posted message list. USHORT fsChangeBitsRemoved;// Bits removed during PeekMessage WCHAR wchInjected; // character from last VK_PACKET DWORD fsReserveKeys; // Keys that must be sent to the active // active console window. PKEVENT *apEvent; // Wait array for xxxPollAndWaitForSingleObject ACCESS_MASK amdesk; // Granted desktop access UINT cWindows; // Number of windows owned by this thread UINT cVisWindows; // Number of visible windows on this thread PHOOK aphkStart[CWINHOOKS]; // Hooks registered for this thread CLIENTTHREADINFO cti; // Use this when no desktop is available#ifdef GENERIC_INPUT HANDLE hPrevHidData;#endif#if DBG UINT cNestedCalls;#endif} THREADINFO;??上面的代码是泄露的WinXP SP1的代码,符号里面没有这个结构体,故无法dt进行查看,注释还比较详细,我们就能知道消息队列在哪里:
PQ pq; // keyboard and mouse input queue??我们创建一个进程或者线程,都会在内核中创建一个结构体。同样窗体也是一样的,当你创建一个窗体,就会在内核创建一个内核对象,在ReactOS中为称之为_WINDOW_OBJECT对象(在最新版的已经没了):
typedef struct _WINDOW_OBJECT{ /* Pointer to the window class. */ PWNDCLASS_OBJECT Class; /* Extended style. */ DWORD ExStyle; /* Window name. */ UNICODE_STRING WindowName; /* Style. */ DWORD Style; /* Context help id */ DWORD ContextHelpId; /* system menu handle. */ HMENU SystemMenu; /* Handle of the module that created the window. */ HINSTANCE Instance; /* Entry in the thread's list of windows. */ LIST_ENTRY ListEntry; /* Pointer to the extra data associated with the window. */ PCHAR ExtraData; /* Size of the extra data associated with the window. */ ULONG ExtraDataSize; /* Position of the window. */ RECT WindowRect; /* Position of the window's client area. */ RECT ClientRect; /* Handle for the window. */ HANDLE Self; /* Window flags. */ ULONG Flags; /* Window menu handle or window id */ UINT IDMenu; /* Handle of region of the window to be updated. */ HANDLE UpdateRegion; HANDLE NCUpdateRegion; /* Pointer to the owning thread's message queue. */ PUSER_MESSAGE_QUEUE MessageQueue; struct _WINDOW_OBJECT* FirstChild; struct _WINDOW_OBJECT* LastChild; /* Lock for the list of child windows. */ FAST_MUTEX ChildrenListLock; struct _WINDOW_OBJECT* NextSibling; struct _WINDOW_OBJECT* PrevSibling; /* Entry in the list of thread windows. */ LIST_ENTRY ThreadListEntry; /* Pointer to the parent window. */ struct _WINDOW_OBJECT* Parent; /* Pointer to the owner window. */ struct _WINDOW_OBJECT* Owner; /* DC Entries (DCE) */ PDCE Dce; /* Property list head.*/ LIST_ENTRY PropListHead; FAST_MUTEX PropListLock; ULONG PropListItems; /* Scrollbar info */ PSCROLLBARINFO pHScroll; PSCROLLBARINFO pVScroll; PSCROLLBARINFO wExtra; LONG UserData; BOOL Unicode; WNDPROC WndProcA; WNDPROC WndProcW; PETHREAD OwnerThread; HWND hWndLastPopup; /* handle to last active popup window (wine doesn't use pointer, for unk. reason)*/ PINTERNALPOS InternalPos;} WINDOW_OBJECT; /* PWINDOW_OBJECT already declared at top of file */??注意上述结构体不同版本的结构和顺序会有差异,有的版本的会有下面的成员:
/* Pointer to the thread information */ PW32THREADINFO ti;??这个就和线程有一个成员指向自己进程地址一样,通过窗体内核对象对应相应的线程。这块仅供了解,由于没有真正的Windows源码,不好定夺。如下是从泄露的代码找出的窗体结构:
typedef struct tagWND { THRDESKHEAD head; WW; // WOW-USER common fields. Defined in wowuserp.h // The presence of "state" at the start of this structure is // assumed by the STATEOFFSET macro. PWND spwndNext; // Handle to the next window PWND spwndPrev; // Handle to the previous window PWND spwndParent; // Backpointer to the parent window. PWND spwndChild; // Handle to child PWND spwndOwner; // Popup window owner field RECT rcWindow; // Window outer rectangle RECT rcClient; // Client rectangle WNDPROC_PWND lpfnWndProc; // Can be WOW address or standard address PCLS pcls; // Pointer to window class KHRGN hrgnUpdate; // Accumulated paint region PPROPLIST ppropList; // Pointer to property list PSBINFO pSBInfo; // Words used for scrolling PMENU spmenuSys; // Handle to system menu PMENU spmenu; // Menu handle or ID KHRGN hrgnClip; // Clipping region for this window LARGE_UNICODE_STRING strName; int cbwndExtra; // Extra bytes in window PWND spwndLastActive; // Last active in owner/ownee list KHIMC hImc; // Associated input context handle KERNEL_ULONG_PTR dwUserData; // Reserved for random application data struct _ACTIVATION_CONTEXT * KPTR_MODIFIER pActCtx;#ifdef LAME_BUTTON KERNEL_PVOID pStackTrace; // Creation stack trace; used by lame // button.#endif // LAME_BUTTON} WND;??一个GUI线程对应一个消息队列,那么消息从哪里来呢?
??当我们单击窗体的关闭按钮,或者光标在窗体上移动,点击键盘,就会产生大量消息。那些消息就是win32k.sys的线程监控捕获的,我们来定位一下它的创建线程地方:
void xxxCreateSystemThreads(BOOL bRemoteThread){ UINT uThreadID; PVOID pvoid; /* * Do not allow any process other than CSRSS to call this function. * The only exception is Ghost thread case since now we allow it to launch * in the context of the shell process */ if (!bRemoteThread && !ISCSRSS()) { RIPMSG0(RIP_WARNING, "xxxCreateSystemThreads get called from a Process other than CSRSS"); return; } if (!CSTPop(&uThreadID, &pvoid, NULL, bRemoteThread)) { return; } LeaveCrit(); switch (uThreadID) { case CST_DESKTOP: xxxDesktopThread((PTERMINAL)pvoid); break; case CST_RIT: RawInputThread(pvoid); break; case CST_GHOST: GhostThread((PDESKTOP)pvoid); break; case CST_POWER: VideoPortCalloutThread(pvoid); break; } EnterCrit();}??而这个函数又是该函数调用的:
/***************************************************************************\* CreateSystemThreads** Simply calls xxxCreateSystemThreads, which will call the appropriate* thread routine (depending on uThreadID).** History:* 20-Aug-00 MSadek Created.\***************************************************************************/WINUSERAPIDWORDWINAPICreateSystemThreads ( LPVOID pUnused){ UNREFERENCED_PARAMETER(pUnused); NtUserCallOneParam(TRUE, SFI_XXXCREATESYSTEMTHREADS); ExitThread(0);}??这玩意挺复杂,还加入了会话隔离机制,由于本人水平有限就定位到这里。上面的代码和咱XP的逆向代码一致。要想要具体的细节,自己可以研究。
??消息队列并不是仅仅有一个,我们可以看看上面所谓的USER_MESSAGE_QUEUE结构体:
typedef struct _USER_MESSAGE_QUEUE{ /* Owner of the message queue */ struct _ETHREAD *Thread; /* Queue of messages sent to the queue. */ LIST_ENTRY SentMessagesListHead; /* Queue of messages posted to the queue. */ LIST_ENTRY PostedMessagesListHead; /* Queue of sent-message notifies for the queue. */ LIST_ENTRY NotifyMessagesListHead; /* Queue for hardware messages for the queue. */ LIST_ENTRY HardwareMessagesListHead; /* Lock for the hardware message list. */ FAST_MUTEX HardwareLock; /* Lock for the queue. */ FAST_MUTEX Lock; /* True if a WM_QUIT message is pending. */ BOOLEAN QuitPosted; /* The quit exit code. */ ULONG QuitExitCode; /* Set if there are new messages in any of the queues. */ KEVENT NewMessages; /* FIXME: Unknown. */ ULONG QueueStatus; /* Current window with focus (ie. receives keyboard input) for this queue. */ HWND FocusWindow; /* True if a window needs painting. */ BOOLEAN PaintPosted; /* Count of paints pending. */ ULONG PaintCount; /* Current active window for this queue. */ HWND ActiveWindow; /* Current capture window for this queue. */ HWND CaptureWindow; /* Current move/size window for this queue */ HWND MoveSize; /* Current menu owner window for this queue */ HWND MenuOwner; /* Identifes the menu state */ BYTE MenuState; /* Caret information for this queue */ PTHRDCARETINFO CaretInfo; /* Window hooks */ PHOOKTABLE Hooks; /* queue state tracking */ WORD WakeBits; WORD WakeMask; WORD ChangedBits; WORD ChangedMask; /* extra message information */ LPARAM ExtraInfo;} USER_MESSAGE_QUEUE, *PUSER_MESSAGE_QUEUE;??从这里看到最起码就有四个消息队列,细节我们将会在下一篇进行。
??我们来查找窗体的时候都是用FindWindow进行查找,我们仅通过名称就可以调用,我们可以猜测窗体句柄就是全局的。我们来定位一下它的代码:
PWND _FindWindowEx( PWND pwndParent, PWND pwndChild, LPCWSTR ccxlpszClass, LPCWSTR ccxlpszName, DWORD dwType){ /* * Note that the Class and Name pointers are client-side addresses. */ PBWL pbwl; HWND *phwnd; PWND pwnd; WORD atomClass = 0; LPCWSTR lpName; BOOL fTryMessage = FALSE; if (ccxlpszClass != NULL) { /* * note that we do a version-less check here, then call FindClassAtom right away. */ atomClass = FindClassAtom(ccxlpszClass); if (atomClass == 0) { return NULL; } } /* * Setup parent window */ if (!pwndParent) { pwndParent = _GetDesktopWindow(); /* * If we are starting from the root and no child window * was specified, then check the message window tree too * in case we don't find it on the desktop tree. */ if (!pwndChild) fTryMessage = TRUE; }TryAgain: /* * Setup first child */ if (!pwndChild) { pwndChild = pwndParent->spwndChild; } else { if (pwndChild->spwndParent != pwndParent) { RIPMSG0(RIP_WARNING, "FindWindowEx: Child window doesn't have proper parent"); return NULL; } pwndChild = pwndChild->spwndNext; } /* * Generate a list of top level windows. */ if ((pbwl = BuildHwndList(pwndChild, BWL_ENUMLIST, NULL)) == NULL) { return NULL; } /* * Set pwnd to NULL in case the window list is empty. */ pwnd = NULL; try { for (phwnd = pbwl->rghwnd; *phwnd != (HWND)1; phwnd++) { /* * Validate this hwnd since we left the critsec earlier (below * in the loop we send a message! */ if ((pwnd = RevalidateHwnd(*phwnd)) == NULL) continue; /* * make sure this window is of the right type */ if (dwType != FW_BOTH) { if (((dwType == FW_16BIT) && !(GETPTI(pwnd)->TIF_flags & TIF_16BIT)) || ((dwType == FW_32BIT) && (GETPTI(pwnd)->TIF_flags & TIF_16BIT))) continue; } /* * If the class is specified and doesn't match, skip this window * note that we do a version-less check here, use pcls->atomNVClassName */ if (!atomClass || (atomClass == pwnd->pcls->atomNVClassName)) { if (!ccxlpszName) break; if (pwnd->strName.Length) { lpName = pwnd->strName.Buffer; } else { lpName = szNull; } /* * Is the text the same? If so, return with this window! */ if (_wcsicmp(ccxlpszName, lpName) == 0) break; } /* * The window did not match. */ pwnd = NULL; } } except (W32ExceptionHandler(FALSE, RIP_WARNING)) { pwnd = NULL; } FreeHwndList(pbwl); if (!pwnd && fTryMessage) { fTryMessage = FALSE; pwndParent = _GetMessageWindow(); pwndChild = NULL; goto TryAgain; } return ((*phwnd == (HWND)1) ? NULL : pwnd);}??上面的代码和内核文件是一样的,整体阅读代码后,发现是从BuildHwndList产生的所谓的列表查找的,我们看一下它的源代码:
PBWL BuildHwndList( PWND pwnd, UINT flags, PTHREADINFO pti){ PBWL pbwl; CheckCritIn(); if ((pbwl = pbwlCache) != NULL) { /* * We're using the cache now; zero it out. */#if DBG pbwlCachePrev = pbwlCache;#endif pbwlCache = NULL;#if DBG { PBWL pbwlT; /* * pbwlCache shouldn't be in the global linked list. */ for (pbwlT = gpbwlList; pbwlT != NULL; pbwlT = pbwlT->pbwlNext) { UserAssert(pbwlT != pbwl); } }#endif } else { /* * sizeof(BWL) includes the first element of array. */ pbwl = (PBWL)UserAllocPool(sizeof(BWL) + sizeof(PWND) * CHWND_BWLCREATE, TAG_WINDOWLIST); if (pbwl == NULL) return NULL; pbwl->phwndMax = &pbwl->rghwnd[CHWND_BWLCREATE - 1]; } pbwl->phwndNext = pbwl->rghwnd; /* * We'll use ptiOwner as temporary storage for the thread we're * scanning for. It will get reset to the proper thing at the bottom * of this routine. */ pbwl->ptiOwner = pti;#ifdef OWNERLIST if (flags & BWL_ENUMOWNERLIST) { pbwl = InternalBuildHwndOwnerList(pbwl, pwnd, NULL); } else { pbwl = InternalBuildHwndList(pbwl, pwnd, flags); }#else pbwl = InternalBuildHwndList(pbwl, pwnd, flags);#endif /* * If phwndNext == phwndMax, it indicates that the pbwl has failed to expand. * The list is no longer valid, so we should just bail. */ if (pbwl->phwndNext >= pbwl->phwndMax) { UserAssert(pbwl->phwndNext == pbwl->phwndMax); /* * Even if we had picked pbwl from the global single cache (pbwlCache), * it should have already been unlinked from the global link list when it was put in the cache. * So we should just free it without manupilating the link pointers. * If we have allocated the pwbl for ourselves, we can simply free it. * In both cases, we should just call UserFreePool(). * As the side effect, it may make some room by providing a free pool block. */ UserFreePool(pbwl); return NULL; } /* * Stick in the terminator. */ *pbwl->phwndNext = (HWND)1;#ifdef FE_IME if (flags & BWL_ENUMIMELAST) { UserAssert(IS_IME_ENABLED()); /* * For IME windows. * Rebuild window list for EnumWindows API. Because ACCESS 2.0 assumes * the first window that is called CallBack Functions in the task is * Q-Card Wnd. We should change the order of IME windows */ pbwl = InternalRebuildHwndListForIMEClass(pbwl, (flags & BWL_REMOVEIMECHILD) == BWL_REMOVEIMECHILD); }#endif /* * Finally link this guy into the list. */ pbwl->ptiOwner = PtiCurrent(); pbwl->pbwlNext = gpbwlList; gpbwlList = pbwl; /* * We should have given out the cache if it was available */ UserAssert(pbwlCache == NULL); return pbwl;}??如果是项目是调试模式,就会有如下代码,这块代码值得我们注意:
PBWL pbwlT;/* * pbwlCache shouldn't be in the global linked list. */for (pbwlT = gpbwlList; pbwlT != NULL; pbwlT = pbwlT->pbwlNext) { UserAssert(pbwlT != pbwl);}??综上所述:窗体句柄是全局的。
??由于我们的教程主要是用来介绍操作系统是怎样执行的,所以我们只看看流程,不挖掘其细节。在3环,我们都是通过CreateWindow这个API来进行的,但它是一个宏,被翻译成CreateWindowEx。我们一步步跟下去如下结果所示:
??也就是说,所有的窗体都是0环绘制的。窗体的绘制都是win32k.sys来负责。
??消息机制篇——消息处理
