Date: | August 9, 2006 / year-entry #268 |
Tags: | history |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20060809-18/?p=30183 |
Comments: | 9 |
Summary: | The mechanism for keeping track of window hooks was very different in 16-bit Windows. The functions involved were SetWindowsHook, UnhookWindowsHook and DefHookProc. The first two functions still exist today, but the third one has been replaced with a macro: // 16-bit prototype DWORD WINAPI DefHookProc(int nCode, WPARAM wParam, LPARAM lParam, HHOOK FAR *phk); // 32-bit... |
The mechanism for keeping track of window hooks was very
different in 16-bit Windows.
The functions involved were // 16-bit prototype DWORD WINAPI DefHookProc(int nCode, WPARAM wParam, LPARAM lParam, HHOOK FAR *phk); // 32-bit macro #define DefHookProc(nCode, wParam, lParam, phhk)\ CallNextHookEx(*phhk, nCode, wParam, lParam) Disclaimer: All code below is "reconstructed from memory". The spirit of the code is intact, but the precise details may be off.
To install a windows hook in 16-bit Windows, you started by
calling HHOOK g_hhkPrev; g_hhkPrev = SetWindowsHook(WH_WHATEVER, MyHookProc);
The return value from // In Win16, hook procedures returned a DWORD, not an LRESULT. DWORD CALLBACK MyHookProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev); }
And then when you were finished, you removed the hook by
calling UnhookWindowsHook(WH_WHATEVER, MyhookProc); g_hhkPrev = NULL; Internally, the chain of hook functions was managed as a linked list, but instead of using some internal data structure to keep track of the hooks, the linked list was managed inside the HHOOK variables themselves.
The internal implementation of // This array is initialized with a bunch // of "do nothing" hook procedures. HOOKPROC g_rgHook[NUMHOOKS]; HHOOK WINAPI SetWindowsHook(int nType, HOOKPROC pfnHookProc) { HHOOK hhkPrev = (HHOOK)g_rgHook[nType]; g_rgHook[nType] = pfnHookProc; return hhkPrev; } Installing a hook merely set your hook procedure as the head of the hook chain, and it returned the previous head. Invoking a hook was a simple matter of calling the hook at the head of the chain: DWORD CallHook(int nType, int nCode, WPARAM wParam, LPARAM lParam) { return g_rgHook[nType](nCode, wParam, lParam); }
Each hook procedure did its work and then sent the call
down the hook chain by calling DWORD WINAPI DefHookProc(int nCode, WPARAM wParam, LPARAM lParam, HHOOK FAR *phk) { HOOKPROC pfnNext = (HOOKPROC)*phk; if (nCode >=0) { return pfnNext(nCode, wParam, lParam); } ... more to come ... }
As you can see, it's all blindingly simple:
Invoking a hook calls the first hook procedure,
which then calls
The real magic happens when somebody wants to unhook.
Recall that the rule for hook procedures is that a negative
hook code should be passed straight to BOOL WINAPI UnhookWindowsHook(int nType, HOOKPROC pfnHookProc) { return DefHookProc(-1, 0, (LPARAM)pfnHookProc, (HHOOK FAR*)&g_rgHook[nType]); } And then the real magic begins: DWORD WINAPI DefHookProc(int nCode, WPARAM wParam, LPARAM lParam, HHOOK FAR *phk) { HOOKPROC pfnNext = (HOOKPROC)*phk; if (nCode >=0) { return pfnNext(nCode, wParam, lParam); } switch (nCode) { case -1: // trying to unhook a node if (pfnNext == (HOOKPROC)lParam) { // found it *phk = (HHOOK)pfnNext(-2, 0, 0); return TRUE; } // else keep looking return pfnNext(nCode, wParam, lParam); case -2: // report the next hook procedure return (DWORD)*phk; } return 0; } And there you have it, the entire window hook system in two dozen lines of code. You have to give 16-bit Windows credit for being small.
Let's walk through hook installation, dispatch, and removal to see
how this all works.
Suppose there is one // In USER g_rgHook[WH_KEYBOARD] = Hook1; // In HOOK1.DLL HHOOK g_hhkPrev1 = DoNothingHookProc; DWORD CALLBACK Hook1(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... work ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev1); }
Now suppose you want to install a new hook, // In HOOK2.DLL HHOOK g_hhkPrev2; g_hhkPrev = SetWindowsHook(WH_KEYBOARD, Hook2);
The // In USER g_rgHook[WH_KEYBOARD] = Hook2; // In HOOK2.DLL HHOOK g_hhkPrev2 = Hook1; DWORD CALLBACK Hook2(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... work ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev2); } // In HOOK1.DLL HHOOK g_hhkPrev1 = DoNothingHookProc; DWORD CALLBACK Hook1(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... work ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev1); }
Now suppose the window manager decides it's time to fire the
Now suppose that
Like a good hook function,
This dispatch calls Observe that at the end of this unhook exercise, we get the desired result: // In USER g_rgHook[WH_KEYBOARD] = Hook2; // unchanged // In HOOK2.DLL g_hhkPrev2 = DoNothingHookProc; // updated! DWORD CALLBACK Hook2(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... work ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev2); }
And
This really isn't all that complicated.
All we did was delete a node from a linked list.
It's just that this particular linked list cannot be traversed
by just dereferencing pointers.
Instead, we have to issue a function call and ask the
recursive function to perform the work on the "next" node for us.
That's what the negative Every time I work through this exercise, I am impressed by how compactly 16-bit Windows was written. In just two dozen lines of code, we managed a linked list of function calls, including a dispatching system as well as arbitrary deletion from the middle of the linked list, and all without any memory allocation. (And because I know people are going to try to change the topic: Remember, I'm talking about 16-bit Windows, not 32-bit window hooks.) Next time, we'll look at one way people abused this simple system. |
Comments (9)
Comments are closed. |
This is really clever!
But it also ‘looks’ fragile as it relies on user functions doing the right thing in order for the whole mechanic to work!
Since I am quite a nice guy (no, really ;) ), I am waiting for your tomorrow’s post to see what evil deeds people managed to do with that!
How perceptive of you, Nawak. ;)
I was rather frustrated by programming under 16-bit Windows, not because of Windows itself, but purely because other developers had chosen to implement strange custom solutions to problems that had established "correct" answers. It was reasonably common that someone would reverse-engineer the contents of a handle – which was deliberately undocumented and unsupported, because it could change at any time – and access it directly.
In the HHOOK case, for example, someone might try to duplicate the above logic himself rather than use the standard Windows calls. If he happened to get something wrong, probably because he didn’t really understand the process, the system would behave oddly and nobody would know why. I suspect that may form the basis of Raymond’s next post.
I love opaque data structures. It’s a relief not knowing how a class or data structure is implemented because otherwise you are tempted to program through the interface instead of to the interface (e.g. guess the implementation by the members of the class so you can ‘optimize’ how you call its methods).
I would have to say that if you are using a debugger to find undocumented members of structs/classes and relying on them being there: a) you’re doing something wrong; and b) you get what you deserve.
I understand that Microsoft makes lots of thunks for broken software (see http://blogs.msdn.com/oldnewthing/archive/2006/01/09/510781.aspx for more information), and I can understand why. But it sure would be nice to say "to heck with them" when somebody pokes around internal undocumented structures and expects things to work that way forever.
In regards to hook dispatching, I can’t imagine how expensive it is for more than one process to have a hook on a high-traffic message. How does Win32 make this work without a ton of context switches?
Tom: The hook function must reside in a DLL. That DLL must/will be loaded in each target process. So, in addition to message processing, you add clutter the virtual address space and some loading time. (More so if the hook function is just one function in a heavy DLL that’s then brought into every process.)
CN: 16-bit windows didn’t have "virtual address space", it had segmenting at best. Only the (4k) pages that is used is loaded into ram, usually not very much.
Unhooking by hooking again.
It seems an app that passed a copy of the HHOOK it originally got would defeat the unhooking mechanism. e.g.
DWORD CALLBACK MyHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
…
HHOOK hhkLocal = g_hhkPrev;
return DefHookProc(nCode, wParam, lParam, &hhkLocal);
}
Why do this? Who knows: Given the lack of a user parameter I can imagine a scenario where parts of the code use their own copies of an HHOOK.
@swapping, @DN
Win16 enhanced mode had demand-paged virtual memory. In any mode, all running Win16 applications were in one memory space (accessed by the app through segment registers, it is true). The hook DLL would not have to be mapped into each target process, because the DLL was already jumpable into through a far call from any running application.