Demystifying The Debugger

by Russell Eggen

Published 1999-05-25    Printer-friendly version

In some of my recent classes and a few newsgroup articles, I was asked to explain or show the debugger. Those in my class may testify that they could have seen a wicked looking smile. Those asking on the newsgroups let me assure you that the smile was indeed there.

I really don't know why the debugger has fallen out of use so quickly. I am big fan of using debuggers.

In CPD 2.x, Clarion users had a debugger, sort of. Remember all those CTRL-S and CTRL-F keystrokes to see one line of code? Not too many things to inspect, but it did work, slow as it was.

In version 3.x, Clarion Software (Topspeed's former name) delivered a really robust, state of the art debugger. I loved it! At the time I was also working in a COBOL shop. Micro Focus made one of the best debuggers I have ever seen. Reading pages of COBOL code is quite a task, and to navigate through so much very quickly is quite a task. The CFD debugger had about the same power and ease of use. No wonder this was one of my most favorite versions of Clarion. (OK, I am a minority on this point, but that is my story and I am sticking to it).

When Clarion for Windows first came out, it wasn't long before I hit the debugger. What is an event anyway? Why did I have to go through the ACCEPT statement so many times and why did I have to step through all those lines of code before I got to the interesting bits?

The debugger seemed OK, but why did I have step through all those routines? I was not interested in those, just my code. I gave up just like everyone else and started using the MESSAGE() function. But that takes some typing to set it up right. So I began using the STOP() statement.

When I couldn't figure out why some code was behaving the way it was, or it started working with all the STOP() statements in place, I was even more mystified! One of my co-workers suggested that I use the debugger. Oh NO! Not the dreaded debugger! PLEASE! I was in a hurry, deadlines approaching. I felt like I was fighting for my life. I fired up the debugger and stepped through every line of code in my procedure. I did eventually find the problem and fixed it. But what a chore that was!

A couple of years ago I had the fortune of talking with designer of the debugger. I started asking some questions. He answered them. I still remember the last question I asked him: "How long has that been in there?!" After we finished our drinks, I had to go back to my room and try a few things. Son of gun! Will ya look at that! I was getting excited about debuggers again! There were the breakpoints, the fast stepping, and even watch windows! He told me how these features worked.

However I wasn't completely convinced. At the time I was having problems with a conversion program, and I had STOP() statements everywhere. The problem was all the values shown by my STOP() statements were correct, but the application still wouldn't work properly. I was sure there was nothing wrong with my code.

Out of desperation, I fired up the debugger, and I found the bug in 20 seconds. This after four hours of reading the code, STOP() statements, etc. The problem wasn't that my code had bugs. In fact, the debugger proved that my code was good, but it did not work! The code I wrote did not have bugs in it; the code I did not write was the problem.

Huh? The problem was that I forgot to add code to OPEN() a file. My bug was missing code! How can STOP() or MESSAGE() ever find that problem? Of course this exposed a few more lines of code I needed to write (error checking, adding or changing the converted data, etc). Needless to say, it was not one of my better moments (or programs)!

In this case, the debugger showed to me that there was an error being returned by the PUT() statement: "File not open." MESSAGE() and STOP() can report this, but you have to write the code to display the error. If I had thought of that, I would have written the error checking in the first place! This lesson taught me a few things, and the first one was humility. In this example, STOP() and MESSAGE() would not have helped. Neither will be of much help in getting into deeper issues either, so you might as well stop using them.

Why not use STOP() and MESSAGE() for debugging? Well, let me be a bit blunt. Neither is designed for debugging! STOP() is designed to suspend program execution (see the Help topic). This means it requires a response from the user. The MESSAGE() procedure displays a Windows-standard message box, typically requiring only a Yes or No response, or no specific response at all. It is also interacting with a user.

These functions process events too! Have you ever had a procedure filled with STOP() or MESSAGE() statements, and it started behaving strangely or went on an entirely new and wrong path? Guess what was messing with your event messages? What about all those embeds you put these functions in, only to get a phone call from your customer that he got a STOP() message! What guarantee is there that you removed all of them before you shipped the product?

I hate to break this to you, but STOP() and MESSAGE() are not safe debugging tools! You have enough to debug as it is!

Enter The Debugger

OK, I admit it. I prefer the 32-bit debugger to the 16-bit. I have my reasons. I think the interface is cleaner and easier to navigate quickly. Also, the startup process makes it easier to get to the code where I think the bug lives. You can also use the 32-bit debugger on NT. The 16-bit debugger won't run on NT - period. NT is designed to crash low level 16-bit applications. And I also like how much easier it is to add watch variables in the 32-bit debugger.

I will now walk through the debugger using the MailList application, which you can find in your Clarion /examples directory. First I'll explain typical debugging techniques, and later I'll show you how the debugger is also a good testing tool (this is one of the secrets I discovered that does not require anyone to pick up a dinner tab). Also I'll give you some tips for using the debugger outside of the Clarion IDE (yes, you read that correctly).

The Basics

Any project you want to use the debugger on must have Debug turned on in the project settings (this is the shipping default). See Figures 1 and 2. If you have changed these settings to Build Release then you must uncheck this and set Debug to Full. You'll also need to set the Target OS to Windows – 32 bit, as this article focuses on the 32 bit debugger.

Figure 1. The global project dialog.

debugger_fig1.gif (5565 bytes)

TIP: If you use string slicing or other code to step through arrays, then set the Array Index checkbox on. This will trap out of range subscript errors..

Figure 2. Global debug settings with array checking on.

debugger_fig2.gif (4981 bytes)

Save your project settings and choose Project|Debug from the main menu or click on the debug button on the toolbar. (Editor's note: If you still have some Clarion 1.0 beta disks sitting around in your archives, dust them off and have a look at the debugger EXE with a resource editor – you'll see some entertaining alternative images to the standard bug-with-the-red-line-drawn-over-it)

After compiling and making the executable, you see something similar to Figure 3:

Figure 3. The 32 bit debugger at startup.

debugger_fig3.gif (20981 bytes)

Now what? The first thing I do is select the procedure I want to debug. You can select more than one, but lets keep it simple for now. I will debug the BrowsePeople procedure. Scroll down the procedures list until you find that procedure name.

TIP: The procedure and module columns are sortable. The default sort is the procedure column and it uses a step locator. Click on the heading for module and the list resorts accordingly.

At this point, as soon as you click on the procedure name, its source file is instantly visible in the source window. I minimized the rest of the windows at this point and resize the source window so that it fills about 3/4 of the visible screen (it can go virtual if you stretch it past the dimensions of the screen, but I don't like that, so I don't use it).

At the bottom of the debugger window, you have the Trace, Disassembler, Procedure (See figure 3), Stack Trace and Globals windows, all minimized. Here's a brief tour of some of these windows.

What if you want to see a variable defined in your dictionary? Since variables declared here are considered global, restore the Globals window. Where are the file variables? The entire display is collapsed (it is a tree display). All dictionary variables are under the node named after your application, in this case, MailList. Variables declared under the global button in your app can also be found here.

Expand the People tree. All variables are sorted alphabetically to make them easier to find. Highlight PEO:FirstName and right click- a pop-up menu appears. There is only one item on the pop-up: Watch Variable. Click on this and presto! The watch window appears. It is at this time that I like to resize and move the watch window under my source window. Now add PEO:LastName in the same fashion. You should have two variables in the watch window. Since the debugger has not yet begun to move through the code, the contents of these should be blank.

If I were to add numeric data or single elements of a string (strings are arrays of characters), then I would have had two more items in the pop-up window, Edit Variable and Examine Memory. You cannot edit an entire string, but you can edit or examine a specific string array element. Expand the PEO:LastName variable and then right click on one of the elements to see these options.

Minimize the global window as there is no further need of it. Move and resize the two open windows so they are orderly. Here is how my screen looks like after I have completed this step:

Figure 4. Source code in the debugger.

debugger_fig4.gif (17994 bytes)

Where Should You Debug?

Page down to BRW1.SetQueueRecord while in the source window. Click on the first line after CODE. This line formats the name fields into a local variable. The line should be green in color. Press the letter "B" or double click or right click and select Set Breakpoint. This sets a hard break. Hard break means that until the debugger hits this line of code, you do not have to step through each line until you get to this point. The debugger gets focus when this line of code is hit. The line also changes color to red indicating a hard break point. It isn't red? Press the down arrow key to highlight the next line. It is now. The current position is green and hard breaks are red. Red and green make yellow. Cute, eh?

Now you are ready to debug, sort of. You cannot add any local variables to the watch window as you are not really in this procedure. But you soon will. To start the debug process, I like pressing the letter G (or you can right-click and choose Go from the popup menu, or from the pulldown menu, Debug/Go). Your program is running normally.

Now choose Browse|People to send mailings. The debugger now gets focus at the breakpoint. Since Clarion uses a stack calling convention (a fancy term for resource management), you will see that there is also a Stack Trace window. This is where you will find your local variables as they have now been pushed onto the stack. Restore the stack trace window. Expand BrowsePeople. Now find the local variable named LastFirst. Right click on this and choose Watch Variable to add it to the watch window. You see the variable is now blank. Minimize the Stack Trace window to move it from the workspace as it is no longer needed. The source window should be the active window.

Figure 5. Source code and watch variables in the debugger.

debugger_fig5.gif (17092 bytes)

If you right click and choose Step Source (or press the letter T), the current line moves down one line of code and right before your eyes, LastFirst is changed in the watch window.

Now for some fun press G and wait for the debugger to go around and update the global variables. Now press T and watch the local variable change to its new contents. It will do this until the list box is filled.

You can set as many break points as you wish to get the job done, inspecting the contents of any variable you wish (as long as it is in scope). Remove the hard break, by repeating how you set it. Now press the Go button and your app runs normally. You can now choose another source file to debug, if you wish.

If you are done debugging, there is something very important you must do. Close down the client application normally, then close down the debugger. Closing down the debugger with the debuggee still active could cause memory leaks, or worse.

What Did You Discover?

  • You discovered that you do not have to step through every friggin' line of code. If this was required, I think I would take my chances with STOP() and MESSAGE().
  • Going right to a line of code you want to check out, setting breakpoints, and then running the application until a breakpoint is hit is much faster than coding STOP() and MESSAGE() statements (and safer!).
  • You have no code to remove once you are done (and no danger of forgetting an embed used for debugging and having your customer find it).

What about debugging DLLs?

DLLs are applications too, and the same rules apply.

Here's a common example. Do you use a data DLL (no procedures, just dct declarations and template variables)? If this DLL's debug options are set to Release Mode, guess what you cannot see, let alone place, in a watch window? Global data. This means every field defined in your dictionary. You won't find a one. So remember to change your project settings there too, before you debug.

Tips and Tricks!

One of the tricks I have "taught" the debugger to do (or is it the other way around?) is to assist me testing an application. Any good developer tries to predict what a user can and will do and writes code accordingly. How many of you have had customers call you and say, "I did this and it went bang?" Customers are a very creative lot.

As an example I will use a program that simply reports the event that has fired. This application is used in the Essentials class, so any former Essential student has this program and can follow along. You can also download the application. The NameEvent routine that returns the event string also checks for a user defined event and unknown errors. The code looks sound (it is quite simple), but here is the question: How can I test this to prove that it works? I don't want to write code that force things down a certain logic path, as that can be risky. Writing more code, no matter how simple, adds complexity to a project, increasing risk of logic errors. What would be ideal is that I steer the program down the "once in a blue moon" logic path, without writing any extra code. Besides, I am too lazy for that!

I can now safely test each condition coded to see if it works at runtime, just like your customers will do. Then I know it works as I have seen it perform as I designed it. Is that not how you should test an application, like a customer would use it?

What I need to do is setup my project to use the debugger (again 32-bit) and start it. In this case, all the code is in one source file. The code I am interested in is at the end of a function that returns a string informing the user what event has just executed, as seen in Listing 1.

Listing 1. Partial source listing showing what the debugger will test.
NameEvent   FUNCTION(EventNumber)
ReturnString  STRING(20)
  CODE
  CASE EventNumber
  OF 01H
    ReturnString = 'EVENT:Accepted'
  ! Many more event tests omitted
  OF 213H
    ReturnString = 'EVENT:Maximize'
  OF 214H
    ReturnString = 'EVENT:MInimize'
  OF 215H
    ReturnString = 'EVENT:Completed'
  OF 400H TO 0FFFH
    ReturnString = EventNumber & ' - User Defined EVENT'
  ELSE
    ReturnString = EventNumber & ' - Unknown EVENT'
  END
  RETURN(ReturnString)

As you can see, this code is part of a large CASE structure, so I have set my break point at CASE EventNumber. At the first event that fires, the debugger gets focus because least one event has fired. Now I need to bring up the Stack Trace window and find the local variable called EventNumber (which is also seen in figure 5 above). Once I do this, right click and select Edit Variable.

Here is something useful. The listing above is testing hex numbers, but the edit window is prompting for a decimal number. No problem. Simply enter the new value as shown in Figure 6:

Figure 6. Enter a hex number by typing 'h' after  the number.

debugger_fig6.gif (2141 bytes)

Click on OK and then "Step Source". Notice in Figure 7 where the cursor has landed?

Figure 7. Detecting a user-defined event.

debugger_fig7.gif (16344 bytes)

The code works as expected and now I have proof it works. The value 501h is certainly between the range reserved for user defined events and the code is picking this up. I can also test for the out of range condition by giving some value that cannot be trapped in this case structure. In this case, I would get the "unknown event."

Moral Of The Story

The debugger can test for all kinds of conditions in your code. If the code doesn't work, then it is a simple matter of setting a few more breakpoints and clearing breakpoints no longer needed until you narrow down what the exact problem is.

A very good friend of mine once told me "the correct 'why' opens the door for handling." And my grandfather told me that all solutions are simple. The debugger is certainly living proof of both sayings.

Sometimes the biggest bugs in our code are staring at right at us, like my embarrassing example earlier. In my experience, the nastiest bugs are hiding in plain sight. It is a relief and a real shout of "doh!" or a slap of the forehead when you finally find them. Once you find the problem, then writing the correction is quite simple.

Registry Settings

If you would like to run the debugger outside of the Clarion IDE (i.e. right click on your EXE and launch the debugger), then try adding these entries to the registry (backup your registry file first - you have the tools needed to do this already. Regedit will do this and Windows 98 will do this upon startup). You may need to change the Clarion drive and/or path to suit your own machine.

[HKEY_CLASSES_ROOT\exefile\shell\16bit Debugger]
[HKEY_CLASSES_ROOT\exefile\shell\16bit Debugger\command]
@="c5db.exe c:\\clarion5\\bin\\clarion5.red c:\\clarion5\\bin\c5ee.ini %1"

[HKEY_CLASSES_ROOT\exefile\shell\32bit Debugger]
[HKEY_CLASSES_ROOT\exefile\shell\32bit Debugger\command]
@="c5dbx.exe c:\\clarion5\\bin\\clarion5.red c:\\clarion5\\bin\c5ee.ini %1"

[HKEY_CLASSES_ROOT\exefile\shell\Restart Debugger]
[HKEY_CLASSES_ROOT\exefile\shell\Restart Debugger\command]
@="c5db.exe /r"

These settings will launch the debugger from Explorer with a right click (pop-up menu). The last grouping restarts the 16-bit debugger with all of your previous settings and windows. This is not yet available in the 32-bit debugger.

Post Mortem Debugging

If you install the 32-bit debugger as your system debugger (from the debugger's Setup menu), then when your program that crashes (GPFs), you get this familiar window with a small addition, as shown in Figure 8.

Figure 8. GPF with debug option.

debugger_fig8.gif (3736 bytes)

If you press the debug button, then the 32-bit debugger starts and tries to find the source that caused the problem. This works with non-Clarion programs too (although you may not have the source), although in this case you will get the disassembly window.

OK, I have a confession to make. I have used the debugger with a bit of an evil twist.

As I stated above, this works on other programs. I have used this to report bugs to other vendors. I simply grab the screen shots of the disassembly window (which shows the code that crashed) along with the memory register contents. I then send the contents to the support folks and ask them "When will I get my fix?" I would be less than honest if I said that I did not get some sort of satisfaction with that statement! In case anyone is interested, I did get a fix! And it worked. If anyone is curious, it was a VB program – 'nuff said.

Conditional Break Points

The debugger does not support conditional breakpoints, I regret to say. This missing feature is known about and when it will make its debut is beyond the scope of this article. However, you are not stuck. You can code your own break points. How?

Imagine you are processing a large number of records (lets make it a cool half million). The process hangs or goes bang every time the process reaches record number 486,457, just when you're almost done! The problem is that you are using a key and filters and this means that physical record number 486,457 is very likely not the cause. Give the debugger some embedded code to check for. In ThisProcess.TakeRecord, you can write the safe code in Listing 2.

Listing 2. Code for the debugger to watch.
?IF Count# = 486457
?  i" = 'Set hard break here'
?ELSE
?  Count# += 1
?END

Since this code is for the debugger only, there's no problem with using implicit variables. It really is throw-away code. So set your hard break on the string and choose Go. When the condition is met (486,457th record in sequence), the debugger becomes active. Now go find which record this is (by adding variables to the watch window) and use tools such as TopScan to find out what is this record's problem. I have run into problems with programs I wrote only to find the source of the problem was bad data, so be on the lookout for it.

It's your turn to try this. How would you write conditional code to find out when a value changes? Sometimes you need to test for a condition that should not change, but it does.

This technique takes almost no overhead, and if you leave it in your code it really doesn't do any harm. The trick is to keep debug code totally encapsulated. It is far more dangerous to use real variables as other processes can come along and change the values.

If you really do not like using implicits (and there are a few of you out there), then add variables in a suitable data embed. That is equally as safe.

TIP: The "?" symbol means that this code is compiled only when debug is turned on. If you switch to release mode, then the code is not compiled at all.

Summary

I certainly hope that you got some good information out of this article. You should now see that the Clarion debugger is a more useful tool than most people know.

I showed you how to step through code that you are interested in. This means that you do not have to step through all manner of code that has nothing to do with your problem, just the lines that you need to look at.

You can use the debugger to test logic flows in your code (especially those that may not be run that often).

I showed some setup tips that make getting ready to use the debugger easier to use. I also explained why STOP() and MESSAGE() are not really designed for debugging and why they are not safe to do so.

I enjoyed writing up my notes and sharing these with you. I am still wearing that smile. In all of these discussions, I have to ask you one last question:

Did anyone see me debug the ABC classes?

Download the source


Russ Eggen has been using Clarion since 1986. Until about 1996, he was using it for business applications, mostly accounting programs. Afterwards he joined Topspeed as a consultant, and later as an instructor. He was a founding member of SoftVelocity when that company formed from Topspeed in May 2000. He left SoftVelocity in January 2001 and now works for his own company, RadFusion Inc. He still teaches and lectures, and is currently working on a new book and setting up a local Clarion classroom. Russ enjoys flying, scuba, and applied philosophy, and with great effort you might coax him into political discussions.

Printer-friendly version

Reader Comments

To add a comment to this article you must log in.

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $184

(includes all back issues since '99)

Renewals from $134

Two years: $274

Renewals from $224

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links