Jack Robbins
John Robbins is a software engineer at NuMega Technologies, Inc., who specializes in debuggers. He can be reached at john@jprobbins.com.
I realize that most developers don’t write debuggers 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 dwCreationFlags 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 WaitForDebugEvent API to receive debugging notifications. When it’s finished processing them, it calls ContinueDebugEvent. 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 multithreading, 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, WaitForDebugEvent 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 WaitForDebugEvent 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_DEBUG_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
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.lpImageName 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_PROCESS_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 breakpoint 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 breakpoint 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 debugger 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.
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 Debugger. 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
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
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
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]"
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 VBDebug 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.
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_DebugCreateThread, 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_colThreads. 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_DebugDllUnload, 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 GetThreadContext 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, BaseExecutive_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 BaseExecutive_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.
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 multithreaded 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 frmVBDebug.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 SimpleExecutive 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. StartDebugThread 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
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
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
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
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
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 InitializeCriticalSection 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 CriticalSection 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 CriticalSection instantiation in VBDebug, the g_clsCritSec private variable in the SimpleExecutive 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 DumpActiveThreads 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 subdirectory, you’ll see another interesting item: DebugSynchClass.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 PrepareWaitForStartup 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 DebugSynchClass SignalGoodStartup or SignalBadStartup methods to indicate how the CreateProcess call worked. The code for the UI-required methods is rather simple: PrepareWaitForStartup creates two manual reset events, WaitForStartup 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 WaitForStartup 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 SignalBadStartup 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 PrepareWaitForStartup 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 DebugSynchClass 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 WaitForDebugEvent. Each of the return values for the DebugSynchClass WaitForSynchObject 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 WaitForSynchObject method calls WaitForMultipleObjects on the array of event handles. Since the debug active event is always signaled, WaitForMultipleObjects 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!
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 DebuggerClass, 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 WaitForSingleObject. 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 MouseDown 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 MSVBVM50.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 WaitForEndOfDebugThread function in EndDbgThread.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!
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.
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.