Multiple Threads in Visual Basic 5.0, Part II: Writing a Win32 Debugger

Jack Robbins

John Robbins is a software engineer at NuMega Technologies, Inc., who specializes in debuggers. He can be reached at john@jprobbins.com.

This article assumes you're familiar with Win32, Visual Basic

I realize that most developers don’t write debug­gers for a living, so I’ll begin by presenting an overview of debuggers in Win32®. My overview will concentrate on the basics in VBDebug, but
if you’re really curious, the DEB sample and the article, “The Win32 Debugging Application Programming Interface,” on the MSDN CD go into more detail on Win32 debuggers. If you want to go hog wild and write a real debugger, I suggest starting with MSDN and the CPU architecture books. Nothing out there fully describes all the gyrations needed to write a debugger, but there are enough hints in these materials to take you a long way.

There are three concepts to keep in mind about Win32 debuggers. First, a debugger consists of two components: the debugger and the debuggee. Simply put, a debugger is a process that can control another process in a debugging relationship, and a debuggee is a process that is started under a debugger. Some operating systems refer to the debugger as the parent process and the debuggee as the child process. Second, the debugger and the debuggee are completely separate processes. This makes the Win32 ­operating systems much more robust when debugging. If the debuggee has wild memory writes, it will not crash the debugger. The 16-bit Windows® and Macintosh operating systems have the debugger and the debuggee running in the same process context. Third, like debuggers in all operating systems, Win32 debuggers generally sit in
some sort of loop waiting for the operating system to report that the debuggee did something. This is commonly referred to as the debug loop.

From a distance, a Win32 debugger is a pretty simple thing, with only a couple of code requirements. The first is that the debugger must pass a special flag in dwCre­ation­Flags to CreateProcess: DEBUG_ONLY_THIS_PROCESS. This tells the operating system that the calling process is to be treated as a debugger. After the debuggee is started, the debugger must sit in a loop calling the WaitFor­Debug­Event API to receive debugging notifications. When it’s finished processing them, it calls Continue­DebugEvent. The pseudo code below shows just how little is required to create a Win32 debugger.

Sub Main()

CreateProcess(...,DEBUG_ONLY_THIS_PROCESS,...)

Do While (1 = WaitForDebugEvent(...))

If EXIT_PROCESS Then

Exit Do

End If

ContinueDebugEvent(...)

Loop

End Sub

Notice that a Win32 debugger does not require multi­threading, a user interface, or much of anything else. But
as with most things in Windows, the difference between minimal and reasonable is pretty big. In reality, the Win32 Debug API almost dictates that the actual debug loop needs to sit in a separate thread. As the name implies, Wait­For­DebugEvent blocks on an internal operating system event until the debuggee does something to make the operating system stop the debuggee so it can tell the debugger about the event. If your debugger had a single thread, then your user interface would be totally hung until a debug event was triggered.

While a debugger sits in the debug loop, it receives various notifications that certain events took place in the debuggee. Unfortunately, the DEBUG_EVENT used by Wait­ForDebugEvent is a C structure that uses a union and cannot be expressed in straight Visual Basic® terms. In C, a union is a collection of other data types that are lumped together, with the total size of the union being the size of the largest structure. It is a convenient way to pack a great deal of data into a small space. To get around the union issue, I defined the DEBUG_EVENT structure in VBDebug using the largest of the unioned structures, EXCEPTION_DE­BUG_INFO, to pad DEBUG_EVENT to the correct size:

Type DEBUG_EVENT

dwDebugEventCode As Long

dwProcessID As Long

dwThreadID As Long

dwUnionData As EXCEPTION_DEBUG_INFO

End Type

To get the appropriate data out of DEBUG_EVENT, use the Visual Basic LSet statement to copy the data out of the dwUnionData into the appropriate user-defined type.

Dim stEvtDbg As DEBUG_EVENT

Dim stCreateProcess AS CREATE_PROCESS_DEBUG_INFO

WaitForDebugEvent ( stEvtDbg , INFINITE )

If ( CREATE_PROCESS_DEBUG_EVENT = stEvtDbg.dwDebugEventCode ) then

' Here's the magic LSet.

LSet stCreateProcess = stEvtDbg.dwUnionData

End If

The dwDebugEventCode field indicates which type of event has been returned. Figure 1 describes all the different events returned by the Win32 Debug API.

Figure 1 Win32 Debug API Events

Event ID Explanation

EXCEPTION_DEBUG_EVENT An exception of some sort occurred. Convert the DEBUG_EVENT.dwUnionData into an EXCEPTION_DEBUG_INFO to find out the type of exception and where it occurred.

CREATE_THREAD_DEBUG_EVENT The debuggee created a thread. Convert the DEBUG_EVENT.dwUnionData into a CREATE_THREAD_DEBUG_INFO to get the thread handle and start address.

CREATE_PROCESS_DEBUG_EVENT The debuggee was created. Convert the DEBUG_EVENT.dwUnionData into a CREATE_ PROCESS_DEBUG_INFO to get the information about the debuggee. The process handle is something you will need to save off so that you can read OutputDebugStrings.

EXIT_THREAD_DEBUG_EVENT The debuggee had a thread terminate. Convert the DEBUG_EVENT.dwUnionData to an EXIT_THREAD_DEBUG_INFO. This is only for threads created after the main thread. The main thread is terminated with EXIT_PROCESS_DEBUG_EVENT.

EXIT_PROCESS_DEBUG_EVENT The debuggee process exited. Convert the DEBUG_EVENT.dwUnionData to an EXIT_ PROCESS_DEBUG_INFO.

LOAD_DLL_DEBUG_EVENT The debuggee loaded a DLL. Convert the DEBUG_EVENT.dwUnionData to a LOAD_DLL_ DEBUG_INFO. Unfortunately, the debug API does not report the name of the DLL; you must pound through the image to find it. The LOAD_DLL_DEBUG_INFO.lp­ImageName is always empty.

UNLOAD_DLL_DEBUG_EVENT The debuggee unloaded a DLL. Convert the DEBUG_EVENT.dwUnionData to an UNLOAD_DLL_DEBUG_INFO.

OUTPUT_DEBUG_STRING_EVENT The debuggee made a call to OutputDebugString. The debugger will need to read the string out of the debuggee's address space with ReadProcessMemory using the process handle saved from the CREATE_PROCESS_DEBUG_EVENT notification.

RIP_EVENT The debuggee has a RIP-debugging event (system debugging error). Convert DEBUG_ EVENT.dwUnionData to a RIP_INFO.

When the debugger is processing the debug events returned by WaitForDebugEvent, it can do anything that it wants to the debuggee because all the threads in the debuggee are completely stopped. If the debugger needs to read or write to the debuggee’s address space, it can use ReadProcessMemory and WriteProcessMemory. Of course, if the debugger patches the debuggee’s code with a call to WriteProcessMemory, it should call FlushInstructionCache to clear out the instruction cache. If the debugger needs to get or set the debuggee’s current context or CPU registers, it can call GetThreadContext or SetThreadContext.

The only Win32 debug event that needs special handling is the loader breakpoint. Right after the CREATE_PRO­CESS_DEBUG_EVENT is received, the debugger will receive an EXCEPTION_DEBUG_EVENT. This is the loader breakpoint, the result of the first assembler instruction executed by the debuggee. The debuggee executes this break­point because the CREATE_PROCESS_DEBUG_EVENT indicates only that the process was loaded, not executed. The loader breakpoint, which the operating system forces each debuggee to execute, is the first time the debugger actually knows when the debuggee is truly running. In real-world debuggers, all of the main data structures, such as symbol tables, are initialized in process creation, and the debugger starts showing code disassembly or doing necessary debuggee patching in the loader breakpoint.

When the loader break­point is hit, the debugger should record that it saw the breakpoint so that subsequent breakpoints can be handled accordingly. The only other processing needed for the first breakpoint (and for all breakpoints in general) depends on the CPU. On the Intel platform, the debugger has to continue processing by calling ContinueDebugEvent and passing it the DBG_CONTINUE flag so that the debuggee resumes execution. On an Alpha CPU, the de­bugger must get the CONTEXT structure that represents the current CPU state with GetThreadContext, then increment the Fir (Fault Instruction) field by the size of an instruction, which is four bytes. After incrementing the Fir, the debugger must call SetThreadContext to change the debuggee’s registers. This operation manually skips the breakpoint. Unlike the Intel CPU, which automatically increments the EIP (Instruction Pointer) register after executing a breakpoint, the Alpha CPU does not.

When handling the various debug events, there are a number of handles that are passed around, including process handles, thread handles, and file handles. While it’s usually good practice to close any open handles an application is given, the Win32 Debug API automatically closes all process and thread handles that are passed to the debugger. If you close them yourself, you might end up closing a random handle in your application because Windows NT® will recycle handle values. This will lead to bugs that are almost impossible to find. I learned this the hard way— always check the return values of CloseHandle.

VBDebug: The Big Picture

When I first started thinking about VBDebug (see Figure 2), my goal was to design it to use as a prototyping vehicle for some of my debugger ideas. To achieve this, I needed a way to get the interesting parts—the actual Win32 debug event handlers—isolated so that I could experiment with different things without having to rewrite the application. There are three main objects at the highest level in VBDebug: the User Interface (UI), the Executive, and the Core Debug­ger. The UI is the main way the user controls the application, and it runs completely in its own thread. The Executive is the portion that handles the actual debug events (the real meat of a debugger), and it runs in the same thread as the Core Debugger. The Core Debugger is where the actual debug loop resides.

Figure 2 VBDebug

frmVBDebug.frm

VERSION 5.00

Object = "{F9043C88-F6F2-101A-A3C9-08002B2F49FB}#1.1#0"; "COMDLG32.OCX"

Begin VB.Form frmVBDebug

Caption = "VBDebug"

ClientHeight = 4935

ClientLeft = 165

ClientTop = 735

ClientWidth = 5985

Icon = "frmVBDebug.frx":0000

LinkTopic = "Form1"

ScaleHeight = 4935

ScaleWidth = 5985

StartUpPosition = 3 'Windows Default

Begin MSComDlg.CommonDialog dlgFileOpen

Left = 3120

Top = 840

_ExtentX = 847

_ExtentY = 847

_Version = 327680

CancelError = -1 'True

DefaultExt = ".exe"

Filter = "Executables (*.exe) | *.exe"

End

Begin VB.TextBox txtOutput

Height = 2055

Left = 120

Locked = -1 'True

MultiLine = -1 'True

ScrollBars = 3 'Both

TabIndex = 0

Top = 360

Width = 2055

End

Begin VB.Menu mnuFile

Caption = "&File"

Begin VB.Menu mnuFileOpen

Caption = "&Open"

Shortcut = ^O

End

Begin VB.Menu mnuFileExit

Caption = "&Exit"

Shortcut = ^Q

End

End

Begin VB.Menu mnuDebug

Caption = "&Debug"

Begin VB.Menu mnuDebugStart

Caption = "&Start"

Enabled = 0 'False

Shortcut = {F5}

End

Begin VB.Menu mnuDebugPause

Caption = "&Pause"

Enabled = 0 'False

End

Begin VB.Menu mnuDebugEnd

Caption = "&End"

Enabled = 0 'False

End

Begin VB.Menu mnuDebugRestart

Caption = "&Restart"

Enabled = 0 'False

Shortcut = +{F5}

End

Begin VB.Menu mnuSep1

Caption = "-"

End

Begin VB.Menu mnuDebugShowActiveThreads

Caption = "S&how Active Threads"

Enabled = 0 'False

End

Begin VB.Menu mnuDebugShowActiveDLLs

Caption = "Sh&ow Active DLLs"

Enabled = 0 'False

End

End

Begin VB.Menu mnuHelp

Caption = "&Help"

Begin VB.Menu mnuHelpAbout

Caption = "&About VBDebug"

End

End

End

Attribute VB_Name = "frmVBDebug"

Attribute VB_GlobalNameSpace = False

Attribute VB_Creatable = False

Attribute VB_PredeclaredId = True

Attribute VB_Exposed = False

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' John Robbins

' Microsoft Systems Journal - August 1997

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FILE : frmVBDebug.frm

' DISCUSSION :

' The main UI form for the whole VBDebug project.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Enumeration types that indicate the state of the UI widgits.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Enum eUIState

' The UI is uninitialized. The only time in this state is when I

' first start and before I have loaded an executable.

eUIUninitialized = 0

' The user has opened a file but has not started debugging or the

' current debuggee has finished and is ready to run again.

eUILoaded = 1

' There is an application running under the debug loop.

eUIDebugging = 2

' The debuggee is running but it is paused.

eUIDebuggingPaused = 3

End Enum

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Form private variables.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' The full name of the executable that I have open for debugging.

Private g_szFullDebuggeeName As String

' The name portion of the debuggee. This is what I use for setting the

' application title.

Private g_szJustDebuggeeName As String

' The debugger class that is passed to the debug thread.

Private g_clsDebug As DebuggerClass

' The executive class for the debugger.

Private g_clsExecutive As SimpleExecutive

' The synchronization class.

Private g_clsSynch As DebugSynchClass

' The handle to the debug thread.

Private g_hDebugThread As Long

' The handle to the thread that waits for the debug thread to end.

Private g_hWaitThread As Long

' The structure that I pass to the wait thread.

Private g_stWaitType As SPECIALWAIT_TYPE

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Form Event Handling

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub Form_Load()

' Force the output text box to cover the client area.

Form_Resize

End Sub

Private Sub Form_Resize()

' Resize the output text box to fill the entire client area.

txtOutput.Top = 0

txtOutput.Left = 0

txtOutput.Height = ScaleHeight

txtOutput.Width = ScaleWidth

End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)

' Tell the debugger thread to die if it is active.

If (Not (g_clsDebug Is Nothing)) Then

g_clsSynch.QuitDebugThread

' Hang out until the wait thread is done to ensure complete and

' proper cleanup.

Dim bRes As Long

bRes = WaitForSingleObject(g_hWaitThread, INFINITE)

End If

' Clear up any outstanding references.

Set g_clsDebug = Nothing

Set g_clsExecutive = Nothing

Set g_clsSynch = Nothing

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' File Menu Handling

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub mnuFileOpen_Click()

' Bring up the file open dialog and get the user's choice.

On Error GoTo mnuFileOpen_Click_Error

dlgFileOpen.ShowOpen

' Get the full name of the executable.

g_szFullDebuggeeName = dlgFileOpen.filename

' Get just the partial name.

g_szJustDebuggeeName = dlgFileOpen.FileTitle

' Set the UI state to not running.

SetUIState (eUILoaded)

' Put the text in the output so the user can see what is going on.

' I am allowed to touch the output edit control here because no

' debugger thread is running so there are no synchronization

' problems

txtOutput.Text = "UI: " + g_szFullDebuggeeName + _

" opened and ready to run." + vbNewLine

Exit Sub

mnuFileOpen_Click_Error:

' If the error was anything other than cancel, throw it on.

If (cdlCancel <> Err.Number) Then

Err.Raise (Err.Number)

End If

End Sub

Private Sub mnuFileExit_Click()

' If there is an active executive class, then I must use the

' AppendText method to access the output edit control.

If (Not g_clsExecutive Is Nothing) Then

g_clsExecutive.AppendText "UI: File Exit selected"

Else

txtOutput.Text = txtOutput.Text + vbNewLine + _

"UI: File Exit selected" + vbNewLine

End If

' Call this to get the Form_QueryUnload function called.

Unload Me

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Debug Menu Handling

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub mnuDebugStart_Click()

On Error GoTo mnuDebugStart_Click_Error

Dim bRet As Long

Dim boolDidStart As Boolean

' To keep everything straight, I do a complete new debugger,

' synchronization, and executive classes on each start.

' Clear out any existing debugger, executive, and synch classes.

Set g_clsDebug = Nothing

Set g_clsExecutive = Nothing

Set g_clsSynch = Nothing

' Clear the text box and indicate that the UI menu was picked.

txtOutput.Text = ""

txtOutput.Text = "UI: Debug Start selected" + vbNewLine

' Instantiate the debugger class.

Set g_clsDebug = New DebuggerClass

' Instantiate the executive.

Set g_clsExecutive = New SimpleExecutive

' Instantiate the synchronization class.

Set g_clsSynch = New DebugSynchClass

' Initialize the executive class text output. After this, the UI

' thread is no longer allowed to touch the text box.

Let g_clsExecutive.txtOutput = txtOutput

' Initialize the debugger class with the program name.

g_clsDebug.SetDebuggeeInfo (g_szFullDebuggeeName)

' Initialize the debugger class with the executive that does all

' the work.

Set g_clsDebug.clsBaseExecutive = g_clsExecutive

' Now that I have all of the required classes set up, I can start

' the debug thread.

SetUIState (eUIDebugging)

' I have to do the synch in two stages: create it before, then

' check it after. If you don't do this, then you can run into cases

' where the debug thread cranks, sets the event and dies before this

' thread can get it created. Granted those cases only happen on

' those super fast Alphas (400Mhz is REALLY great!) but they do

' happen.

g_clsSynch.PrepareWaitForStartup

' Crank the debug thread passing it debugger class.

g_hDebugThread = StartDebugThread(g_clsDebug)

' Wait until the debug thread at least gets through the

' CreateProcess on the debuggee and see how it did.

boolDidStart = g_clsSynch.WaitForStartup

' If WaitForStartup returned False, then the debuggee was not

' started.

If (False = boolDidStart) Then

' There was a problem starting up so clean up.

Set g_clsDebug = Nothing

Set g_clsExecutive = Nothing

Set g_clsSynch = Nothing

' Let the user know.

MsgBox ("Unable to start " + g_szFullDebuggeeName)

txtOutput.Text = "UI: Unable to start " + g_szFullDebuggeeName

' Make sure to set the UI state back.

SetUIState (eUILoaded)

Exit Sub

End If

' Get the debuggee process ID into the synch class.

g_clsSynch.dwUniqueID = g_clsDebug.dwDebuggeePID

' Create the synchronization objects for THIS thread.

g_clsSynch.CreateSynchObjects

' Create the thread that waits on the debug thread to end.

Set g_stWaitType.frmDTE = Me

g_stWaitType.hThread = g_hDebugThread

' Crank up the wait thread that will watch for the debug thread to

' end.

g_hWaitThread = StartWaitThread(g_stWaitType)

g_clsExecutive.AppendText "UI: Debuggee started"

Exit Sub

mnuDebugStart_Click_Error:

MsgBox ("Error in mnuDebugStart_Click: " + Err.Description)

End Sub

Private Sub mnuDebugEnd_Click()

g_clsExecutive.AppendText "UI: Debug End selected"

' Tell the debugger thread to die.

g_clsSynch.QuitDebugThread

' NOTE: I don't set the UI state here. I need to make sure the

' debug thread is really done before I set it. It is set in the

' btnPostMsgButton_MouseDown handler.

End Sub

Private Sub mnuDebugPause_Click()

g_clsExecutive.AppendText "UI: Debug Pause selected"

' Tell the debugger thread to pause.

g_clsSynch.PauseDebugThread

SetUIState (eUIDebuggingPaused)

End Sub

Private Sub mnuDebugRestart_Click()

g_clsExecutive.AppendText "UI: Debug Restart selected"

' Tell the debugger thread to resume.

g_clsSynch.ResumeDebugThread

SetUIState (eUIDebugging)

End Sub

Private Sub mnuDebugShowActiveThreads_Click()

g_clsExecutive.DumpActiveThreads

End Sub

Private Sub mnuDebugShowActiveDlls_Click()

g_clsExecutive.DumpLoadedDLLs

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Help Menu Handling

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub mnuHelpAbout_Click()

If (Not g_clsExecutive Is Nothing) Then

g_clsExecutive.AppendText "UI: Help About selected"

Else

txtOutput.Text = txtOutput.Text + vbNewLine + _

"UI: Help About selected" + vbNewLine

End If

frmAbout.Show vbModal, Me

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' The special function that is called from the thread that waits for the

' debug thread to end. This is how I get the UI resynched to know what

' the current state is.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugThreadEnded()

SetUIState (eUILoaded)

Dim bRet As Long

' This is very ugly. This function is called in another thread's

' context and it seems that while the caption is set correctly, it

' does not get updated correctly. To force the update, I simply make

' a direct call to SetWindowText to get everything updated.

bRet = SetWindowText(Me.hWnd, Me.Caption)

g_clsExecutive.AppendText "UI: DebugThreadEnded called!"

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Form specific helper functions

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : SetUIState

' DISCUSSION :

' A helper function to set the state of all UI widgits.

' PARAMETERS :

' eUIToSet - The enum to set the user interface to.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub SetUIState(eUIToSet As eUIState)

Select Case (eUIToSet)

' I have an executable open, but I am not debugging yet.

Case eUILoaded

' If the user wants to open another file, they can.

mnuFileOpen.Enabled = True

' Debugging can start.

mnuDebugStart.Enabled = True

' Since I am not debugging, I cannot end, pause or restart

' debugging.

mnuDebugEnd.Enabled = False

mnuDebugPause.Enabled = False

mnuDebugRestart.Enabled = False

' Set the info commands off.

mnuDebugShowActiveThreads.Enabled = False

mnuDebugShowActiveDLLs.Enabled = False

' Set the title.

Me.Caption = k_APPNAME + " - " + _

g_szJustDebuggeeName + _

k_NOTRUNNINGSTATE

' I am debugging!

Case eUIDebugging

' Nope, cannot open files.

mnuFileOpen.Enabled = False

' Cannot start debugging again.

mnuDebugStart.Enabled = False

' I am debugging so I can end and pause but not restart.

mnuDebugEnd.Enabled = True

mnuDebugPause.Enabled = True

mnuDebugRestart.Enabled = False

' Turn the info commands on.

mnuDebugShowActiveThreads.Enabled = True

mnuDebugShowActiveDLLs.Enabled = True

' Set the title.

Me.Caption = k_APPNAME + " - " + _

g_szJustDebuggeeName + _

k_DEBUGGINGSTATE

' The debuggee is paused.

Case eUIDebuggingPaused

' Since this state is only allowed from eUIDebugging, all

' that is done here is to set restart to true and pause

' to false.

mnuDebugPause.Enabled = False

mnuDebugRestart.Enabled = True

Me.Caption = k_APPNAME + " - " + _

g_szJustDebuggeeName + _

k_PAUSEDSTATE

' The uninitialized state.

Case eUIUninitialized

mnuFileOpen.Enabled = True

mnuDebugStart.Enabled = False

mnuDebugEnd.Enabled = False

mnuDebugPause.Enabled = False

mnuDebugRestart.Enabled = False

mnuDebugShowActiveThreads.Enabled = False

mnuDebugShowActiveDLLs.Enabled = False

Me.Caption = k_APPNAME

End Select

End Sub

SimpleExecutive.cls

VERSION 1.0 CLASS

BEGIN

MultiUse = -1 'True

END

Attribute VB_Name = "SimpleExecutive"

Attribute VB_GlobalNameSpace = False

Attribute VB_Creatable = True

Attribute VB_PredeclaredId = False

Attribute VB_Exposed = False

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' John Robbins

' Microsoft Systems Journal - August 1997

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FILE : SimpleExecutive.cls

' DESCRIPTION :

' Implements a simple debugger executive that conforms to the

' BaseExecutive abstract base class. If you want to extend VBDebug to

' handle more advanced things, derive your own class from

' BaseExecutive.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Polymorphic Bliss

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Implements BaseExecutive

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

'Class Specific Private Variables

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' The handle to the main thread.

Private g_hMainThread As Long

' The handle to the process.

Private g_hProcess As Long

' Has the initial breakpoint already been seen?

Private g_bSeenFirstBP As Boolean

' The synchronization object that keeps us from having trouble in this

' class.

Private g_clsCritSec As CriticalSection

' The text box where all output is placed.

Private g_txtOutput As TextBox

' The internal list of threads.

Private g_colThreads As Collection

' The internal list of DLLs.

Private g_colDlls As Collection

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

'Property Setting Functions

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Friend Property Let txtOutput(txtBox As TextBox)

Set g_txtOutput = txtBox

End Property

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Class_Initialize

' DISCUSSION :

' The initialization for the class.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub Class_Initialize()

Set g_clsCritSec = New CriticalSection

Set g_colThreads = New Collection

Set g_colDlls = New Collection

g_bSeenFirstBP = False

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Class_Terminate

' DISCUSSION :

' The termination for the class.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub Class_Terminate()

Set g_clsCritSec = Nothing

Set g_colThreads = Nothing

Set g_colDlls = Nothing

End Sub

Private Sub BaseExecutive_DebugCreateProcess _

(clsDebugger As DebuggerClass, _

dwProcessID As Long, _

dwThreadID As Long)

g_clsCritSec.Enter

On Error GoTo DebugCreateProcess_Error

g_hMainThread = clsDebugger.CreateProcessDbgEvt.hThread

g_hProcess = clsDebugger.CreateProcessDbgEvt.hProcess

#If DEBUGBUILD Then

OutputDebugString ("Created Process ID: " + _

Hex$(dwProcessID) + _

" Thread Handle : " + _

Hex$(g_hProcess) + _

" With Thread " + _

Hex$(g_hMainThread) + _

vbNewLine)

#End If

AppendText "Process &H" + Hex$(g_hProcess) + " created"

AppendText "Thread &H" + Hex$(g_hMainThread) + " created"

' Add the main thread to the collection.

g_colThreads.Add CStr(g_hMainThread), CStr(dwThreadID)

#If DEBUGBUILD Then

OutputDebugString ("Adding thread &H" + _

Hex$(g_hMainThread) + _

" to thread collection" + _

vbNewLine)

OutputDebugString ("Total of " + _

CStr(g_colThreads.Count) + _

" in the collection" + vbNewLine)

#End If

g_clsCritSec.Leave

Exit Sub

DebugCreateProcess_Error:

#If DEBUGBUILD Then

MsgBox ("DebugCreateProcess Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_DebugCreateThread _

(clsDebugger As DebuggerClass, _

dwProcessID As Long, _

dwThreadID As Long)

g_clsCritSec.Enter

On Error GoTo DebugCreateThread_Error

Dim stCTDI As CREATE_THREAD_DEBUG_INFO

stCTDI = clsDebugger.CreateThreadDbgEvt

g_colThreads.Add CStr(stCTDI.hThread), CStr(dwThreadID)

AppendText "Thread &H" + Hex$(stCTDI.hThread) + " created"

#If DEBUGBUILD Then

OutputDebugString ("Adding thread &H" + _

Hex$(stCTDI.hThread) + _

" to thread collection" + vbNewLine)

OutputDebugString ("Total of " + _

CStr(g_colThreads.Count) + _

" in the collection" + vbNewLine)

#End If

g_clsCritSec.Leave

Exit Sub

DebugCreateThread_Error:

#If DEBUGBUILD Then

MsgBox ("DebugCreateThread Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_DebugDllLoad(clsDebugger As DebuggerClass, _

dwProcessID As Long, _

dwThreadID As Long)

g_clsCritSec.Enter

On Error GoTo DebugDllLoad_Error

Dim stLDDI As LOAD_DLL_DEBUG_INFO

stLDDI = clsDebugger.LoadDllDbgEvt

g_colDlls.Add Hex$(stLDDI.lpBaseOfDll), Hex$(stLDDI.lpBaseOfDll)

AppendText "DLL Loaded at &H" + Hex$(stLDDI.lpBaseOfDll)

#If DEBUGBUILD Then

OutputDebugString ("Adding DLL &H" + _

Hex$(stLDDI.lpBaseOfDll) + _

" to DLL collection" + _

vbNewLine)

OutputDebugString ("Total of " + _

CStr(g_colDlls.Count) + _

" in the collection" + vbNewLine)

#End If

g_clsCritSec.Leave

Exit Sub

DebugDllLoad_Error:

#If DEBUGBUILD Then

MsgBox ("DebugDllLoad Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_DebugDllUnload(clsDebugger As DebuggerClass, _

dwProcessID As Long, _

dwThreadID As Long)

g_clsCritSec.Enter

On Error GoTo DebugDllUnload_Error

Dim stULDDI As UNLOAD_DLL_DEBUG_INFO

stULDDI = clsDebugger.UnloadDllDbgEvt

g_colDlls.Remove Hex$(stULDDI.lpBaseOfDll)

AppendText "DLL Unloaded at &H" + Hex$(stULDDI.lpBaseOfDll)

#If DEBUGBUILD Then

OutputDebugString ("Removing DLL &H" + _

Hex$(stULDDI.lpBaseOfDll) + _

" from the DLL collection" + _

vbNewLine)

OutputDebugString ("Total of " + _

CStr(g_colDlls.Count) + _

" in the collection" + vbNewLine)

#End If

g_clsCritSec.Leave

Exit Sub

DebugDllUnload_Error:

#If DEBUGBUILD Then

MsgBox ("DebugDllUnload Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_DebugException(clsDebugger As DebuggerClass, _

dwContType As Long, _

dwProcessID As Long, _

dwThreadID As Long)

g_clsCritSec.Enter

On Error GoTo DebugException_Error

Dim stER As EXCEPTION_RECORD

stER = clsDebugger.ExceptionDbgEvt.ExceptionRecord

' If this is a breakpoint exception and if the loader breakpoint has

' not been seen yet, then it is OK.

If ((False = g_bSeenFirstBP) And _

(EXCEPTION_BREAKPOINT = stER.ExceptionCode)) Then

g_bSeenFirstBP = True

#If Alpha Then

SkipBreakPoint g_hMainThread

#End If

dwContType = DBG_CONTINUE

Else

' The program has a problem. Here is where stack dumps and other

' helpful things could occur.

' If this is a first chance exception, pass it on to the debuggee

' to let them handle it first. The second time that it is seen,

' show the warning.

If (0 = clsDebugger.ExceptionDbgEvt.dwFirstChance) Then

AppendText GetExceptionString(stER.ExceptionCode) + _

" occurred at &H" + _

Hex$(stER.ExceptionAddress)

End If

dwContType = DBG_EXCEPTION_NOT_HANDLED

End If

g_clsCritSec.Leave

Exit Sub

DebugException_Error:

#If DEBUGBUILD Then

MsgBox ("DebugException Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_DebugExitProcess _

(clsDebugger As DebuggerClass, _

dwProcessID As Long, _

dwThreadID As Long)

g_clsCritSec.Enter

On Error GoTo DebugExitProcess_Error

#If DEBUGBUILD Then

Dim hThread As Long

hThread = g_colThreads.Item(CStr(dwThreadID))

#End If

g_colThreads.Remove CStr(dwThreadID)

AppendText "Process &H" + _

Hex$(g_hProcess) + _

" ended and returned &H" + _

Hex$(clsDebugger.ExitProcessDbgEvt.dwExitCode)

#If DEBUGBUILD Then

OutputDebugString ("Removing thread &H" + _

Hex$(hThread) + _

" from the thread collection" + _

vbNewLine)

OutputDebugString ("Total of " + _

CStr(g_colThreads.Count) + _

" in the collection" + vbNewLine)

#End If

g_clsCritSec.Leave

Exit Sub

DebugExitProcess_Error:

#If DEBUGBUILD Then

MsgBox ("DebugExitProcess Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_DebugExitThread _

(clsDebugger As DebuggerClass, _

dwProcessID As Long, _

dwThreadID As Long)

g_clsCritSec.Enter

On Error GoTo DebugExitThread_Error

Dim hThread As Long

hThread = g_colThreads.Item(CStr(dwThreadID))

g_colThreads.Remove CStr(dwThreadID)

AppendText "Thread &H" + _

Hex$(hThread) + _

" ended and returned &H" + _

Hex$(clsDebugger.ExitThreadDbgEvt.dwExitCode)

#If DEBUGBUILD Then

OutputDebugString ("Removing thread &H" + _

Hex$(hThread) + _

" from the thread collection" + _

vbNewLine)

OutputDebugString ("Total of " + _

CStr(g_colThreads.Count) + _

" in the collection" + vbNewLine)

#End If

g_clsCritSec.Leave

Exit Sub

DebugExitThread_Error:

#If DEBUGBUILD Then

MsgBox ("DebugExitThread Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_DebugODS(clsDebugger As DebuggerClass, _

dwProcessID As Long, _

dwThreadID As Long)

g_clsCritSec.Enter

On Error GoTo DebugODS_Error

Dim stODS As OUTPUT_DEBUG_STRING_INFO

Dim szOutBuff As String * 1024

Dim bRet As Long

Dim dwBytes As Long

stODS = clsDebugger.ODSDbgEvt

bRet = ReadProcessMemory(g_hProcess, _

stODS.lpDebugStringData, _

szOutBuff, _

stODS.nDebugStringLength, _

dwBytes)

AppendText "ODS: " + Left$(szOutBuff, stODS.nDebugStringLength)

g_clsCritSec.Leave

Exit Sub

DebugODS_Error:

#If DEBUGBUILD Then

MsgBox ("DebugODS Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_DebugRipInfo(clsDebugger As DebuggerClass, _

dwProcessID As Long, _

dwThreadID As Long)

g_clsCritSec.Enter

On Error GoTo DebugRipInfo_Error

g_clsCritSec.Leave

Exit Sub

DebugRipInfo_Error:

#If DEBUGBUILD Then

MsgBox ("DebugRipInfo Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_PauseProcess()

g_clsCritSec.Enter

On Error GoTo PauseProcess_Error

Dim lData As Variant

Dim bRet As Long

For Each lData In g_colThreads

bRet = SuspendThread(CLng(lData))

Next lData

g_clsCritSec.Leave

Exit Sub

PauseProcess_Error:

#If DEBUGBUILD Then

MsgBox ("PauseProcess Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

Private Sub BaseExecutive_ResumeProcess()

g_clsCritSec.Enter

On Error GoTo ResumeProcess_Error

Dim lData As Variant

Dim bRet As Long

For Each lData In g_colThreads

bRet = ResumeThread(CLng(lData))

Next lData

g_clsCritSec.Leave

Exit Sub

ResumeProcess_Error:

#If DEBUGBUILD Then

MsgBox ("ResumeProcess Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : AppendText

' DISCUSSION :

' This is a public interface off the SimpleExecutive class. Both the

' UI and this class will only call through it to do their output.

' PARAMETERS :

' szStr - The string to append.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub AppendText(szStr As String)

' Hey! Shouldn't there be a critical section in here? Technically,

' there should be, but since the edit control we are protecting is

' protected internally, it doesn't really need it.

On Error GoTo AppendText_Error

Dim lLen As Long

lLen = Len(szStr)

If (0 = lLen) Then

Exit Sub

End If

Dim szTemp As String

szTemp = g_txtOutput.Text

If (Chr$(13) <> Right$(szStr, lLen - 2)) Or _

(Chr$(10) <> Right$(szStr, lLen - 1)) Then

g_txtOutput.Text = szTemp + szStr + vbNewLine

Else

g_txtOutput.Text = szTemp + szStr

End If

' Now scroll the text into view.

g_txtOutput.SelStart = Len(g_txtOutput.Text)

Exit Sub

AppendText_Error:

#If DEBUGBUILD Then

MsgBox ("AppendText Error: " + Err.Description)

#End If

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DumpActiveThreads

' DISCUSSION :

' To demonstrate some of the cross-thread coordination, this function

' dumps the active threads for the debuggee. This is to give you an

' idea how to access information from the UI.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DumpActiveThreads()

g_clsCritSec.Enter

On Error GoTo PauseProcess_Error

Dim lData As Variant

Dim i As Long

For Each lData In g_colThreads

AppendText "Info: Active thread #" + CStr(i) + " Handle : &H" + _

Hex$(CLng(lData))

i = i + 1

Next lData

g_clsCritSec.Leave

Exit Sub

PauseProcess_Error:

#If DEBUGBUILD Then

MsgBox ("DumpActiveThreads Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DumpLoadedDLLs

' DISCUSSION :

' Like DumpActiveThreads, dumps the loaded DLLs that are currently in

' the debuggee's address space.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DumpLoadedDLLs()

g_clsCritSec.Enter

On Error GoTo PauseProcess_Error

Dim szData As Variant

Dim i As Long

For Each szData In g_colDlls

AppendText "Info: DLL #" + CStr(i) + " Loaded at &H" + _

szData

i = i + 1

Next szData

g_clsCritSec.Leave

Exit Sub

PauseProcess_Error:

#If DEBUGBUILD Then

MsgBox ("DumpActiveThreads Error: " + Err.Description)

#End If

g_clsCritSec.Leave

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : SkipBreakPoint

' DISCUSSION :

' The function that skips over Alpha breakpoints. Where Intel

' breakpoints automatically increment EIP when hit, on the Alpha, Fir

' is not.

' PARAMETERS :

' hThread - The thread where the skip is to take place.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

#If Alpha Then

Private Sub SkipBreakPoint(hThread As Long)

Dim ctx As CONTEXT

Dim bRet As Long

ctx.ContextFlags = CONTEXT_CONTROL

bRet = GetThreadContext(hThread, ctx)

If (1 <> bRet) Then

MsgBox ("GetThreadContext failed!")

End If

' Alpha instructions are all four bytes long.

ctx.Fir.lowpart = ctx.Fir.lowpart + 4

bRet = SetThreadContext(hThread, ctx)

If (1 <> bRet) Then

MsgBox ("SetThreadContext failed!")

End If

End Sub

#End If

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : GetExceptionString

' DISCUSSION :

' Given an exception code, returns the human readable string that

' describes it.

' PARAMETERS :

' dwCode - The exception code.

' RETURN :

' The string. If the exception is unknown then "Unknown exception"

' is returned.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Function GetExceptionString(dwCode As Long) As String

Select Case (dwCode)

Case EXCEPTION_ACCESS_VIOLATION

GetExceptionString = "Access Violation Exception"

Exit Function

Case EXCEPTION_DATATYPE_MISALIGNMENT

GetExceptionString = "Datatype Misalignment Exception"

Exit Function

Case EXCEPTION_BREAKPOINT

GetExceptionString = "Breakpoint Exception"

Exit Function

Case EXCEPTION_SINGLE_STEP

GetExceptionString = "Single Step Exception"

Exit Function

Case EXCEPTION_ARRAY_BOUNDS_EXCEEDED

GetExceptionString = "Array Bounds Exceeded Exception"

Exit Function

Case EXCEPTION_FLT_DENORMAL_OPERAND

GetExceptionString = "Floating Point Denormal Operand Exception"

Exit Function

Case EXCEPTION_FLT_DIVIDE_BY_ZERO

GetExceptionString = "Floating Point Divide By Zero Exception"

Exit Function

Case EXCEPTION_FLT_INEXACT_RESULT

GetExceptionString = "Floating Point Inexact Result Exception"

Exit Function

Case EXCEPTION_FLT_INVALID_OPERATION

GetExceptionString = "Floating Point Invalid Operation Exception"

Exit Function

Case EXCEPTION_FLT_OVERFLOW

GetExceptionString = "Floating Point Overflow Exception"

Exit Function

Case EXCEPTION_INT_DIVIDE_BY_ZERO

GetExceptionString = "Integer Divide By Zero Exception"

Exit Function

Case EXCEPTION_INT_OVERFLOW

GetExceptionString = "Integer Overflow Exception"

Exit Function

Case EXCEPTION_PRIV_INSTRUCTION

GetExceptionString = "Privileged Instruction Exception"

Exit Function

Case EXCEPTION_IN_PAGE_ERROR

GetExceptionString = "In Page Error Exception"

Exit Function

Case EXCEPTION_ILLEGAL_INSTRUCTION

GetExceptionString = "Illegal Instruction Exception"

Exit Function

Case EXCEPTION_NONCONTINUABLE_EXCEPTION

GetExceptionString = "Noncontinuable Exception"

Exit Function

Case EXCEPTION_STACK_OVERFLOW

GetExceptionString = "Stack Overflow Exception"

Exit Function

Case EXCEPTION_INVALID_DISPOSITION

GetExceptionString = "Invalid Disposition Exception"

Exit Function

Case EXCEPTION_GUARD_PAGE

GetExceptionString = "Guard Page Exception"

Exit Function

Case Else

GetExceptionString = "Unknown Exception (&H" + _

Hex$(dwCode) + ")"

Exit Function

End Select

End Function

VBD_Constants.bas

Attribute VB_Name = "VBD_Constants"

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' John Robbins

' Microsoft Systems Journal - August 1997

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Helpful constants that really should be in a .RES file.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' The name of the application.

Global Const k_APPNAME As String = "VBDebug"

' The names for the different states that we show in the main window

' title.

Global Const k_NOTRUNNINGSTATE As String = " [Loaded]"

Global Const k_PAUSEDSTATE As String = " [Paused]"

Global Const k_DEBUGGINGSTATE As String = " [Running]"

EndDbgThread.bas

Attribute VB_Name = "EndDbgThread"

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' John Robbins

' Microsoft Systems Journal - August 1997

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FILE : EndDbgThread.bas

' DESCRIPTION :

' The thread that tells the UI that the debug thread has ended.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

' The type that is passed to the wait thread.

Public Type SPECIALWAIT_TYPE

' The thread to wait on.

hThread As Long

' The form to call the DebugThreadEnded method on. As long as your

' form has the DebugThreadEnded method, the type is the only thing

' that should be changed in this file.

frmDTE As frmVBDebug

End Type

' The function that starts the debug thread.

Public Function StartWaitThread(stWaitType As SPECIALWAIT_TYPE) As Long

Dim hThread As Long

Dim lThreadID As Long

' Create the thread.

hThread = CreateThread(0, _

0, _

AddressOf WaitForEndOfDebugThread, _

stWaitType, _

0, _

lThreadID)

StartWaitThread = hThread

End Function

Public Function WaitForEndOfDebugThread _

(stWaitType As SPECIALWAIT_TYPE) As Long

On Error GoTo WaitForEndOfDebugThread_Error

Dim bRet As Long

' Wait for the debug thread to become signaled, which means it is

' done.

bRet = WaitForSingleObject(stWaitType.hThread, INFINITE)

' Instead of calling directly into the form, I would have preferred to

' do a PostMessage here, but when this program is run on a

' multiprocessor machine, the message never makes it through the

' runtime. Therefore, we have to call into the form.

stWaitType.frmDTE.DebugThreadEnded

WaitForEndOfDebugThread = 1

Exit Function

WaitForEndOfDebugThread_Error:

MsgBox ("Got an error in DebugThread: " + Err.Description)

WaitForEndOfDebugThread = 0

End Function

These three objects are pretty much equally important and they do have to interact with one another. The UI object is responsible for creating the Executive and Core Debugger objects, getting them running in the background thread, and stopping the background thread to release the objects. Since the Executive object is responsible for handling the debugging events, it needs to get the information it controls up to the UI object so the user knows what’s going on. This means that the UI and Executive objects need to coordinate some sort of interface that the Executive can use to display the information the user requested.

The Core Debugger object is simple and just encapsulates the debugger loop. It has a reference to the Executive object, so it calls into the Executive object for processing when it receives a debug event. If you set everything correctly, the Core Debugger object should only have to be written once.

From a multithreading standpoint, the only object that will be accessed from both the UI thread and the debug thread is the Executive object. This means the Executive must use one of the Win32 data protection synchronization types, like a critical section, on each of its interfaces.

Once I decided on my design goals for VBDebug, I had to wrestle with some implementation issues. Since I needed a way to plug in various Executives and wanted as much flexibility as possible, I implemented the Executive and Core Debugger portions as Visual Basic classes. By using classes and taking advantage of the late binding offered by COM, the Executive can be easily replaced.

The source files frmVBDebug.frm, SimpleExecutive.cls, and DebuggerClass.cls implement the UI, Executive, and Core Debugger objects, respectively. As you look through the code for SimpleExecutive.cls, you might notice that the application implements the interface from a class called BaseExecutive. This is the abstract class that DebuggerClass uses to access the Executive. This means that DebuggerClass only needs to be implemented once, and it can have any number of different Executive objects passed to it.

As you peruse the source code, pay careful attention to the interactions between the three main objects. Probably the best way to see everything in action is to start with the user interface, frmVBDebug.frm, and carefully track how the other classes are created and accessed. There is a good deal of code in VBDebug, so I will only cover some of the more interesting highlights in this article. I will start with the debugger portions and move into the multithreaded sections. As with all new code, you might want to load VBDe­bug in a debugger, set breakpoints everywhere, and start it. When a breakpoint is hit, note what thread it came from.

Another interesting feature of the VBDebug code is just how much of it can be reused by another debugger UI and Executive implementation. The source code tree is set up so that all the specific VBDebug code resides in the VBDebug directory, and all the generic code that can be used for other debuggers using my architecture is in the VBDebug\Reuse directory. I hope this will help you when you sit down and play with your own debugger implementations.

More On Win32 Debuggers

VBDebug does not pretend to be a full-fledged debugger by any means, but it does enough work to be useful. And it provides a good starting point for developing more advanced support. All of the good debugging support in VBDebug can be found in the SimpleExecutive class. As you look through the class, you will notice that the functions that are derived from BaseExecutive are the usual debug event handlers. For the most part, these are pretty straightforward.

The only thing the event handlers do is keep two lists: one for dealing with threads and one for dealing with DLLs. The methods that deal with thread creation, BaseExecutive_
DebugCreateProcess and BaseExecutive_Debug­Create­Thread, put the thread handle into a collection class, g_colThreads. The methods that deal with thread destruction, BaseExecutive_DebugExitProcess and BaseExecutive_
DebugExitThread, remove the thread handle from g_col­Threads. While VBDebug only uses these thread handles to show which ones started and stopped, a full-fledged debugger needs to have these handles so that it can set breakpoints in a thread, query the thread’s registers, walk the thread’s stack, and let the user see the priority of threads.

The methods that deal with DLL loading and unloading, BaseExecutive_DebugDllLoad and BaseExecutive_Debug­DllUnload, place the load address into a g_colDlls collection, much like the thread handler methods. Likewise, the DLL handler methods don’t do a whole lot. They simply report that a DLL loaded or unloaded at a particular address. If you looked at the information returned in a LOAD_DLL_DEBUG_INFO structure, you would notice a field called lpImageName, which the documentation says is the name of the file. In reality, the name of the DLL is never filled in. This means that you must wind through the actual PE image in memory to get the export section and then find the name. I’ll leave this as an exercise for the reader.

A slightly more interesting method is BaseExecutive_
DebugODS, where the debuggee calls to OutputDebugString are handled. When a debuggee calls OutputDebugString, the operating system notifies the debugger and it actually reads the string from the debuggee’s address space. While this may sound difficult to initiate, there is a simple API call that handles it for you pretty easily—ReadProcessMemory.

Another interesting method in the SimpleExecutive class is BaseExecutive_DebugException, which offers plenty of room for more advanced development. The special handling in this method is needed only if the first breakpoint has not been seen and the exception is a breakpoint, as mentioned. If those conditions are true and VBDebug is compiled for an Alpha CPU, the SkipBreakPoint private method is called. Looking at SkipBreakPoint, you will see the GetThread­Context and SetThreadContext APIs are used to skip over the fault instruction. After skipping the breakpoint on the Alpha, which is done automatically by Intel CPUs, Base­Executive_DebugException tells the DebuggerClass to pass DBG_CONTINUE onto ContinueDebugEvent. If the exception passed to BaseExecutive_DebugException is not the first breakpoint or is something other than a breakpoint, it passes the exception to the debuggee for it to handle the exception. If the exception is a second-chance exception, then I output the formal name of the exception and where it occurred. Here is where some of the advanced debugger operations need to be added, such as stack walking and variable dumping.

Since there is more to debuggers than just responding to the basic events from the Debug API, there are a couple of methods that do things outside the event architecture. In VBDebug, these are BaseExecutive_PauseProcess and Base­Executive_ResumeProcess. As the names suggest, they are used to pause and restart the debuggee. What makes pausing and restarting the debuggee interesting is that the debugger must actually call the SuspendThread and ResumeThread APIs on all the debuggee’s threads individually. There isn’t a single API call to instantly suspend or resume a debuggee. While this might seem odd, the idea makes sense. When I first looked at the Win32 Debug API, I thought that the entire debuggee was stopped when I did not call WaitForDebugEvent. But when you think about the Debug API, it makes sense that it is not.

The debuggee is running full tilt until it does something that the operating system notices will cause an event to trigger, at which point the debuggee is stopped dead in its tracks. This means that, even though I could tell the debug thread to block and not call WaitForDebugEvent (because I wanted to pause the debuggee), it is still running until the debuggee does something that causes a debug event. The way to properly pause the debuggee is to call SuspendThread on each of the active threads in the debuggee, then block on the debug thread. Keep in mind that suspending a thread is not the same as halting a thread. To halt a thread, you would need to suspend it, write a breakpoint instruction to the current program counter, and then resume the thread to cause the breakpoint to be hit.

Senior Project Time!

Since the title of this article concerns multithreading, it’s high time that I talk about the hardcore multithreading in VBDebug. While some interesting pieces of the actual debugger were left as exercises for the reader, the multi­threaded portions are complete and good enough for you to consider for your applications. When you read over this section, you should look at the code very carefully, because it sometimes gets difficult to imagine exactly what happens in a multithreaded system. There are three major areas that need to be covered here: getting the debug thread started, protecting items that can be accessed from multiple threads, and handling the coordination between the UI thread and the debugger thread.

Of the items that I will discuss in this section, getting the debugger thread cranked up is the easiest to explain. After opening an executable and selecting Start from the Debug menu, the mnuDebugStart_Click method in frmVB­Debug.frm does all the work (see Figure 2). As described earlier, the UI is responsible for creating the Core Debugger and the Executive class, so this is where the DebuggerClass from DebuggerClass.cls (see Figure 3) and the Simple­Exe­cutive class are instantiated. After instantiating the classes, mnuDebugStart_Click tells the DebuggerClass which Executive to use, then calls the StartDebugThread function from DebugThread.bas. There are some other things that go on before the call to StartDebugThread, but I’ll discuss them later as part of the thread synchronization. Start­DebugThread calls CreateThread with the DebugThread function (also in DebugThread.bas) as the thread function, then passes the DebuggerClass as the thread parameter. Since I only want to write the actual DebugThread function once, I have it take the DebugClass passed in and start calling methods on it. The first is StartDebuggee. If the debuggee starts correctly, it lets the DebuggerClass spin in the ProcessDebugEvents method until it returns.

Figure 3 Reusable VBDebug Code

BaseExecutive.cls

VERSION 1.0 CLASS

BEGIN

MultiUse = -1 'True

END

Attribute VB_Name = "BaseExecutive"

Attribute VB_GlobalNameSpace = False

Attribute VB_Creatable = True

Attribute VB_PredeclaredId = False

Attribute VB_Exposed = False

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' John Robbins, Microsoft Systems Journal - August 1997

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FILE : BaseExecutive.cls

' DESCRIPTION :

' The abstract base class for all debugger executives.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DebugCreateProcess

' DISCUSSION :

' Called when the debugger thread gets a process create notification.

' PARAMETERS :

' clsDebugger - The debugger class to query for additional information.

' dwProcessID - The process ID for the process.

' dwThreadID - The thread ID for the thread.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugCreateProcess(clsDebugger As DebuggerClass,

dwProcessID As Long, dwThreadID As Long)

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DebugExitProcess

' DISCUSSION :

' Called when the debugger thread gets a process exit notification.

' PARAMETERS :

' clsDebugger - The debugger class to query for additional information.

' dwProcessID - The process ID for the process.

' dwThreadID - The thread ID for the thread.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugExitProcess(clsDebugger As DebuggerClass, _

dwProcessID As Long, dwThreadID As Long)

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DebugCreateThread

' DISCUSSION :

' Called when the debugger thread gets a thread create notification.

' PARAMETERS :

' clsDebugger - The debugger class to query for additional information.

' dwProcessID - The process ID for the process.

' dwThreadID - The thread ID for the thread.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugCreateThread(clsDebugger As DebuggerClass, _

dwProcessID As Long, dwThreadID As Long)

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DebugExitThread

' DISCUSSION :

' Called when the debugger thread gets a thread exit notification.

' PARAMETERS :

' clsDebugger - The debugger class to query for additional information.

' dwProcessID - The process ID for the process.

' dwThreadID - The thread ID for the thread.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugExitThread(clsDebugger As DebuggerClass, _

dwProcessID As Long, dwThreadID As Long)

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DebugDllLoad

' DISCUSSION :

' Called when the debugger thread gets a DLL load notification.

' PARAMETERS :

' clsDebugger - The debugger class to query for additional information.

' dwProcessID - The process ID for the process.

' dwThreadID - The thread ID for the thread.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugDllLoad(clsDebugger As DebuggerClass, _

dwProcessID As Long, dwThreadID As Long)

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DebugDllUnload

' DISCUSSION :

' Called when the debugger thread gets a DLL unload notification.

' PARAMETERS :

' clsDebugger - The debugger class to query for additional information.

' dwProcessID - The process ID for the process.

' dwThreadID - The thread ID for the thread.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugDllUnload(clsDebugger As DebuggerClass, _

dwProcessID As Long, dwThreadID As Long)

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DebugODS

' DISCUSSION :

' Called when the debugger thread gets an OutputDebugString

' notification

' PARAMETERS :

' clsDebugger - The debugger class to query for additional information.

' dwProcessID - The process ID for the process.

' dwThreadID - The thread ID for the thread.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugODS(clsDebugger As DebuggerClass, _

dwProcessID As Long, dwThreadID As Long)

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DebugException

' DISCUSSION :

' Called when the debugger thread gets an exception notification.

' PARAMETERS :

' clsDebugger - The debugger class to query for additional information.

' dwContType - The way to continue debugging.

' dwProcessID - The process ID for the process.

' dwThreadID - The thread ID for the thread.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugException(clsDebugger As DebuggerClass, _

dwContType As Long, dwProcessID As Long, _

dwThreadID As Long)

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DebugRipInfo

' DISCUSSION :

' Called when the debugger thread gets an RIP notification.

' PARAMETERS :

' clsDebugger - The debugger class to query for additional information.

' dwProcessID - The process ID for the process.

' dwThreadID - The thread ID for the thread.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DebugRipInfo(clsDebugger As DebuggerClass, _

dwProcessID As Long, dwThreadID As Long)

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : PauseProcess

' DISCUSSION :

' When the debug process has been told to pause, this function is

' called because it is up to the executive to do the SuspendThreads.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub PauseProcess()

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : ResumeProcess

' DISCUSSION :

' When the debug process has been told to resume, this function is

' called because it is up to the executive to do the ResumeThreads.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub ResumeProcess()

End Sub

CriticalSection.cls

VERSION 1.0 CLASS

BEGIN

MultiUse = -1 'True

END

Attribute VB_Name = "CriticalSection"

Attribute VB_GlobalNameSpace = False

Attribute VB_Creatable = True

Attribute VB_PredeclaredId = False

Attribute VB_Exposed = False

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' John Robbins, Microsoft Systems Journal - August 1997

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FILE : CriticalSection.cls

' DESCRIPTION :

' A nice helper class that encapsulates critical section handling.

' If you use this class to control access to data, make sure you set

' up all the functions that use it with error handlers to make sure

' that the Leave method is called after the enter. In the following

' example, g_CritSec is the critical section class.

'

'Public Sub DoSomething ( )

' g_CritSec.Enter

' On Error GoTo DoSomething_Error

' ' Do some work here!

' g_CritSec.Leave

' Exit Sub

'

'DoSomething_Error:

' g_CritSec.Leave

'End Sub

'

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Class private variables

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private m_CritSec As CRITICAL_SECTION

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Class_Initialize

' DISCUSSION :

' The initialization for the class. This just initializes the private

' CRITICAL_SECTION structure.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub Class_Initialize()

InitializeCriticalSection m_CritSec

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Class_Terminate

' DISCUSSION :

' The termination for the class. This just frees up the private

' CRITICAL_SECTION structure.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub Class_Terminate()

DeleteCriticalSection m_CritSec

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Enter

' DISCUSSION :

' Locks on the critical section by calling EnterCriticalSection.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub Enter()

EnterCriticalSection m_CritSec

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Leave

' DISCUSSION :

' Unlocks the critical section by calling LeaveCriticalSection.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub Leave()

LeaveCriticalSection m_CritSec

End Sub

DebuggerClass.cls

VERSION 1.0 CLASS

BEGIN

MultiUse = -1 'True

END

Attribute VB_Name = "DebuggerClass"

Attribute VB_GlobalNameSpace = False

Attribute VB_Creatable = True

Attribute VB_PredeclaredId = False

Attribute VB_Exposed = False

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' John Robbins, Microsoft Systems Journal - August 1997

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FILE : DebuggerClass.cls

' DESCRIPTION :

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Class private variables

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' The required full filename of the debuggee.

Private m_szDebuggee As String

' The optional command line to use.

Private m_szCmdLine As String

' The optional startup directory.

Private m_szWorkDir As String

' The executive class that the debugger will use.

Private m_clsExecutive As BaseExecutive

' The synchronization class that the debugger will use. This is its own

' instance.

Private m_clsSynch As DebugSynchClass

' The process ID for the debuggee.

Private m_dwDebuggeePID As Long

' The error code returned when I tried to create the debuggee.

Private m_dwCreateError As Long

' The individual debug events. These are declared as private members

' and all have specific friend property get functions that will return

' them. If the debugger class is supposed to move into an ActiveX

' control, then this decision will have to be rethought.

Private m_CreateProcessDbgEvt As CREATE_PROCESS_DEBUG_INFO

Private m_CreateThreadDbgEvt As CREATE_THREAD_DEBUG_INFO

Private m_ExceptionDbgEvt As EXCEPTION_DEBUG_INFO

Private m_ExitProcessDbgEvt As EXIT_PROCESS_DEBUG_INFO

Private m_ExitThreadDbgEvt As EXIT_THREAD_DEBUG_INFO

Private m_ODSDbgEvt As OUTPUT_DEBUG_STRING_INFO

Private m_LoadDllDbgEvt As LOAD_DLL_DEBUG_INFO

Private m_UnloadDllDbgEvt As UNLOAD_DLL_DEBUG_INFO

Private m_RipInfo As RIP_INFO

Friend Property Set clsBaseExecutive(clsBase As BaseExecutive)

Set m_clsExecutive = clsBase

End Property

Friend Property Get CreateProcessDbgEvt() As CREATE_PROCESS_DEBUG_INFO

CreateProcessDbgEvt = m_CreateProcessDbgEvt

End Property

Friend Property Get CreateThreadDbgEvt() As CREATE_THREAD_DEBUG_INFO

CreateThreadDbgEvt = m_CreateThreadDbgEvt

End Property

Friend Property Get ExceptionDbgEvt() As EXCEPTION_DEBUG_INFO

ExceptionDbgEvt = m_ExceptionDbgEvt

End Property

Friend Property Get ExitProcessDbgEvt() As EXIT_PROCESS_DEBUG_INFO

ExitProcessDbgEvt = m_ExitProcessDbgEvt

End Property

Friend Property Get ExitThreadDbgEvt() As EXIT_THREAD_DEBUG_INFO

ExitThreadDbgEvt = m_ExitThreadDbgEvt

End Property

Friend Property Get ODSDbgEvt() As OUTPUT_DEBUG_STRING_INFO

ODSDbgEvt = m_ODSDbgEvt

End Property

Friend Property Get LoadDllDbgEvt() As LOAD_DLL_DEBUG_INFO

LoadDllDbgEvt = m_LoadDllDbgEvt

End Property

Friend Property Get UnloadDllDbgEvt() As UNLOAD_DLL_DEBUG_INFO

UnloadDllDbgEvt = m_UnloadDllDbgEvt

End Property

Friend Property Get RipInfo() As RIP_INFO

RipInfo = m_RipInfo

End Property

Friend Property Get dwDebuggeePID() As Long

dwDebuggeePID = m_dwDebuggeePID

End Property

Friend Property Get dwCreateError() As Long

dwCreateError = m_dwCreateError

End Property

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Class_Initialize

' DISCUSSION :

' The initialization for the class.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub Class_Initialize()

Set m_clsExecutive = Nothing

Set m_clsSynch = New DebugSynchClass

m_dwDebuggeePID = 0

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Class_Terminate

' DISCUSSION :

' The termination for the class.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub Class_Terminate()

Set m_clsExecutive = Nothing

Set m_clsSynch = Nothing

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : SetDebuggeeInfo

' DISCUSSION :

' Sets the information that MUST be set before the class can be used.

' PARAMETERS :

' szDebuggee - The full path and name of the program to debug.

' szCmdLine - The command line for the debuggee. This can be

' vbNullString.

' szWorkDir - The working directory for the debuggee. This can be

' vbNullString.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub SetDebuggeeInfo(szDebuggee As String, _

Optional szCmdLine As String = vbNullString, _

Optional szWorkDir As String = vbNullString)

' Set the variables.

m_szDebuggee = szDebuggee

m_szCmdLine = szCmdLine

m_szWorkDir = szWorkDir

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : StartDebuggee

' DISCUSSION :

' Starts up the debuggee. This can only be called from the debug

' thread.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Function StartDebuggee() As Boolean

On Error GoTo StartDebuggee_Error

Dim bRet As Long

Dim si As STARTUPINFO

Dim pi As PROCESS_INFORMATION

' Initialize si and pi to known values.

si.cb = &H44

si.cbReserved2 = 0

si.dwFillAttribute = 0

si.dwFlags = 0

si.dwX = 0

si.dwX = 0

si.dwY = 0

si.dwXSize = 0

si.dwYSize = 0

si.dwXCountChars = 0

si.dwYCountChars = 0

si.dwFillAttribute = 0

si.dwFlags = 0

si.wShowWindow = 0

si.cbReserved2 = 0

si.lpReserved2 = 0

si.hStdInput = 0

si.hStdOutput = 0

si.hStdError = 0

pi.hProcess = 0

pi.hThread = 0

pi.dwProcessID = 0

pi.dwThreadID = 0

' Try and start up the application to debug.

bRet = CreateProcess(m_szDebuggee, m_szCmdLine, 0, 0,0, _

DEBUG_ONLY_THIS_PROCESS, _

vbNullString, m_szWorkDir, si, pi)

' Set the create error.

m_dwCreateError = GetLastError

' If the CreateProcesses failed, signal the event that the debuggee

' failed to start, and then leave.

If (0 = bRet) Then

m_clsSynch.SignalBadStartup

StartDebuggee = False

Exit Function

End If

' Close some handles I don't need.

bRet = CloseHandle(pi.hProcess)

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("StartDebuggee CloseHandle failed!")

End If

#End If

bRet = CloseHandle(pi.hThread)

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("StartDebuggee CloseHandle failed!")

End If

#End If

' Set the unique PID for the debuggee.

m_dwDebuggeePID = pi.dwProcessID

m_clsSynch.dwUniqueID = m_dwDebuggeePID

' Create the synchronization objects for the synch class for just

' this thread.

m_clsSynch.CreateSynchObjects

' At least the debuggee was able to start so signal that event.

m_clsSynch.SignalGoodStartup

StartDebuggee = True

Exit Function

StartDebuggee_Error:

MsgBox ("Got an error in StartDebuggee: " + Err.Description)

End Function

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : ProcessDebugEvents

' DISCUSSION :

' Does the looping and processing of debug events.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub ProcessDebugEvents()

Dim DbgEvt As DEBUG_EVENT

Dim dwContType As Long

Dim bRet As Long

Dim bDebugRunning

Dim iObject As Long

' The debugger loop is always running.

bDebugRunning = True

While (True = bDebugRunning)

' Which thing am I supposed to be doing?

iObject = m_clsSynch.WaitForSynchObject(INFINITE)

Select Case iObject

' Quitting.

Case 0

bDebugRunning = False

' Pausing.

Case 1

m_clsExecutive.PauseProcess

' Resuming.

Case 2

m_clsExecutive.ResumeProcess

' Regular debugging.

Case 3

' Wait a bit for the debug event.

If (1 = WaitForDebugEvent(DbgEvt, 100)) Then

' Default to continuing.

dwContType = DBG_CONTINUE

' Process all the debug events by calling the executive

' class that I was passed at startup.

Select Case DbgEvt.dwDebugEventCode

Case CREATE_PROCESS_DEBUG_EVENT

LSet m_CreateProcessDbgEvt = DbgEvt.dwUnionData

m_clsExecutive.DebugCreateProcess Me, _

DbgEvt.dwProcessID, _

DbgEvt.dwThreadID

Case CREATE_THREAD_DEBUG_EVENT

LSet m_CreateThreadDbgEvt = DbgEvt.dwUnionData

m_clsExecutive.DebugCreateThread Me, DbgEvt.dwProcessID, _

DbgEvt.dwThreadID

Case EXIT_PROCESS_DEBUG_EVENT

LSet m_ExitProcessDbgEvt = DbgEvt.dwUnionData

m_clsExecutive.DebugExitProcess Me, DbgEvt.dwProcessID, _

DbgEvt.dwThreadID

' EXIT_PROCESS_DEBUG_EVENT is handled special in

' that I will just go ahead and exit the

' processing subroutine.

Exit Sub

Case EXIT_THREAD_DEBUG_EVENT

LSet m_ExitThreadDbgEvt = DbgEvt.dwUnionData

m_clsExecutive.DebugExitThread Me, DbgEvt.dwProcessID, _

DbgEvt.dwThreadID

Case EXCEPTION_DEBUG_EVENT

LSet m_ExceptionDbgEvt = DbgEvt.dwUnionData

m_clsExecutive.DebugException Me, dwContType, _

DbgEvt.dwProcessID, _

DbgEvt.dwThreadID

Case OUTPUT_DEBUG_STRING_EVENT

LSet m_ODSDbgEvt = DbgEvt.dwUnionData

m_clsExecutive.DebugODS Me, DbgEvt.dwProcessID, _

DbgEvt.dwThreadID

Case LOAD_DLL_DEBUG_EVENT

LSet m_LoadDllDbgEvt = DbgEvt.dwUnionData

m_clsExecutive.DebugDllLoad Me, DbgEvt.dwProcessID, _

DbgEvt.dwThreadID

Case UNLOAD_DLL_DEBUG_EVENT

LSet m_UnloadDllDbgEvt = DbgEvt.dwUnionData

m_clsExecutive.DebugDllUnload Me,DbgEvt.dwProcessID, _

DbgEvt.dwThreadID

Case RIP_EVENT

LSet m_RipInfo = DbgEvt.dwUnionData

m_clsExecutive.DebugRipInfo Me, DbgEvt.dwProcessID, _

DbgEvt.dwThreadID

End Select

bRet = ContinueDebugEvent(DbgEvt.dwProcessID, DbgEvt.dwThreadID, _

dwContType)

End If

Case Else

#If DEBUGBUILD Then

MsgBox ("WaitForSynchObject returned something other " & _

"than 0 - 3!!")

#End If

End Select

Wend

End Sub

DebugSynchClass.cls

VERSION 1.0 CLASS

BEGIN

MultiUse = -1 'True

END

Attribute VB_Name = "DebugSynchClass"

Attribute VB_GlobalNameSpace = False

Attribute VB_Creatable = True

Attribute VB_PredeclaredId = False

Attribute VB_Exposed = False

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' John Robbins, Microsoft Systems Journal - August 1997

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FILE : DebugSynchClass.cls

' DESCRIPTION :

' The class that handles the chores of encapsulating all of the

' synchronization objects that are used to coordinate the debugger

' thread and the user interface.

' When looking this over, you might wonder why there is no

' synchronization handling to indicate that the debug thread is fully

' shut down. While it could be done, it would be redundant because

' the UI should be triggered off of the debug thread handle. That's

' the only way to ensure that a thread is really done processing.

' Each thread MUST create their own instances of this class.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Class private constants

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' The event that signals when I had a good debuggee process startup.

' The PID is appended to it.

Private Const k_GOODSTARTUPSTRING As String = "VBDEBUG_GOODSTARTUP"

' The event that signals when I had a failure of the debuggee process

' startup. The PID is appended to it.

Private Const k_BADSTARTUPSTRING As String = "VBDEBUG_BADSTARTUP"

' The array indexes for the startup events.

Private Const k_GOODSTARTID As Long = 0

Private Const k_BADSTARTID As Long = 1

' The core synchronization objects: the debugging event, quit, pause,

' and resume events. These have the DEBUGGEE process ID appended to

' them to make sure that they are unique.

Private Const k_QUITSTRING As String = "VBDEBUG_QUIT"

Private Const k_PAUSESTRING As String = "VBDEBUG_PAUSE"

Private Const k_RESUMESTRING As String = "VBDEBUG_RESUME"

Private Const k_DEBUGSTRING As String = "VBDEBUG_DEBUG"

' The IDs for which elements in the event array belong to which. Too

' bad VB won't let you expose constants.

' The quit event is first because the WaitForMultipleObjects function

' will return on it first when it is signaled before looking at the

' regular debug events.

Private Const k_QUITEVENT As Long = 0

Private Const k_PAUSEEVENT As Long = 1

Private Const k_RESUMEEVENT As Long = 2

Private Const k_DEBUGACTIVEEVENT As Long = 3

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Class private variables

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' The process ID of the debugger.

Private m_hControlPID As Long

' The process ID for the debuggee.

Private m_hDebuggeePID As Long

' The core events I wait on, the quit, pause, resume, and debug.

Private m_hDebugEvents(4) As Long

' The startup events that can be signaled.

Private m_hStartupEvents(2) As Long

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Class_Initialize

' DISCUSSION :

' The initialization for the class.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub Class_Initialize()

m_hDebugEvents(0) = 0

m_hDebugEvents(1) = 0

m_hDebugEvents(2) = 0

m_hDebugEvents(3) = 0

m_hDebuggeePID = 0

m_hStartupEvents(0) = 0

m_hStartupEvents(1) = 0

m_hControlPID = GetCurrentProcessId()

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : Class_Terminate

' DISCUSSION :

' The termination for the class.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub Class_Terminate()

' If the handles have not been destroyed, do it now.

If ((0 <> m_hDebugEvents(k_QUITEVENT)) And _

(0 <> m_hDebugEvents(k_DEBUGACTIVEEVENT))) Then

DeleteSynchObjects

End If

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Properties and methods only for the debug thread!

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : dwUniqueID

' DISCUSSION :

' The property set for the unique ID for the core synch events.

' PARAMETERS :

' dwID - The value.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Property Let dwUniqueID(dwID As Variant)

m_hDebuggeePID = dwID

End Property

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : SignalGoodStartup

' DISCUSSION :

' Signals that the debuggee started up correctly. This is only called

' from the debug thread. The UI is supposed to be waiting in the

' WaitForStartup method.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub SignalGoodStartup()

SignalManualResetEvent (k_GOODSTARTUPSTRING + Hex$(m_hControlPID))

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : SignalBadStartup

' DISCUSSION :

' Signals that the debuggee failed to start correctly. This is only

' called from the debug thread. The UI is supposed to be waiting in

' the WaitForStartup method.

' PARAMETERS :

' None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub SignalBadStartup()

SignalManualResetEvent (k_BADSTARTUPSTRING + Hex$(m_hControlPID))

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : CreateSynchObjects

' DISCUSSION :

' The function that creates the synchronization objects that are used

' to coordinate everything between the debug thread and the UI.

' PARAMETERS :

' None.

' RETURNS :

' True - Everything was created correctly.

' False - There was a problem.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Function CreateSynchObjects() As Boolean

Dim bRet As Long

' I assume that the return value will be good.

CreateSynchObjects = True

#If DEBUGBUILD Then

If (0 = m_hDebuggeePID) Then

MsgBox ("CreateSynchObjects called before the unique id set!")

End If

#End If

If (0 = m_hDebuggeePID) Then

CreateSynchObjects = False

Exit Function

End If

' Create the quit event. It is a manual reset event that is not signaled

m_hDebugEvents(k_QUITEVENT) = CreateEvent(0, 1, 0, k_QUITSTRING + _

Hex$(m_hDebuggeePID))

#If DEBUGBUILD Then

If (0 = m_hDebugEvents(k_QUITEVENT)) Then

MsgBox ("CreateSynchObjects unable to create Quit event!")

End If

#End If

If (0 = m_hDebugEvents(k_QUITEVENT)) Then

CreateSynchObjects = False

Exit Function

End If

' Create the pause event. It is a manual reset event that is not signaled

m_hDebugEvents(k_PAUSEEVENT) = CreateEvent(0, 1, 0, k_PAUSESTRING + _

Hex$(m_hDebuggeePID))

#If DEBUGBUILD Then

If (0 = m_hDebugEvents(k_PAUSEEVENT)) Then

MsgBox ("CreateSynchObjects unable to create Pause event!")

End If

#End If

If (0 = m_hDebugEvents(k_PAUSEEVENT)) Then

bRet = CloseHandle(m_hDebugEvents(k_QUITEVENT))

CreateSynchObjects = False

Exit Function

End If

' Create the resume event. It is a manual reset event that is not signaled

m_hDebugEvents(k_RESUMEEVENT) = CreateEvent(0, 1, 0, k_RESUMESTRING + _

Hex$(m_hDebuggeePID))

#If DEBUGBUILD Then

If (0 = m_hDebugEvents(k_RESUMEEVENT)) Then

MsgBox ("CreateSynchObjects unable to create Resume event!")

End If

#End If

If (0 = m_hDebugEvents(k_RESUMEEVENT)) Then

bRet = CloseHandle(m_hDebugEvents(k_QUITEVENT))

bRet = CloseHandle(m_hDebugEvents(k_PAUSEEVENT))

CreateSynchObjects = False

Exit Function

End If

' Create the debug event. It is a manual reset event that is

' signaled.

m_hDebugEvents(k_DEBUGACTIVEEVENT) = CreateEvent(0, 1, 1, k_DEBUGSTRING + _

Hex$(m_hDebuggeePID))

#If DEBUGBUILD Then

If (0 = m_hDebugEvents(k_DEBUGACTIVEEVENT)) Then

MsgBox ("CreateSynchObjects unable to create Debug event!")

End If

#End If

If (0 = m_hDebugEvents(k_DEBUGACTIVEEVENT)) Then

bRet = CloseHandle(m_hDebugEvents(k_QUITEVENT))

bRet = CloseHandle(m_hDebugEvents(k_PAUSEEVENT))

bRet = CloseHandle(m_hDebugEvents(k_RESUMEEVENT))

CreateSynchObjects = False

Exit Function

End If

End Function

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : DeleteSynchObjects

' DISCUSSION : Deletes the synchronization objects created with

' CreateSynchObjects.

' PARAMETERS : None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub DeleteSynchObjects()

Dim bRet As Long

#If DEBUGBUILD Then

If ((0 = m_hDebugEvents(k_DEBUGACTIVEEVENT)) Or _

((0 = m_hDebugEvents(k_QUITEVENT)))) Then

MsgBox ("DeleteSynchObjects called before CreateSynchObjects!")

End If

#End If

bRet = CloseHandle(m_hDebugEvents(k_DEBUGACTIVEEVENT))

m_hDebugEvents(k_DEBUGACTIVEEVENT) = 0

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("DeleteSynchObjects failed closing debug event handle")

End If

#End If

bRet = CloseHandle(m_hDebugEvents(k_QUITEVENT))

m_hDebugEvents(k_QUITEVENT) = 0

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("DeleteSynchObjects failed closing quit event handle")

End If

#End If

bRet = CloseHandle(m_hDebugEvents(k_PAUSEEVENT))

m_hDebugEvents(k_PAUSEEVENT) = 0

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("DeleteSynchObjects failed closing pause event handle")

End If

#End If

bRet = CloseHandle(m_hDebugEvents(k_RESUMEEVENT))

m_hDebugEvents(k_RESUMEEVENT) = 0

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("DeleteSynchObjects failed closing resume event handle")

End If

#End If

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : WaitForSynchObject

' DISCUSSION :

' This is the function that the debug thread will call to figure out

' what it should be doing.

' PARAMETERS :

' The maximum time, in milliseconds to wait. The SDK define INFINITE

' is permitted.

' RETURNS :

' 0 - The quit event was signaled.

' 1 - The pause event was signaled.

' 2 - The resume event was signaled.

' 3 - The debug event was signaled.

' Otherwise - See the return values for WaitForMultipleObjects

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Function WaitForSynchObject(dwWaitTime As Long) As Long

#If DEBUGBUILD Then

If ((0 = m_hDebugEvents(k_DEBUGACTIVEEVENT)) Or _

((0 = m_hDebugEvents(k_QUITEVENT)))) Then

MsgBox ("DeleteSynchObjects called before CreateSynchObjects!")

End If

#End If

Dim dwRet As Long

dwRet = WaitForMultipleObjects(4, m_hDebugEvents(0), 0, dwWaitTime)

' Handle the resets on Pause and Resume.

Dim bTemp As Long

If (k_PAUSEEVENT = dwRet) Then

bTemp = ResetEvent(m_hDebugEvents(k_DEBUGACTIVEEVENT))

' Put the pause back into its normal state.

bTemp = ResetEvent(m_hDebugEvents(k_PAUSEEVENT))

ElseIf (k_RESUMEEVENT = dwRet) Then

bTemp = SetEvent(m_hDebugEvents(k_DEBUGACTIVEEVENT))

bTemp = ResetEvent(m_hDebugEvents(k_RESUMEEVENT))

End If

WaitForSynchObject = dwRet

End Function

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Properties and methods only for the UI thread!

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : PrepareWaitForStartup

' DISCUSSION :

' The UI MUST call this function before it can call WaitForStartup.

' It must also be called before spawning the debug thread as well.

' As I found out on those really, really fast DEC Alphas, the debugger

' thread can fail the CreateProcess and call SignalBadStartup

' before the UI can call WaitForStartup, which, you guessed it, causes

' massive deadlock. By requiring the UI to call this function first,

' I make sure that the event is ready. 400Mhz really helps show

' synchronization problems!

' PARAMETERS : None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub PrepareWaitForStartup()

' Create the events

m_hStartupEvents(k_GOODSTARTID) = CreateEvent(0, 1, 0, k_GOODSTARTUPSTRING + _

Hex$(m_hControlPID))

m_hStartupEvents(k_BADSTARTID) = CreateEvent(0, 1, 0, k_BADSTARTUPSTRING + _

Hex$(m_hControlPID))

#If DEBUGBUILD Then

If ((0 = m_hStartupEvents(k_BADSTARTID)) Or _

(0 = m_hStartupEvents(k_GOODSTARTID))) Then

MsgBox ("PrepareWaitForStartup failed to create events")

End If

#End If

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : WaitForStartup

' DISCUSSION :

' The UI thread calls this after it has created the thread so that it

' can then check if life got cranked up. The UI must call

' PrepareWaitForStartup FIRST!

' PARAMETERS : None.

' RETURNS :

' TRUE - The debuggee was started correctly.

' FALSE - The debuggee was not started correctly.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Function WaitForStartup() As Boolean

#If DEBUGBUILD Then

If ((0 = m_hStartupEvents(k_BADSTARTID)) Or _

(0 = m_hStartupEvents(k_GOODSTARTID))) Then

MsgBox ("WaitForStartup can only be called after " + _

"PrepareWaitForStartup!")

End If

#End If

Dim dwRet As Long

Dim bRet As Long

' Wait for one of the startup events.

dwRet = WaitForMultipleObjects(2, m_hStartupEvents(0), 0, INFINITE)

' Close the handles.

bRet = CloseHandle(m_hStartupEvents(k_BADSTARTID))

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("WaitForStartup CloseHandle(k_BADSTARTID) failed!")

End If

#End If

bRet = CloseHandle(m_hStartupEvents(k_GOODSTARTID))

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("WaitForStartup CloseHandle(k_GOODSTARTID) failed!")

End If

#End If

If (k_GOODSTARTID = dwRet) Then

WaitForStartup = True

Else

WaitForStartup = False

End If

End Function

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : PauseDebugThread

' DISCUSSION : Pauses the debug thread. Only the UI thread should call this!

' PARAMETERS : None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub PauseDebugThread()

#If DEBUGBUILD Then

If ((0 = m_hDebugEvents(k_DEBUGACTIVEEVENT)) Or _

((0 = m_hDebugEvents(k_QUITEVENT)))) Then

MsgBox ("PauseDebugThread called before CreateSynchObjects!")

End If

#End If

Dim bRet As Long

bRet = SetEvent(m_hDebugEvents(k_PAUSEEVENT))

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("PauseDebugThread ResetEvent failed!")

End If

#End If

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : ResumeDebugThread

' DISCUSSION : Resumes the debug thread. Only the UI thread should call this!

' PARAMETERS : None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub ResumeDebugThread()

#If DEBUGBUILD Then

If ((0 = m_hDebugEvents(k_DEBUGACTIVEEVENT)) Or _

((0 = m_hDebugEvents(k_QUITEVENT)))) Then

MsgBox ("ResumeDebugThread called before CreateSynchObjects!")

End If

#End If

Dim bRet As Long

bRet = SetEvent(m_hDebugEvents(k_RESUMEEVENT))

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("ResumeDebugThread SetEvent failed!")

End If

#End If

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : QuitDebugThread

' DISCUSSION : Quits the debug thread. Only the UI thread should call this!

' PARAMETERS : None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub QuitDebugThread()

#If DEBUGBUILD Then

If ((0 = m_hDebugEvents(k_DEBUGACTIVEEVENT)) Or _

((0 = m_hDebugEvents(k_QUITEVENT)))) Then

MsgBox ("QuitDebugThread called before CreateSynchObjects!")

End If

#End If

' All I have to do is set the debug event to the signaled state.

Dim bRet As Long

bRet = SetEvent(m_hDebugEvents(k_QUITEVENT))

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("QuitDebugThread SetEvent failed!")

End If

#End If

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Class Private Helper Routines

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FUNCTION : SignalManualResetEvent

' DISCUSSION :

' A helper function for SignalGoodStartup and SignalBadStartup. Simply

' creates and signals the event with the passed in string.

' PARAMETERS : None.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Sub SignalManualResetEvent(szStr As String)

Dim hEvent As Long

Dim bRet As Long

' Create the event.

hEvent = CreateEvent(0, 1, 1, szStr)

#If DEBUGBUILD Then

If (0 = hEvent) Then

MsgBox ("SignalManualResetEvent failed to create event")

End If

#End If

If (0 = hEvent) Then

Exit Sub

End If

' Signal the event.

bRet = SetEvent(hEvent)

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("SignalManualResetEvent failed to set event")

End If

#End If

' Close the handle.

bRet = CloseHandle(hEvent)

#If DEBUGBUILD Then

If (0 = bRet) Then

MsgBox ("SignalManualResetEvent CloseHandle failed!")

End If

#End If

End Sub

DebugThread.bas

Attribute VB_Name = "DebugThread"

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' John Robbins, Microsoft Systems Journal - August 1997

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' FILE : DebugThread.bas

' DESCRIPTION :

' Since the AddressOf operator can only take things out of .BAS

' modules, this is the actual debug thread and the thread function.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

' The function that starts the debug thread.

Public Function StartDebugThread(clsDebug As DebuggerClass) As Long

Dim hThread As Long

Dim lThreadID As Long

' Create the thread.

hThread = CreateThread(0, 0, AddressOf DebugThread, clsDebug, 0, lThreadID)

StartDebugThread = hThread

End Function

' The actual debug thread.

Public Function DebugThread(clsDebug As DebuggerClass) As Long

On Error GoTo DebugThread_Error

Dim boolRet As Boolean

boolRet = clsDebug.StartDebuggee

If (False = boolRet) Then

DebugThread = 0

Exit Function

End If

' Process debug events until done.

clsDebug.ProcessDebugEvents

DebugThread = 1

Exit Function

DebugThread_Error:

MsgBox ("Got an error in DebugThread: " + Err.Description)

End Function

Once the debug thread is up and running, you need to protect the data accessed by both threads. Fortunately, the SimpleExecutive class is the only class accessed by multiple threads. Since VBDebug is designed so that it will never have multiple processes accessing the debugger data, the critical section—the simplest of synchronization objects—can protect everything. I wanted to make the critical section as easy to use as possible, so I created a class, CriticalSection, that encapsulates the critical section operations. In CriticalSection.cls, you can see that I use the Initialize and Terminate methods to call InitializeCritical­Section and DeleteCriticalSection, respectively. This is good object-oriented design because it hides the grunge work from the user. When you need to enter a critical section to block access to some data, you simply call the Enter method. When you are finished accessing the data, you simply call the Leave method. Whatever you do, make sure that you match up the calls to Enter and Leave. If you don’t, you’ll have an instant deadlock because the critical section will be owned by one thread permanently. Using the Critical­Section class requires that you have an error handler in each function that calls the Enter method so that you guarantee a matching Leave method is called, even if something bad happens. This will take a lot of planning to get it right.

There is only one Critical­Sec­tion instantiation in VB­Debug, the g_clsCritSec private variable in the SimpleExe­cutive class. The best places to see it used are in the DumpActiveThreads and DumpLoadedDLLs methods. As their names suggest, they dump the current active threads and the loaded DLLs to the UI. When either of these methods is called, the first thing it does is grab the class critical section g_clsCritSec by calling the Enter method. Every class event in SimpleExecutive that can be accessed from multiple threads must grab the class critical section before it does anything. If DumpActiveThreads is in the middle of listing the current threads to the screen and a new thread event notification is sent to the debug loop, then the BaseExecutive_DebugCreateThread method will block until DumpActiveThreads releases the class critical section. All of these gyrations make sure the data is safe and accessed by only one thread at a time. If the new thread was added by BaseExecutive_DebugCreateThread before DumpActive­Threads was finished, there is no telling what output you would get from DumpActiveThreads. It could be wrong, or it might even crash.

After the debug thread is running and the data in SimpleExecutive is protected, you have to do the really hard part: synchronize the UI thread and the debugger thread. To me, getting multiple threads properly synchronized is one of the most difficult aspects of multithreading. One of the main reasons that I did VBDebug as the big sample program for this article is that the thread synchronization for a debugger is pretty difficult; I wanted to show you how it could be done in a real-world instance.

In VBDebug, the debugger thread really does not tell the UI thread much, but the UI thread needs to tell the debug thread a lot. Specifically, the debugger thread needs to tell the UI thread when the debuggee started—or failed to start—and when the debug thread ended. The UI thread needs to tell the debugger thread when to start debugging, when to stop debugging, when to pause debugging, and when to restart debugging.

As I demonstrated in Part I of this article (MSJ, August 1997), Win32 event objects are an excellent way for one thread to signal a state change to another thread. Here, I put all of the events that are needed, and the times that you would want to wait on an event, in a single Visual Basic class, DebugSynchClass.cls, which is designed so the debug and UI threads can each create their own instances and use them appropriately. Looking at DebugSynchClass, you will see that the concept of getting threads to communicate is simple, but the implementation is a little more involved.

If you take a look in the VBDebug\ReUse sub­direc­tory, you’ll see another ­inter­esting item: Debug­­­Synch­­­Class.cls is the largest of the reusable files by a good bit. A lot of work goes on in there to hide the nasty implementation details so that each thread just has to call a single method when appropriate. Although I placed all the synchronization code for the UI and debug threads into DebugSynchClass, you might consider splitting them into separate classes. Since the key to using Win32 event objects is to keep the names straight and unique, I thought it was better to have everything lumped together and just document which thread is allowed to call what method.

Before getting the debug thread started, the UI thread is responsible for calling the DebugSynchClass Prepare­WaitForStartup method to prepare for startup. After creating the debug thread, the UI thread must call the DebugSynchClass WaitForStartup method to block and wait for the debug thread to start. The debug thread, after attempting to start the debuggee, must call the Debug­SynchClass SignalGoodStartup or SignalBadStartup methods to indicate how the CreateProcess call worked. The code for the UI-required methods is rather simple: Prepare­WaitForStartup creates two manual reset events, Wait­For­Startup tells WaitForMultipleObjects to wait until one of the events is signaled, and WaitForStartup returns True if the startup was successful. The debug thread-required methods, SignalGoodStartup or SignalBadStartup, create and signal the appropriate event.

The startup synchronization code itself is also very simple and looks like just about every other piece of sample code that demonstrates event object synchronization. There are two things that make the code interesting. If you look at the parameters passed to CreateEvent in the code, there is a value appended to the end of the constant string which is initialized to the current process ID in the Class_Initialize method. The reason for appending this value is logical when you think about it. First, remember that in Win32 events themselves can be accessed across processes, so it’s possible that two instances of VBDebug could be running at the same time. If there is some twist of the scheduler, both UIs could be waiting to see if the debuggee started. If the event doesn’t have a unique name, the instant that one of the debug threads signals how the CreateProcess worked, both process’s UI threads would continue when only one should. In VBDebug, you might be able to play around with autoreset events, but I want to prepare you for worst-case scenarios. By adding the unique process ID to the event name, you avoid any problems with multiple processes.

The other interesting part of the startup synchronization is that the UI thread has to prepare the startup events before it can create the debug thread. I originally developed the code so that the DebugSynchClass WaitForStartup method created the events to wait on and then did the WaitForMultipleObjects. This worked fine when I tested VBDebug on Intel machines. However, when I tested it on a 400MHz DEC Alpha, I ran into a small problem: if the CreateProcess failed on the debuggee, then the UI thread would block and hang in the DebugSynchClass WaitFor­Startup method. Obviously, I had a synchronization problem, but I didn’t see how it could have happened.

After setting breakpoints with WinDBG everywhere and running VBDebug many times in the debugger, I found that it sometimes runs correctly under the debugger. After placing calls to OutputDebugString in strategic places, I was surprised to find that after the CreateProcess failed in the debug thread, the call to the DebugSynchClass Signal­BadStartup method created the event, signaled it, and destroyed it before the UI thread could get the two events created in the DebugSynchClass WaitForStartup method! I suspect that either the DEC Alpha’s excellent speed got in the way, or there are some subtle differences in the Windows NT thread handling between Intel and Alpha CPUs. When I created the DebugSynchClass PrepareWait­For­Startup and called it before creating the debug thread, everything worked out fine.

After the UI and debug threads are running, the synchronization gets a little easier. As both threads are purring along, the only time something happens is when the UI needs to tell the debug thread how to behave. Essentially, the UI thread needs to create the appropriate event and signal it so the debug thread can respond. Obviously, the debug thread needs to be waiting on some events, and these correspond to the pause, restart, and quit events discussed earlier. Again, like the startup, this arrangement is nothing unusual.

The debug thread in VBDebug needs to continue running so that it can process debug events but still be responsive to the events signaled by the UI thread. In the debug loop, I wait to see what UI-debug synchronization events are signaled. If the UI thread does not signal anything, I take a quick peek to see if there are any debug events to process and loop back to the UI-debug synchronization. All of the UI-debug synchronization is wrapped in the DebugSynch­Class WaitForSynchObject method and it is called in the DebuggerClass ProcessDebugEvents method.

The DebugSynchClass WaitForSynchObject method returns one of four values, zero through 3, which correspond to what the UI thread tells the debug thread to do. The only time something does not match up to the synchronization conditions described earlier is when the DebugSynchClass WaitForSynchObject method returns 3, which means that the UI thread did not signal anything so it is safe to call Wait­ForDebugEvent. Each of the return values for the Debug­SynchClass WaitFor­SynchObject method indicates which event was signaled.

Inside the DebugSynchClass is an array of events created by calling the CreateSynchObjects method. The first three handles in the array are quit, pause, and resume events. These three events are manual-reset events that are created in the nonsignaled state. The final event is the debug active event, which is a manual-reset event created in the signaled state. Since the debug and UI threads need unique events for a particular instance of VBDebug, I append the debuggee process ID to the event names to keep them unique. While I could have appended the VBDebug process ID, this would have limited me to a single debug loop per process, which might be fine for now but would limit future growth.

The DebugSynchClass Wait­ForSynchObject method calls WaitForMultipleObjects on the array of event handles. Since the debug active event is always signaled, Wait­For­MultipleObjects returns immediately every time. If the UI does signal a synchronization event, WaitForSynchObject takes care of getting all the event object states set in the debug thread since all the events are manual-reset events. For example, if the UI thread signals that it wants to pause, the debug active event is set to nonsignaled and the pause event is set to nonsignaled. The reset event is similar except that it sets the debug active event back to signaled and clears the reset event. I probably could have made pause and reset autoreset events instead of manual-reset events, but I wanted to make sure I kept everything consistent. Remember the motto of multithreading: Trust Nothing—Verify Everything!

Handling Thread Demise

So far, I have discussed handling the synchronization for startup and when the UI thread needs to tell the debug thread to do something, but I have not discussed what happens when the debug thread terminates normally. Specifically, I am now going to cover how the UI thread knows that the debug thread ended without polling some global variable. This turned out to be a much nastier problem than I thought because I had to work around the Visual Basic UI portions—which do not all appear to be thread-safe—to solve it.

If I were writing a C++ debugger, I would have simply passed an HWND (the main UI window) into the Debugger­Class, specifying that it uses PostMessage to send a private message to that window. Once that message was received in the UI, I could reset the state of the UI so the debugger was ready to debug again. Instead of passing the HWND all the way down into the Core Debugger portions, the idea is to create a secondary background thread whose sole purpose is to wait on the debug thread handle with WaitFor­SingleObject. Waiting on the actual debug thread handle is the only way to guarantee that the debug thread truly ended. After the debug thread handle is signaled, the secondary thread calls PostMessage to notify the main window. This is a fairly tried-and-true method of handling this situation.

Since Visual Basic does not, without subclassing, allow you to handle private messages, I set a protocol where the HWND for the form that would receive the message also handles the MouseDown event, indicating that the debug thread was finished. I chose the MouseDown event because I only had to do a PostMessage with the WM_
MBUTTONDOWN message. To ensure that I was not handling a legitimate message, I required that the X and Y parameters to the Mouse­Down event both be –15 (remember, these are client coordinates, so a multiple monitor system as David Campbell described in the June 1997 MSJ won’t break my code). When I posted the message as the last thing in the debug thread before it ended, it worked perfectly and made it quite easy to know when the debug thread ended. Then I ran VBDebug on a multiprocessor machine and, unfortunately, the UI never received the WM_MBUTTONDOWN message so the UI would never get notified that the debug thread ended. Little problems like these are why it is absolutely vital that you test your multithreaded applications on as many different machines as possible.

I should have expected this because the Visual Basic Books Online specifically say that the apartment threading model support is only for non-UI components. On the single-processor machines, I was getting lucky that it worked. When I tried tracking the lost message down, all I could tell was that the PostMessage went off and the message was lost deep down in the bowels of MSVB­VM50.DLL. I must point out here that this is not a bug in Visual Basic. Multithreading Visual Basic-based programs is undocumented hacking, and Microsoft never said that they support full multithreading.

Unfortunately, VBDebug was one of those applications that needed to do something with the UI in response to another thread ending. This meant that even if I called a form method directly from the debug thread, I could still cause some problems with the Visual Basic runtime.

Since I had no other choice, I decided that I would force the UI update myself by calling one of the methods directly on the form. Since I did not want to lose all the benefits of the reusable code I had developed, I did not want to be passing a specific form type all the way through the Executive and DebuggerClass. I decided to create a secondary background thread that waits for the debug thread to end, making it easier to isolate the pieces that have to change in future versions. This secondary thread, the Wait­For­End­OfDebugThread function in EndDbg­Thread.bas, will call the DebugThreadEnded method from the main VBDebug form to help the UI change its state.

The code in the frmVBDebug DebugThreadEnded method simply calls the private SetUIState where the work of setting the menus and the new caption takes place. While this code might look simple, it brushes up against one of the areas where the runtime is not thread-safe. Even though the menu and the caption are all set correctly, the fact that the form Caption property is set from another thread ends up not quite getting the titlebar text set. After the call to SetUIState in the DebugThreadEnded method, I have to call the SetWindowText API with the same settings to get the title text updated.

Like GDI objects, it looks like Visual Basic UI objects should not be passed for use by different threads. There might be a few other ways that you could get an indication that a thread has ended. In a commercial multithreaded application, the best way to overcome the small problem on the nonthread-safe UI would be to create a couple of small ActiveX™ controls that handle the synchronization. The control would signal an event that would indicate to the UI that a thread has ended. Whatever mechanism that you finally decide on, I cannot stress enough: test, test, test, and test some more!

Independent Study

Now that you have the groundwork for a debugger, here are some additional ideas to try on your own:

The whole point is to experiment a little and see what you can come up with. By the time you get done writing a moderately featured debugger, you will have an excellent understanding of the entire operating system.

Conclusion

The new Visual Basic 5.0 AddressOf operator is a pretty magical thing. Now you can do things in Visual Basic that I never expected. By utilizing some of the Win32 APIs, you are able to support multithreading in Visual Basic. Be sure to check your code carefully, since some portions of Visual Basic do not support multithreading completely. However, if you use multithreading carefully and you test well, you will be able to make your applications more responsive and useful to the user. The extra functionality you gain might get you more cash in your pocket, or at least happier users.

To obtain complete source code listings, see page 5.