It's amazing what you can do in an hour...

| | Comments (12)

I suspect that one of the reasons people aren't using System.DebugLog (at least, on Windows) is because there's no built-in viewer for it. On OS X, you have the Console (not to be confused with the Terminal). On Linux, you have the console (which is the same thing as a terminal). But on Windows, you need to download a 3rd party application in order to view this output.

So that got me to thinking -- I wonder if I can write my own debug string viewer in REALbasic. I figure, hey, if I can make this work on Windows, then I'm pretty sure I can get it to work on the Mac (or Jon, Mars or Joe could) as well as Linux. And if it's written in pure RB code, then I might be able to include it in the product (however, after completing this little exercise, I realized a better way to do things that would work with remote debugging as well).

So I wrote a Win32 debugger in an hour using REALbasic.

The first thing I did was make a thread subclass, because I knew I'd need to loop and wait for input, but I didn't want the rest of my UI to lock up. In the thread's Run event, the first thing I do is attach to a running process as a debugger, like this:[rbcode]
Declare Function DebugActiveProcess Lib "Kernel32" ( proc as Integer ) as Boolean
dim procId as Integer = mProcID
if not DebugActiveProcess( procId ) then
MsgBox "Could not debug the process specified"
return
end if[/rbcode]
I won't go into how I find the process IDs, but it is trivial (I use WindowFromPoint and GetWindowThreadProcessId, in case you're wondering).

Now that I'm attached to a process as a debugger, I can do all sorts of fun things. I just need to wait for them to happen! So I set up an "infinite" while loop that does nothing more than wait for debugging events to occur, and handles the ones we care about.[rbcode]
Declare Function WaitForDebugEvent Lib "Kernel32" ( ev as Ptr, time as Integer ) as Boolean
Declare Sub ContinueDebugEvent Lib "Kernel32" ( proc as Integer, thread as Integer, status as Integer )

while true
// Make an event
dim theEvent as new MemoryBlock( 96 )

// Wait for a debug event
if not WaitForDebugEvent( theEvent, 0 ) then
// There was no event that we got, so let's yield the
// rest of our time slice
App.YieldToNextThread
end if

// We got an active debug event, so let's
// see if it's one for the debug string viewer
Const OUTPUT_DEBUG_STRING_EVENT = 8
Const EXIT_PROCESS_DEBUG_EVENT = 5
if theEvent.Long( 0 ) = OUTPUT_DEBUG_STRING_EVENT then
// We got a string!
GrabAndOutputString( theEvent )
elseif theEvent.Long( 0 ) = EXIT_PROCESS_DEBUG_EVENT then
// We're done debugging now
exit
end if

// Let's allow other debuggers to continue processing
Const DBG_CONTINUE = &h10002
ContinueDebugEvent( theEvent.Long( 4 ), theEvent.Long( 8 ), DBG_CONTINUE )
wend[/rbcode]
As you can see (through all the comments and stuff), the code really isn't that complicated. I just wait for an event, and if there is no event, I yield the rest of my thread's timeslice (the thought is that I yield instead of sleep because debugging events happen often enough that sleeping would delay the events too much). If there is an event, I figure out what type of event I got (there's only two that I care about) and process it. Once I'm done processing, I tell the application "you go ahead and finish whatever it was that you were doing."

In the case of an EXIT_PROCESS_DEBUG_EVENT, I simply break out of the while loop since the procss I am debugging is done.

The interesting case is the OUTPUT_DEBUG_STRING_EVENT, since that's the one that we need to capture information for. The GrabAndOutputString method may look big and scary, but it's actually a pussycat.[rbcode]
Sub GrabAndOutputString(theEvent as MemoryBlock)
static procDict as new Dictionary

// First, we need to get the process handle so that we can
// read the string's data.
Dim procHandle as Integer = 0
if procDict.HasKey( theEvent.Long( 4 ) ) then
// Great, we've already cached it! Just grab the handle
procHandle = procDict.Value( theEvent.Long( 4 ) )
else
// Shoot, we need to open the process ourselves.
Declare Function OpenProcess Lib "Kernel32" ( access as Integer, inherit as Boolean, _
procId as Integer ) as Integer

Const PROCESS_ALL_ACCESS = &h1F0FFF
procHandle = OpenProcess( PROCESS_ALL_ACCESS, false, theEvent.Long( 4 ) )

// Then store the process handle in our dictionary
procDict.Value( theEvent.Long( 4 ) ) = procHandle
end if

// If we have a process handle, then we need to
// read some memory from it.
if procHandle = 0 then
MsgBox "Could not read the process memory because there's no process handle"
return
end if

Declare Function ReadProcessMemory Lib "Kernel32" ( handle as Integer, _
base as Integer, buffer as Ptr, size as Integer, ByRef read as Integer ) as Boolean

// Check to see if we're reading unicode or ANSI
dim isUnicode as Boolean = theEvent.Short( 16 ) <> 0

// Get the number of characters to read in
dim numCharacters as Integer = theEvent.Short( 18 )

// Make a buffer large enough to hold all the data
dim bufSize as Integer = numCharacters
if isUnicode then bufSize = bufSize * 2
dim buffer as new MemoryBlock( bufSize )

// Now read in the data from the other process
dim read as Integer
if not ReadProcessMemory( procHandle, theEvent.Long( 12 ), buffer, buffer.Size, read ) then
MsgBox "Unable to read memory from the process"
return
end if

// We read the memory, so add it to our listbox
if isUnicode then
ListBox1.AddRow( "[" + Format( mProcID, "######" ) + "] " + buffer.WString( 0 ) )
else
ListBox1.AddRow( "[" + Format( mProcID, "######" ) + "] " + buffer.CString( 0 ) )
end if
End Sub[/rbcode]
The first chunk of code is simply a caching mechanism. You see, the string that we want to print out lives in another application's memory space (since all applications have their own, 100% logically separate memory space). So I need to be able to read into that other application's memory space, otherwise I can't get the string to print out! So the first thing I need to do is get an OS handle to the process we're debugging. I do this using the OpenProcess API call, and cache the results in the dictionary.

Once I have the process handle, the rest is just reading the string from the other process. I use ReadProcessMemory to grab the actual string. And I know the encoding of the string because the debugging event tells me whether I'm dealing with unicode (UTF-16) or ANSI. It also tells me how large the string is by giving me the number of characters (including the null) that I care about. So I setup a buffer to read into, and then read from the other process' memory space.

Ta da! Now I've got the debug string that the other process wanted to print out, so I simply stuff it into a ListBox.

Ignoring some support code (for things like easily getting the process ID of an application for the user, or making it so that the debug viewer doesn't kill the debug application when it quits), that's literally all the more code it took to write a simple Win32 debugger in REALbasic and it only took about an hour of my time (that includes time spent debugging).

I'm not releasing the entire app yet because I might decide to flesh it out into a full debugger sometime (just for fun, not for any real reason). That and there's still some very ugly code in there (stuff dealing with mouse cursors, and window locating code, yech!). But this should be more than enough to show you that you can do some really awesome stuff using REALbasic in such a short amount of time.

12 Comments

You rock, Aaron. Nice!

You've got a bug. If you didn't get an event, you need to "Continue" the loop after yielding -- not just yield to the next thread.

But other than that, nice work!

Yeah if this gets into REALbasic, definitely get it to work with Remote Debugging -- this is very exciting stuff. =)

@Jon -- good catch, I'll fix it in my code base

@Phil -- this code itself won't go into RB because it requires the app to be local or the remote debug stub to do the work. There's an easier mechanism that we can use. And you should file a feature request. ;-)

Wow! Maybe you ought to write a magazine article!

Aaron,

1 hour? I fear you. Yet, I cannot look away.

LoL @ Russ

Stupid question...

What about the Event Viewer in Administrative Tools? Given, I've never used a (recent) RB on Windows, but I just kind of assumed that's where the messages would be viewed. It has a section for "Application" messages that doesn't get used for anything else, at least.

Naw, the event viewer is for System.Log messages. It's there for permanent messages that you want to display to admins. The DebugLog is for ephermal messages that are only for debugging purposes and you don't want to keep them around.

When I started out on RB I didnt realize that there was a System.DebugLog ..

So I went and wrote my own debugging plugin so that I could simply call a method called XDebug "This is a test".. I was logging these to a seperate window with a clear/save/minimize function. I found this to be very useful, because I could leave the XDebug commands in a built application (The plugin will just ignore if it is not in debugger mode).

So now, I use a mix of XDebug and System.DebugLog (because I use WinDebug from SysInternals for unmonitored logging).

Anyway. This sort of functionality is great if it is built into RB (like VS) and there is some way of turning it off (there should really be some difference between a debug build and a release build).

There is a difference between debug and release builds, if you care to utilize it -- #if TargetDebug. The System.DebugLog is there for writing out to the debug log regardless of whether you're debugging or not. If we required it to be in debug builds only, then it loses a lot of its utility. For example, we use it extensively in the remote debugger, and it's helped me track down a handful of bugs without ever having to crack open source code or do special builds because I can just ask users to watch their debug logs and report back what they say.

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.