Writing a Debugger in Visual Basic

Introduction

Windows NT (and successive operating system versions) expose a set of API functions and structures for debugging a running process. This article shows how these can be accessed from Visual Basic (version 5 or 6). It is recomended that the article Inside the executable: an introduction to the Portable Executable format for VB programmers be read in conjunction with this article.

Getting a Process to Debug

There are two ways to get a process to debug. Either (a) attach a debugger to a process that is already running or (b) start a new process with a debugger attached to it.

Starting a new process with a debugger attached

To start a process, you can use the CreateProcess API call:

Private Declare Function CreateProcess
Lib _
   "kernel32" Alias "CreateProcessA" (ByVal lpApplicationName As String, _
   ByVal lpCommandLine As String, _ =
   ByVal lpProcessAttributes As Long,_
   ByVal lpThreadAttributes As Long,_
   ByVal bInheritHandles As Long, _ =
   ByVal dwCreationFlags As ProcessCreationFlags, _
   ByVal lpEnvironment As Long, _
   ByVal lpCurrentDirectory As String, _
   lpStartupInfo As STARTUPINFO,_
   lpProcessInformation As PROCESS_INFORMATION) _
As Long

In addition to the functionality of the Shell command, this allows you to specify additional flags that affect how the process is created:

Public Enum ProcessCreationFlags
   DEBUG_PROCESS              = &H1
   DEBUG_ONLY_THIS_PROCESS    = &H2
   CREATE_SUSPENDED           = &H4
   DETACHED_PROCESS           = &H8
   CREATE_NEW_CONSOLE         = &H10
   NORMAL_PRIORITY_CLASS      = &H20
   IDLE_PRIORITY_CLASS        = &H40
   HIGH_PRIORITY_CLASS        = &H80
   REALTIME_PRIORITY_CLASS    = &H100
   CREATE_NEW_PROCESS_GROUP   = &H200
   CREATE_UNICODE_ENVIRONMENT = &H400
   CREATE_SEPARATE_WOW_VDM    = &H800
   CREATE_SHARED_WOW_VDM      = &H1000
   CREATE_FORCEDOS            = &H2000
   CREATE_DEFAULT_ERROR_MODE  = &H4000000
   CREATE_NO_WINDOW           = &H8000000
End Enum

To start the process with an attached debugger, you specify the flags DEBUG_PROCESS + DEBUG_ONLY_THIS_PROCESS.

Attaching a debugger to an existing process

To attach a debugger to a process that is already running, you need to obtain a handle to it and then attach a debugger using the DebugActiveProcess API call.

Private Declare Function DebugActiveProcess Lib "kernel32" _
(ByVal dwProcessId As Long) As Long

The Debug Loop

Once you have attached your debugger to the process, you need to go into a debug loop. This consists of waiting for a debug event, processing the event when it comes in, and then allowing the debugee to continue.

Waiting for a debug event to occur

To wait for a debug event, you call the WaitForDebugEvent API call:

Private Declare Function WaitForDebugEvent Lib "kernel32" _
(lpDebugEvent As DEBUG_EVENT_BUFFER, _
 ByVal dwMilliseconds As Long) As Long

This will return TRUE when a debug event has occurred and filled out the DEBUG_EVENT_... structure, which depends on what event occurred but always starts with a DEBUG_EVENT_HEADER:

Private Type DEBUG_EVENT_HEADER
   dwDebugEventCode As DebugEventTypes
   dwProcessId As Long
   dwThreadId As Long
End Type

Processing the debug event

How you deal with a debug event depends, naturally enough, on what event occurred. The event types are:

Public Enum DebugEventTypes
   EXCEPTION_DEBUG_EVENT      = 1&
   CREATE_THREAD_DEBUG_EVENT  = 2&
   CREATE_PROCESS_DEBUG_EVENT = 3&
   EXIT_THREAD_DEBUG_EVENT    = 4&
   EXIT_PROCESS_DEBUG_EVENT   = 5&
   LOAD_DLL_DEBUG_EVENT       = 6&
   UNLOAD_DLL_DEBUG_EVENT     = 7&
   OUTPUT_DEBUG_STRING_EVENT  = 8&
   RIP_EVENT                  = 9&
End Enum

EXCEPTION_DEBUG_EVENT

This debug event is thrown whenever an exception occurs in the application being debugged. For example, if there was code in the application that was attempting to divide by zero, you would get an EXCEPTION_DEBUG_EVENT. The buffer that is passed back for this event is:

Public Enum ExceptionCodes
   EXCEPTION_GUARD_PAGE_VIOLATION     = &H80000001
   EXCEPTION_DATATYPE_MISALIGNMENT    = &H80000002
   EXCEPTION_BREAKPOINT               = &H80000003
   EXCEPTION_SINGLE_STEP              = &H80000004
   EXCEPTION_ACCESS_VIOLATION         = &HC0000005
   EXCEPTION_IN_PAGE_ERROR            = &HC0000006
   EXCEPTION_INVALID_HANDLE           = &HC0000008
   EXCEPTION_NO_MEMORY                = &HC0000017
   EXCEPTION_ILLEGAL_INSTRUCTION      = &HC000001D
   EXCEPTION_NONCONTINUABLE_EXCEPTION = &HC0000025
   EXCEPTION_INVALID_DISPOSITION      = &HC0000026
   EXCEPTION_ARRAY_BOUNDS_EXCEEDED    = &HC000008C
   EXCEPTION_FLOAT_DENORMAL_OPERAND   = &HC000008D
   EXCEPTION_FLOAT_DIVIDE_BY_ZERO     = &HC000008E
   EXCEPTION_FLOAT_INEXACT_RESULT     = &HC000008F
   EXCEPTION_FLOAT_INVALID_OPERATION  = &HC0000090
   EXCEPTION_FLOAT_OVERFLOW           = &HC0000091
   EXCEPTION_FLOAT_STACK_CHECK        = &HC0000092
   EXCEPTION_FLOAT_UNDERFLOW          = &HC0000093
   EXCEPTION_INTEGER_DIVIDE_BY_ZERO   = &HC0000094
   EXCEPTION_INTEGER_OVERFLOW         = &HC0000095
   EXCEPTION_PRIVILEGED_INSTRUCTION   = &HC0000096
   EXCEPTION_STACK_OVERFLOW           = &HC00000FD
   EXCEPTION_CONTROL_C_EXIT           = &HC000013A
End Enum

Public Enum ExceptionFlags
   EXCEPTION_CONTINUABLE = 0
   EXCEPTION_NONCONTINUABLE = 1    '\\ Noncontinuable exception
End Enum

Private Type DEBUG_EXCEPTION_DEBUG_INFO
   Header            As DEBUG_EVENT_HEADER
   ExceptionCode     As ExceptionCodes
   ExceptionFlags    As ExceptionFlags
   pExceptionRecord  As Long
   ExceptionAddress  As Long
   NumberParameters  As Long
   ExceptionInformation (EXCEPTION_MAXIMUM_PARAMETERS)   As Long
   dwFirstChance     As Long
End Type

The exception flags tell you whether it is possible to resume from the exception.

Writing a Debugger in Visual Basic

CREATE_THREAD_DEBUG_EVENT

This event occurs whenever a new thread is created by the debugee application. The buffer that is passed in is:

Private Type DEBUG_CREATE_THREAD_DEBUG_INFO
   Header As DEBUG_EVENT_HEADER
   hThread As Long
   lpThreadLocalBase As Long
   lpStartAddress As Long
End Type

This gives you a thread handle (for thread control API calls) and the base address and start address of the thread in the debugee process which is useful for analysing the memory of that application:

CREATE_PROCESS_DEBUG_EVENT

This event occurs when the process is created. The buffer passed in is:

Private Type DEBUG_CREATE_PROCESS_DEBUG_INFO
   Header                As DEBUG_EVENT_HEADER
   hfile                 As Long
   hProcess              As Long
   hThread               As Long
   lpBaseOfImage         As Long
   dwDebugInfoFileOffset As Long
   nDebugInfoSize        As Long
   lpThreadLocalBase     As Long
   lpStartAddress        As Long
   lpImageName           As Long
   fUnicode              As Integer
End Type

You can use the file handle passed in as part of this buffer to find the different parts of the process (imports section, exports, debug information, and so forth) as per this article.

EXIT_THREAD_DEBUG_EVENT

This event occurs when a thread exits. The buffer passed in is:

Private Type DEBUG_EXIT_THREAD_DEBUG_INFO
   Header As DEBUG_EVENT_HEADER
   dwExitCode As Long
End Type

The exit code is whatever the thread set it to but is usually set to be non-zero if an error caused the thread exit.

EXIT_PROCESS_DEBUG_EVENT

This event occurs when the process exits. The buffer passed in is:

Private Type DEBUG_EXIT_PROCESS_DEBUG_INFO
   Header As DEBUG_EVENT_HEADER
   dwExitCode As Long
End Type

The exit code is whatever the process set it to but is usually set to be non-zero if an error caused the thread exit. You should stop the debug loop after you recieve this event.

LOAD_DLL_DEBUG_EVENT

This event occurs when the application being debugged loads a dynamic link library. The buffer passed in is:

Private Type DEBUG_LOAD_DLL_DEBUG_INFO
   Header As DEBUG_EVENT_HEADER
   hfile As Long
   lpBaseOfDll As Long
   dwDebugInfoFileOffset As Long
   nDebugInfoSize As Long
   lpImageName As Long
   fUnicode As Integer
End Type

You can use the file handle passed in as part of this buffer to find the different parts of the DLL (imports section, exports, debug information, and the like) as per this article

UNLOAD_DLL_DEBUG_EVENT

This event occurs when the process being debugged unloads a DLL it had loaded. The buffer passed in is:

Private Type DEBUG_UNLOAD_DLL_DEBUG_INFO
   Header As DEBUG_EVENT_HEADER
   lpBaseOfDll As Long
End Type

And you can use the lpBaseOfDll value to identify which dll was unloaded

OUTPUT_DEBUG_STRING_EVENT

This event occurs when the debugee calls the API call OutputDebugString to send debugging information to a debugger (where one is attached). The buffer passed in is:

Private Type DEBUG_OUTPUT_DEBUG_STRING_INFO
   Header As DEBUG_EVENT_HEADER
   lpDebugStringData As Long
   fUnicode As Integer
   nDebugStringLength As Integer
End Type

And you can read the string from the debugee by using the ReadProcessMemory API call.

RIP_EVENT

This occurs if your process being debugged dies unexpectedly. The buffer passed in is:

Private Type DEBUG_RIP_INFO
    Header As DEBUG_EVENT_HEADER
    dwError As Long
    dwType As Long
End Type

Resuming the debugee

Once you have extracted the information you need from the debug event, you need to resume the debugee so that it can continue running. To do this you call the ContinueDebugEvent API call:

Public Enum DebugStates
   DBG_CONTINUE              = &H10002
   DBG_TERMINATE_THREAD      = &H40010003
   DBG_TERMINATE_PROCESS     = &H40010004
   DBG_CONTROL_C             = &H40010005
   DBG_CONTROL_BREAK         = &H40010008
   DBG_EXCEPTION_NOT_HANDLED = &H80010001
End Enum

Private Declare Function ContinueDebugEvent Lib "kernel32" _
(ByVal dwProcessId As Long, _
 ByVal dwThreadId As Long, _
 ByVal dwContinueStatus As DebugStates) As Long

Further Development

To expand on this framework and create a full debugger requires the ability to map the memory to the variables and/or functions used in the source code and walk the stack of the process being debugged and also to set breakpoints. I hope to get to this in the next article.



About the Author

Duncan Jones

Freelance developer with 10 years experience in Visual basic and SQL - now moving on up to the next generation with .NET

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

  • On-demand Event Event Date: October 29, 2014 It's well understood how critical version control is for code. However, its importance to DevOps isn't always recognized. The 2014 DevOps Survey of Practice shows that one of the key predictors of DevOps success is putting all production environment artifacts into version control. In this webcast, Gene Kim discusses these survey findings and shares woeful tales of artifact management gone wrong! Gene also shares examples of how high-performing DevOps …

Most Popular Programming Stories

More for Developers

RSS Feeds