2004-12-10

Emulating WINKEY-L

De nagy az Isten állatkertje! Vesd össze Ctrl-Alt-Del, Enter

I've long been addicted to the combination of Win XP's “Fast User Switching” feature and the <WinKey>-L hotkey combination as a quick way to lock my PC when I step away from it for a moment.  I like to tell Keith it's because I'm extremely security conscious, but it probably has more to do with having 4 kids running around my house that like to see what Dad's working on when he's away from his computer and (worse) see if they can help me out with my work a bit.

But at some point after getting hooked on <WinKey>-L, I switched from Dell to IBM laptops (I use a laptop exclusively - the only proper desktop I have is a dual Xeon server in the basement).  ThinkPads, unfortunately, don't have a Windows Key.

For self-preservation reasons, I still locked my workstation when I left it alone, but it really irked me that I had to invoke the following sequence of hotkeys to pull it off:

CTRL-SHIFT-ESC (starts taskmgr)
ALT-U (select Shutdown menu)
S (switch user)

It didn't take long conclude (i.e., instantly) that I had to find a work around.  I probably could have googled up a solution, but being not so bright, and knowing about the existence of the Windows LockWorkStation function from some previous escapade, I figured I could whip up a little program on my own and drop a shortcut to it on my desktop with an assigned hotkey in less time than it took to find a solution on the web.

So I cranked up VS.NET (2002 at the time), ran the New Project wizard, selected a Win32 Project, named it LockWorkStation, and hit go.  Then I dropped into the wizard-generated code, which had a lot of goo in it I didn't need, and stripped it down to this:

int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
                        LPTSTR lpCmdLine, int nCmdShow )
{
    LockWorkStation();
}

I originally forgot to account for the fact that this function wasn't part of the original Win32 API, so my program failed to build.  But adding the following to stdafx.h before inclusion of windows.h fixed that problem:

#define _WIN32_WINNT 0x0500

A quick CTRL-SHIFT-B and I had myself a function LockWorkStation.exe.  Then I noticed the size of this one-line executable on disk: 167,936 bytes.  Yikes!

Of course, that was a debug build (the default build target) so I quickly toggled over to do a release build.  This yielded a more streamlined executable weighing in at 81,920 bytes.  Hmm.  Kind of hefty for a 1 line program that just jumped to an exported entrypoint in another DLL.

At this point, I started fiddling around with compiler and linker switches, but wasn't making any headway at all.  So I stopped what I was doing for a moment and dusted off an old assembly language Windows program I had laying around from years gone by.  I wanted to see just how small a 1 line Windows program I could make to understand what my goal was.  Here's what it looked like (comments stripped for brevity):

TITLE LockWorkStation

.386
.MODEL  FLAT, STDCALL

option casemap:none

ExitProcess     PROTO   WINAPI  :DWORD
LockWorkStation PROTO   WINAPI

.code

WinMain:
    invoke LockWorkStation
    xor eax, eax
    invoke ExitProcess, eax
END WinMain

Building this guy resulted in a functioning LockWorkStation executable measuring a svelt 2,048 bytes.  Now we're cookin' with gas!  Of course, it really ends up occupying 4096 bytes in memory (a page), but that's not my fault.

Being a stubborn person, and evidently having lots of free time on my hands that day, I went back to VS.NET determined to harrass it into producing an executable image of equivalent size.  My starting point was the 81,920 byte release build.

The first thing I did (not that I can tell you why it was the first thing I did) was to tell the linker to not include all the default libraries it normally includes.  This was done as follows (path to settings on the project settings dialog box):

Configuration Properties\Linker\Input
  Ignore All Default Libraries = Yes (/NODEFAULTLIB)

This change necessitated a tweak to my code so that the release build version of it defined its own WinMainCRTStartup function, which had previously been included in one of the default libraries.  My revamped app now looked like so:

#ifdef _DEBUG
int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
                        LPTSTR lpCmdLine, int nCmdShow)
#else
// Release build linker settings includes /NODEFAULTLIB.
//
int APIENTRY WinMainCRTStartup( HINSTANCE hInstance, HINSTANCE hPrevInstance,
                                LPTSTR lpCmdLine, int nCmdShow )
#endif
{
    LockWorkStation();
    return(0);

}

This change produced an executable that was 61,440 bytes.  Not a huge change, but on the right track at least.  The next change I made was to disable a Windows 98-specific optimization.  I have no idea what this optimization does, but I knew I didn't need it for a program that would only run on XP:

Configuration Properties\Linker\Optimization
  Optimize for Windows98 = No (/OPT:NOWIN98)

New image size: 51,200 bytes.  So far, I was batting a thousand - each change I'd made reduced the size of the image a bit.  But that didn't last long.  I then made a bunch of changes to the compiler's “optimization” settings:

Configuration Properties\C/C++\Optimization
  Optimization = Minimize Size (/O1)
  Global Optimization = Yes (/Og)
  Inline Function Expansion = Only __inline (/Ob1)
  Favor Size or Speed = Favor Small Code (/Os)
  Omit Frame Pointers = Yes (/Oy)
  Optimize for Windows Application = Yes (/GA)

New image size: 51,200 bytes.  No change.  Not what I'd expected.  But you gotta love a compiler switch called “oh be one”.  Leaving those changes in place, I moved onto the next configuration area, making another shotgun blast at settings that sounded even remotely like they might make my program smaller:

Configuration Properties\C/C++\Code Generation
  Enable String Pooling = Yes
  Enable C++ Exceptions = No
  Buffer Security Check = No
  Enable Function-Level Linking = Yes (/Gy)

New image size: 51,200 bytes.  Hrumph.

At this point, I realized that the project wizard had generated and included some resources for me that I surely didn't need for this program.  So I removed all the files from my solution that were included under the “Resources” folder (*.ico, *.rc).

New image size: 51,200 bytes.  Huh.

Then I hit the motherload:

Configuration Properties\C/C++\Advanced
  Compile As = Compile as C Code (/TC)

New image size: 2,048 bytes.  Ahhh.  Finally.  A proper VS.NET solution that generated the smallest Windows program apparantly possible.

Of course, I also realized that I had no clue which one, or combination, of the above changes actually caused the effect I finally achieved.  So I started up another default wizard-generated project and just made the /TC change.  That alone didn't work.  So I backed that out and made just one other change.  That didn't work either.  I played around a bit to see if I could find a smaller subset of the switches that still resulted in the 2,048 byte executable, but I never found my way back to a 2,048 byte image and eventually had to get back to paying work and gave up.  I'm pretty sure not all of the switches I ended up throwing need to be thrown, but I'm also pretty sure more than one needs to be thrown.  Somewhere in between the default settings and what I ended up is just the right set of switches that will result in a 2,048 byte version of my LockWorkStation program.  But I'd run out of patience looking for it.  Having solved my original problem, I decided it was time to move on.

So I dropped a shortcut to my LockWorkStation.exe on my desktop, configured it's hotkey as CTRL-SHIFT-L, and I was in business.  It's 3 keys instead of the 2 required by <WinKey>-L, but it beats the pants off the 6 key taskmgr hack (or letting my kids have open season on my laptop).

But it was fun dusting off my assembly language skills and running ml.exe for the first time in something like 6 years.  And only another 2 years to blog about it after being asked several times by colleagues what hotkey I was using to lock my ThinkPad.  :-)

A copy of the code, including a prebuilt version of the 2,048 byte exe, is here if you're interested.  Keep those workstations locked!

[Via Pluralsight Blogs]