Export (0) Print
Expand All
Expand Minimize
2 out of 3 rated this helpful - Rate this topic

Diagnosing Memory Leaks

 

Mike Hall
Microsoft Corporation

Steve Maillet
Entelechy Consulting

October 15, 2002

Summary: Looks at the tools and functionality built into Microsoft® Windows® CE .NET 4.1 that can be used to track memory leaks in a custom operating system, specifically, debug zones, Remote Performance Monitor, and LMEMDEBUG. (15 printed pages)


Hopefully, you have been following along with the Microsoft Windows CE projects. If so, you should be comfortable building operating system images, and you've probably written some of your own code to run on top of Windows CE—either an application or perhaps drivers. This month's article focuses on memory leaks and how to track them down.

I know you are all great developers. Your code is always cleanly written with more comments than you can shake a stick at. The code is super easy to maintain, and contains no memory leaks or bugs... Okay, now back to the real world.

We can all make mistakes in our code, especially if we're on a time crunch or are simply on a roll and hammering out code deep into the night. Once we have our code complete, how do we determine whether it contains a memory leak? Better still, how do we track the leak back to source code and fix the issue? In this month's article, we will look at debug zones, Remote Performance Monitor, and LMEMDEBUG.

We will use a simple, console-based application to help illustrate some of the tools and features of Windows CE. The sample is called memLeak. This (as the name implies) leaks memory (no surprises there). The application has two threads, a main thread that spins in a sleep loop so that the application doesn't exit, and a child thread that allocates memory every 500ms. We will use this application to illustrate the use of debug zones, the Remote Performance Monitor, and LMEMDEBUG. There are a series of functions called in the memLeak application. These are (in order):

  • AllocateMemory( )
  • UseMemory( )
  • FreeMemory( )

You can get a feel for the purpose of the functions by simply looking at the function names. AllocateMemory checks the current memory load. If we're below 60% load, we allocate 2048*TCHAR and then move on. UseMemory does something useful with the memory, and FreeMemory hopefully frees the memory we've allocated.

Here's the complete application:

// memLeak.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <celog.h>

#define MAX_LOADSTRING 100

        // Definitions for our debug zones                               
        #define ZONEID_INIT      0
        #define ZONEID_TRACE     1
        #define ZONEID_MEMORY    2
        #define ZONEID_RSVD3     3
        #define ZONEID_RSVD4     4
        #define ZONEID_RSVD5     5
        #define ZONEID_RSVD6     6
        #define ZONEID_RSVD7     7
        #define ZONEID_RSVD8     8
        #define ZONEID_RSVD9     9
        #define ZONEID_RSVD10   10
        #define ZONEID_RSVD11   11
        #define ZONEID_RSVD12   12
        #define ZONEID_RSVD13   13        
        #define ZONEID_WARN     14
        #define ZONEID_ERROR    15

        // These masks are useful for initialization of dpCurSettings
        #define ZONEMASK_INIT      (1<<ZONEID_INIT) 
        #define ZONEMASK_TRACE     (1<<ZONEID_TRACE) 
        #define ZONEMASK_MEMORY    (1<<ZONEID_MEMORY) 
        #define ZONEMASK_RSVD3     (1<<ZONEID_RSVD3) 
        #define ZONEMASK_RSVD4     (1<<ZONEID_RSVD4) 
        #define ZONEMASK_RSVD5     (1<<ZONEID_RSVD5) 
        #define ZONEMASK_RSVD6     (1<<ZONEID_RSVD6) 
        #define ZONEMASK_RSVD7     (1<<ZONEID_RSVD7) 
        #define ZONEMASK_RSVD8     (1<<ZONEID_RSVD8) 
        #define ZONEMASK_RSVD9     (1<<ZONEID_RSVD9) 
        #define ZONEMASK_RSVD10    (1<<ZONEID_RSVD10)
        #define ZONEMASK_RSVD11    (1<<ZONEID_RSVD11)
        #define ZONEMASK_RSVD12    (1<<ZONEID_RSVD12)
        #define ZONEMASK_RSVD13    (1<<ZONEID_RSVD13)        
        #define ZONEMASK_WARN      (1<<ZONEID_WARN )  
        #define ZONEMASK_ERROR     (1<<ZONEID_ERROR) 
 
#ifdef DEBUG
        // These macros are used as the first arg to DEBUGMSG
        #define ZONE_INIT       DEBUGZONE(ZONEID_INIT)
        #define ZONE_TRACE      DEBUGZONE(ZONEID_TRACE)
        #define ZONE_MEMORY     DEBUGZONE(ZONEID_MEMORY)
        #define ZONE_RSVD3      DEBUGZONE(ZONEID_RSVD3)
        #define ZONE_RSVD4      DEBUGZONE(ZONEID_RSVD4)
        #define ZONE_RSVD5      DEBUGZONE(ZONEID_RSVD5)
        #define ZONE_RSVD6      DEBUGZONE(ZONEID_RSVD6)
        #define ZONE_RSVD7      DEBUGZONE(ZONEID_RSVD7)
        #define ZONE_RSVD8      DEBUGZONE(ZONEID_RSVD8)
        #define ZONE_RSVD9      DEBUGZONE(ZONEID_RSVD9)
        #define ZONE_RSVD10     DEBUGZONE(ZONEID_RSVD10)
        #define ZONE_RSVD11     DEBUGZONE(ZONEID_RSVD11)
        #define ZONE_RSVD12     DEBUGZONE(ZONEID_RSVD12)
        #define ZONE_RSVD13     DEBUGZONE(ZONEID_RSVD13)
        #define ZONE_WARN       DEBUGZONE(ZONEID_WARN )
        #define ZONE_ERROR      DEBUGZONE(ZONEID_ERROR)
#endif

DBGPARAM dpCurSettings = {
    TEXT("MemLeak"), {
        TEXT("Init"),TEXT("Trace Fn( );"),TEXT("Memory"),TEXT(""),
        TEXT(""),TEXT(""),TEXT(""),TEXT(""),
        TEXT(""),TEXT(""),TEXT(""),TEXT(""),
        TEXT(""),TEXT(""),TEXT(""),TEXT("")},
    // By default, turn on the zones for init and errors.
    ZONEMASK_INIT    
}; 


DWORD WINAPI MemoryThread(LPVOID lpParameter);
void AllocateMemory( );
void FreeMemory( );
void StartAllocation( );
void UseMemory( );


MEMORYSTATUS g_MemStatus;
HLOCAL g_tcTemp,g_tc_Temp;
DWORD dwThreadID;
TCHAR szMessage[256];

int WINAPI main (HINSTANCE hInstance, 
HINSTANCE hInstPrev, LPWSTR pCmdLine, int nCmdShow)
{
   OutputDebugString(L"leakApp Starting\n");

   DEBUGREGISTER(NULL);      // Register the debug zones

   StartAllocation( );
   while(true) {
      Sleep(1000);
   }
   return 0;
}

void StartAllocation( )
{
   g_tcTemp=NULL;
   g_tc_Temp=NULL;

   OutputDebugString(L"Creating Thread...\n");
   CreateThread(NULL,0,
(LPTHREAD_START_ROUTINE)MemoryThread,
(LPVOID)0,0,&dwThreadID);
}

DWORD WINAPI MemoryThread(LPVOID lpParameter)
{
   while(TRUE) {
      Sleep(500);
      DEBUGMSG (ZONE_TRACE, (TEXT("-------------------------\n")));
      AllocateMemory( );
      UseMemory( );
      FreeMemory( );
   }
}


void AllocateMemory( )
{
   DEBUGMSG (ZONE_TRACE, (TEXT("Enter - AllocateMemory( ) Function\n")));

   DEBUGMSG (ZONE_MEMORY, (TEXT("Check GlobalMemoryStatus( )\n")));
   memset(&g_MemStatus,0x00,sizeof(g_MemStatus));
   g_MemStatus.dwLength=sizeof(g_MemStatus);
   GlobalMemoryStatus(&g_MemStatus);
   DEBUGMSG (ZONE_MEMORY, 
  (TEXT("Memory Load %d%%\n"),
  g_MemStatus.dwMemoryLoad));

   CELOGDATA(TRUE, CELID_RAW_LONG, &g_MemStatus.dwMemoryLoad, 
(WORD) (sizeof(DWORD)), 1, CELZONE_MISC);
   
   if (g_MemStatus.dwMemoryLoad < 60) {
      DEBUGMSG (ZONE_MEMORY, 
       (TEXT("Allocate TCHAR *2048 (4096 UNICODE Characters)\n")));
      g_tcTemp=LocalAlloc(LPTR,(2048*sizeof(TCHAR)));   
      DEBUGMSG (ZONE_MEMORY, (TEXT("Pointer 0x%lx\n"),g_tcTemp));

   } else {
      DEBUGMSG (ZONE_MEMORY, 
               (TEXT("Memory Load too high - not allocating memory \n"),
               g_MemStatus.dwMemoryLoad));
   }

   DEBUGMSG (ZONE_TRACE, (TEXT("Leave - AllocateMemory( ) Function\n")));
}

void FreeMemory( )
{
   DEBUGMSG (ZONE_TRACE, (TEXT("Enter - FreeMemory( ) Function\n")));
   DEBUGMSG (ZONE_MEMORY, (TEXT("Free Pointer 0x%lx\n"),g_tc_Temp));
   LocalFree(g_tc_Temp);
   DEBUGMSG (ZONE_TRACE, (TEXT("Leave - FreeMemory( ) Function\n")));
}

void UseMemory( )
{
   DEBUGMSG (ZONE_TRACE, (TEXT("Enter - UseMemory( ) Function\n")));
   DEBUGMSG (ZONE_MEMORY, (TEXT("Do Something Interesting here.\n")));
   DEBUGMSG (ZONE_TRACE, (TEXT("Leave - UseMemory( ) Function\n")));

}

There are a number of ways in which we can track the flow of a running program. Perhaps the simplest way is the use of OutputDebugString( ). This function can be used to output any useful information from our code. Unfortunately, we get the debug message whether we want it or not, which can perhaps lead to information overload.

It may be useful to switch on debug messages as and when we need them. For example, tracking the entry point and exit points of functions within our code would provide code flow information as our application runs. We may not need this to be running all the time, but it could be useful to determine the flow of code when we're tracking down leaks, or crashes. So how do we enable this?

The answer is debug zones. Most modules within the Windows CE operating system have debug zones enabled. Take a look at the following screen shot from Platform Builder. (The dialog box is displayed in Platform Builder using Target | CE Debug Zones.) This shows the debug zones exposed from GWES.exe. You can see that 16 zones are exposed (0-15), and that by default, zone 6, Warnings, is enabled.

Figure 1. Debug Zones dialog box from Platform Builder

Debug zones provide the ability to selectively turn debug message output from your code on and off. This allows you to trace the execution of your code without halting the operating system. Tracing is a simple and non-intrusive way of catching problems in code without causing the operating system to stop responding. Debug zones can be enabled either through Target Control (Target | CE Target Control), or through the Platform Builder IDE (Target | CE Debug Zones).

Each application or module (driver, DLL, etc.) can contain 16 debug zones. The purpose of each zone is not fixed; you can code each zone to display information appropriate to your current project. You may only need one or two zones in your application/driver. These may be used to track the entry/exit point of functions (code tracing). You may also be interested in memory allocations/free; code tracing or memory allocations could be coded as debug zones for your application or driver.

Debug zones are implemented by declaring a DBGPARAM structure, which is defined in dbgapi.h. The DBGPARAM structure contains three elements: the module name, the zone names, and a bitmask showing which zones are enabled by default. This shows how the DBGPARAM structure from the memLeak program is filled out:

DBGPARAM dpCurSettings = {
    TEXT("MemLeak"), {
        TEXT("Init"),TEXT("Trace Fn( );"),TEXT("Memory"),TEXT(""),
        TEXT(""),TEXT(""),TEXT(""),TEXT(""),
        TEXT(""),TEXT(""),TEXT(""),TEXT(""),
        TEXT(""),TEXT(""),TEXT(""),TEXT("")},
    // By default, turn on the zones for init and errors.
    ZONEMASK_INIT    
}; 

We can see the name of the module 'memLeak' and then 16 strings defining the human readable names of the zones within our code. (These are the names displayed in the Platform Builder 'Debug Zones' Dialog.) In this case, we're exposing three zones: Init, Trace Fn( ), and Memory The third section of the DBGPARAM structure defines which zones are enabled by default—we have the init zone enabled.

So, how do we enable debug zones in our application? Simple. We define a DBGPARAM structure, called dpCurSettings, and on initialization of our program or module, we call DEBUGREGISTER( ). The syntax for the DEBUGREGISTER macro is DEBUGREGISTER(hMod | NULL). If you are debugging a .dll, call DEBUGREGISTER with the appropriate hModule, and pass NULL if you are building an application.

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
                     )

So far so good. We know how to fill out the structure and how to register the debug zones with the debugger. This will simply show the list of zones inside Platform Builder (menu item Target | CE Debug Zones). So how do we use debug zones in our code? Again, this is simple. Let's examine the Trace Fn( ) debug message and how this is enabled. Here's a sample line of code from the memLeak application:

   DEBUGMSG (ZONE_TRACE, (TEXT("Enter - AllocateMemory( ) Function\n")));

We can see the call to DEBUGMSG( ). This function takes two parameters: a bit test and a string. The bit test can be used in a couple of different ways. If you always want the debug message to appear, you can simply use DEBUGMSG(1,L"String"). Notice the numeric one is the first parameter—or, we could test to see whether one of our debug zones is enabled using a bit test similar to the following DEBUGZONE(ZONEID_INIT,L"String"). That's as hard as it gets—either bit-test, or use numeric 1 as the first parameter to DEBUGZONE( ). We simply need a way to determine whether our specific debug zone is enabled; if so, output the debug string.

So how exactly does this help us with tracking memory leaks? We can include a MemoryAllocate, and a MemoryFree function in our applications. This can (at the basic level) track the number of memory allocations, increment a global variable, and output a debug message with the current number of allocations. Obviously, the MemoryFree function will decrement this global number and also output a debug message. Once our program run is complete, we can determine whether we have a leak by examining the value of the global variable. Hopefully, this number will be zero (in which case we have matching memory allocations and frees); we're in trouble if the number is either positive, or negative.

So, we have our application/driver with debug zones enabled, how do we use these from Platform Builder? There are two options. The first is to use the Target | CE Debug Zones... menu option. This will list all of the running processes/modules; you can then select from the list and enable/disable the appropriate zones.

Figure 2. Setting memLeak debug zones

The second way to work with debug zones is through the CE Target Control Window (Target | CE Target Control). If you've been working with Windows CE since version 2.0, this will be VERY familiar to you. The target control window provides a command-line interface to some of the common tasks when developing/debugging a Windows CE operating system image. These include getting a list of running processes, starting processes, and stopping processes, and setting debug zones on applications or modules.

Here's how I enabled memory tracing on memLeak using the Target Control window:

  • gi proc displays a list of running processes. You will notice that memLeak is process #6.
  • zo p 6 lists the debug zones for process #6. Only init is enabled; this is displayed with a '*' next to the zone name.
  • zo p 6 on 2 turns on zone bit 2 for process #6 (memLeak).

We could also terminate the process using kp 6 (kill process #6). Here is how the above commands look in the Target Control window.

Windows CE>gi proc
PROC: Name            hProcess: CurAKY :dwVMBase:CurZone
 P00: NK.EXE          0dfff002 00000001 c2000000 00000100
 P01: filesys.exe     2dff4416 00000002 04000000 00000000
 P02: shell.exe       4dfd7442 00000004 06000000 00000001
 P03: device.exe      8dfd750a 00000008 08000000 00000004
 P04: gwes.exe        adedd692 00000010 0a000000 00000040
 P05: services.exe    0ded0316 00000020 0c000000 00000001
 P06: memleak.exe     2de7631e 00000040 0e000000 00000001
Windows CE>zo p 6
Registered Name:MemLeak   CurZone:00000001
Zone Names - Prefixed with bit number and * if currently on
 0*Init            : 1 Trace Fn( );    : 2 Memory          : 3      
 4                 : 5                 : 6                 : 7  
 8                 : 9                 :10                 :11  
12                 :13                 :14                 :15    
Windows CE>zo p 6 on 2
Registered Name:MemLeak   CurZone:00000005
Zone Names - Prefixed with bit number and * if currently on
 0*Init            : 1 Trace Fn( );    : 2*Memory          : 3    
 4                 : 5                 : 6                 : 7    
 8                 : 9                 :10                 :11    
12                 :13                 :14                 :15     
Windows CE>

Here's how the debug zone output looks. We can see that function tracing is enabled, and memory allocation/free is also enabled. This shows the current memory load, and the pointer returned from the LocalAlloc( ) function.

555568 PID:edee9902 TID:de7628a 0x8de766ac: ----------------------------------
 555568 PID:edee9902 TID:de7628a 0x8de766ac: Enter - AllocateMemory( ) Function
 555568 PID:edee9902 TID:de7628a 0x8de766ac: Check GlobalMemoryStatus( )
 555569 PID:edee9902 TID:de7628a 0x8de766ac: Memory Load 13%
 555569 PID:edee9902 TID:de7628a 0x8de766ac: Allocate TCHAR *2048 
                                            (4096 UNICODE Characters)
 555570 PID:edee9902 TID:de7628a 0x8de766ac: Pointer 0x159550
 555570 PID:edee9902 TID:de7628a 0x8de766ac: Leave - AllocateMemory( ) Function
 555571 PID:edee9902 TID:de7628a 0x8de766ac: Enter - UseMemory( ) Function
 555572 PID:edee9902 TID:de7628a 0x8de766ac: Do Something Interesting here.
 555572 PID:edee9902 TID:de7628a 0x8de766ac: Leave - UseMemory( ) Function
 555572 PID:edee9902 TID:de7628a 0x8de766ac: Enter - FreeMemory( ) Function
 555572 PID:edee9902 TID:de7628a 0x8de766ac: Free Pointer 0x0
 555572 PID:edee9902 TID:de7628a 0x8de766ac: Leave - FreeMemory( ) Function
 556074 PID:edee9902 TID:de7628a 0x8de766ac: -----------------

Debug zones can be useful in tracking a variety of items within our code, including memory allocation. We can also run our application over time and examine the operating system memory load. We would expect the memory load to stay flat over the run time of our application. Obviously, if the memory load increases over time, then this is an indication of a potential memory leak.

There are a couple of ways in which we can track memory load. The first is to call the GlobalMemoryStatus( ) API within our code, and use OutputDebugMessage( ) or DEBUGMSG( ) to output a debug message showing the current load (perhaps using a debug zone to control the output of this message).

We can also make use of the Remote Performance Monitor. This tool provides the ability to track a number of items within the Windows CE operating system. (It's actually very similar to the desktop PerfMon application.) We can, for example, track Remote Access Server (RAS), Internet Control Message Protocol (ICMP), TCP IP, User Datagram Protocol (UDP), memory, battery, system, process, and thread activity within the Windows CE operating system.

In our case, we can run the Remote Performance Monitor and monitor memory load. Here's how the Performance Monitor application looks when we're running memLeak. You can see that memory load is steadily increasing over time. For some applications, this may be normal behavior. Perhaps the application is caching information into memory from a connected data source You would expect the application to flush this captured information to the file system, or to a remote server—perhaps calling an XML Web service or using MSMQ, and therefore the memory load should reduce.

Figure 3. Remote Performance Monitor

At this point, we've seen that debug zones and Remote Performance Monitor can be useful in determining whether we have a leak in our code. This doesn't necessarily help us track down the cause of the leak. This is where LMEMDEBUG can be useful. Let's take a look at LMEMDEBUG.

The goal of LMEMDEBUG is to allow developers to get a better understanding of memory allocations within their device. LMEMDEBUG can be added to a platform directly from the Platform Builder catalog. I'm currently working with a headless platform to test out the sample code for this month's article. The LMEMDEBUG component can be found here in the Platform Builder catalog: Core OS | Headless Devices | Core OS Services | Debugging Tools | LMemDebug memory debugging hooks. (Note that LMEMDEBUG is also available for display-based devices.)

LMEMDEBUG can be used for memory leak tracing as well as performance analysis. There are two logical parts to LMEMDEBUG: the hooks included in the Core operating system components and a sample installable DLL provided in source code, the location of which is C:\WINCE410\PUBLIC\COMMON\OAK\DRIVERS\LMEMDEBUG. The sample DLL exposes memory-tracking functions. When CoreDLL is loaded, it will call the LMemInit() function of heap.c. This function has been changed for Windows CE .NET 4.1 to attempt to load a DLL named LMemDebug.DLL. If this DLL can be loaded into the process space, then it will do a GetProcAddress() of the following functions:

HeapCreate
HeapDestroy
HeapAlloc
HeapAllocTrace
HeapReAlloc
HeapFree
HeapSize

You can implement all or a subset of the functions in your implementation of the LMemDebug DLL. Since all of the Local* functions (like LocalAlloc()) call into the HeapAlloc functions, all of those will be redirected. Malloc, calloc and new also go into the Local* functions and then to the Heap* functions. CoreDLL also now exports Int_ (Internal) versions of all of these functions. This allows you to call into the current implementations as a lower layer of your implementation. At a minimum, your functions could simply call into the internal implementations and keep a simple count of the total allocations or total number of bytes allocated. A simple implementation could perhaps implement HeapAlloc/HeapFree as:

LPVOID WINAPI HeapAlloc(HANDLE hHeap, DWORD dwFlags, DWORD dwBytes)
{
    v_NumAllocs++;
    return Int_HeapAlloc (hHeap, dwFlags, dwBytes);
}

BOOL WINAPI HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem)
{
    v_NumAllocs--;
    return Int_HeapFree (hHeap, dwFlags, lpMem);
}

The HeapAlloc and HeapFree functions could use OutputDebugString( ) to show the current number of allocations. Again, this is fine, but how do we use LMemDebug to track a leak? We already know that memLeak leaks memory; we can clearly see this from the Remote Performance Monitor output. Now let's take a look at the debug output when we close our application:

315130 PID:adee97b2 TID:aded0af2 0x8dee383c:       
       Ptr=0x0004B410 Size=4096 Count=30 
LineNum=137 File=C:\WINCE410\PUBLIC\Small\memLeak\memLeak.cpp
 315130 PID:adee97b2 TID:aded0af2 0x8dee383c:         Stack=0x03FBED36
 315130 PID:adee97b2 TID:aded0af2 0x8dee383c:         Stack=0x03FBD52A
 315130 PID:adee97b2 TID:aded0af2 0x8dee383c:         Stack=0x0E0111EE
 315131 PID:adee97b2 TID:aded0af2 0x8dee383c:         Stack=0x0E0110D6
 315131 PID:adee97b2 TID:aded0af2 0x8dee383c:         Stack=0x03FB6136

Now this looks interesting. LMEMDEBUG will output a stack trace, the amount of memory allocated, a file name, and line number for each allocation that wasn't tidied up by the host application, in this case memLeak. We can now quickly jump to line 137 of memLeak.cpp in the Platform Builder IDE to see where the memory was allocated. In this case, it's the following line of code:

g_tcTemp=LocalAlloc(LPTR,(2048*sizeof(TCHAR)));   

We can now see one location within our code that may be the culprit for leaking memory—the call to LocalAlloc( ) where we allocate 2048*TCHAR. This translates to 4096 bytes. It may be useful to "break" our application when the allocation of 4096 bytes takes place, and then single step the code to determine where the LocalFree( ) is either missing or failing to free the memory (perhaps we're passing an incorrect parameter into the call). LMEMDEBUG also provides a mechanism for trapping the initial allocation and forcing a DebugBreak( ) when this occurs. To enable this feature, we need to use the Target Control window (Target | CE Target Control).

At a Target Control prompt, you can type "?" to get a list of available commands. In our case, we want to force a breakpoint when an allocation within memLeak occurs of 4096 bytes. The command-line option for this is lmem memleak breaksize 4096. The command line simply requests a DebugBreak( ) to fire when 4096 bytes is allocated in the process memLeak. The call to DebugBreak( ) will take place in the HeapAllocTrace( ) function within LMEMDEBUG. We can use the Call Stack window within Platform Builder to step back into our application and single step from that point forward.

Here's how the Call Stack looks for the memLeak application once DebugBreak( ) has been called from LMEMDEBUG.

Figure 4. Call Stack looking for memLeak

Conclusion

In this month's article, we've looked at some of the tools and functionality built into Windows CE .NET 4.1 that can be used to track memory leaks in a custom operating system, specifically, debug zones, Remote Performance Monitor, and LMEMDEBUG.

The Windows Embedded Developers Conference is running in Las Vegas October 21–24. Hope to see you there!

 

Get Embedded

Mike Hall is a Product Manager in the Microsoft Embedded and Appliance Platform Group (EAPG). Mike has been working with Windows CE since 1996—in developer support, Embedded System Engineering, and the Embedded product group. When not at the office, Mike can be found with his family, working on Skunk projects, or riding a Honda ST1100.

Steve Maillet is the Founder and Senior Consultant for Entelechy Consulting. Steve has provided training and has developed Windows CE solutions for clients since 1997, when CE was first introduced. Steve is a frequent contributor to the Microsoft Windows CE development newsgroups. When he's not at his computer burning up the keys, Steve can be found jumping out of airplanes at the nearest drop zone.

Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.