Debugging in IDEs

Steven Zeil

Last modified: Jul 27, 2023
Contents:

Next we turn our attention to automated debuggers.

As we have discussed earlier, there are certain common functions that we expect an automatic debugger to provide:

The gdb debugger provides these functions for C++, and the jdb debugger does the same for Java. The basic interfaces of these debuggers leave a bit to be desired, however, which is why we studied the emacs debugging mode as an interface to them.

In fact, all of the debuggers that we will look at in this section are actually interfaces to the same underlying gdb and/or jdb debuggers.

In general, these debuggers come in two different forms. Some are standalone programs that can be used with any code that has been compiled with the appropriate compiler (and the appropriate compiler options). Others are part of a larger IDE, including each of the IDEs that we have just looked at.

1 emacs Debugging Mode

The gdb and jdb modes for controlling the debugger in emacs acquire some debugging-specific menus when emacs is run under X. Certainly, the emacs interface to the gdb and jdb debuggers remains a viable option when working under X. But the differences are small enough that we have really already covered this option sufficiently.

2 nemiver

 

nemiver is a standalone tool for C++ debugging.

Because nemiver is a standalone debugger interface, not tied to any of our IDEs, it is a useful complement to vim, the only one of our IDEs that lacks debugging support.

A nemiver session is contained within a single window, dominated by a source code viewer on the top and a multi-purpose panel below, which can be switched between an input/output console and a “context” area where program variables can be explored.

 
Example 1: Try This: nemiver
  1. For this exercise, you will use your code from one of the earlier IDE “Try This” exercises.

    • cd to ~/playing/vscode/sieve
    • Use your choice of Eclipse, VSCode, emacs, or vim to edit findPrimes.cpp. Near the bottom, you will find two statements that look like this:
    // bool* theSieve = NULL; // error
    bool* theSieve = new bool[maxNum]; // correct
    

    Comment out the second statement and remove the comment markers from the left of the first statement. We are, in fact, deliberately injecting a bug into the program for the purpose of illustration.

  2. Then, in an xterm window, cd to the directory where the IDE has placed your executable program.

    Compile the program using the provided makefile.

    Do an ls and make sure that the executable file findPrimes has been created.

  3. Give the command

    nemiver ./findPrimes
    

    (If your IDE called the compiled program something other than “findPrimes”, change the name in the above command accordingly.)

    The nemiver window should appear shortly. It will have started execution of your program, but paused at a breakpoint automatically set at the beginning of your main() function.

  4. Click the Context tab at the bottom to get a view of the current call stack (only main() has been called so far) and, to the right, the parameters and local variables of main().

    maxNum and theSieve could have any value at the moment. They have not been initialized yet, so they actually have whatever random bits happened to have been left at their respective locations in memory by earlier-running code.

  5. Click the Next (“stepping over”) button twice to move forward past the initialization of maxNum. Observe how the value of maxNum changes in the Context display.

  6. Click the Continue button. The program seems to hang up, waiting for something. Click on the “Target Terminal” tab at the bottom to view the console. You can see that you are being prompted for input. Enter an integer of your choosing.

    1. A box will pop up to tell you that a Segmentation Fault has occurred. This is one of several run-time errors that can occur when our code tries to access an illegal memory location.

      Click OK to clear that pop-up.

    2. Then click on the Context tab to view the stack and see where the problem occurred,

      In the display, you can see that main has called findPrimes, findPrimes has called sieve, sieve has called fill, and the crash occurred somewhere in fill.

    3. Click on any of the stack entries to see in what line of code the various calls take place. Notice that the display of local variables and function arguments changes each time you do, according to which function call you have selected.

  7. Now, return to your IDE or editor, fix the bug that we injected, and run make to recompile. nemiver will probably detect when you save the change to findPrimes.cpp and ask if you want to load the new, changed file. You can agree to this.

    Then, back in nemiver, click the Restart button. Again, when you see that you have stopped at the initial breakpoint, click Continue, then switch to the Target Terminal tab, and enter a number. This time, the program should run to completion. Click OK in the pop-up box that notifies you that the program is finished.

  8. Click the Restart button. Find the body of the findPrimes function, and locate the line that writes to cout. Set a breakpoint at this line by clicking in the margin, just to the right of the line number. Click continue, answer the prompt in the Target Terminal window, and observe that, this time, execution stops at your new breakpoint.

  9. There’s another way to display variables, particularly ones that are not listed in the context window (e.g., because they are not local variables of the current function). Let your mouse hover over one of the variables in the source code for a few seconds. Eventually a window will pop up containing the value of that specific variable. If it’s a structured variable (like message), you may need to expand the components of the variable in this pop-up window.

  10. One last thing to try. Remember that this program can accept parameters on the command line. There are two ways to get that kind of information to nemiver. Try Clicking on “Load Executable…” in the File menu. A dialog box will pop up that allows you to select the executable program that you want to debug (it should already be indicating your findPrimes executable) and the arguments to supply to it. Type an integer into the Arguments: box and click on Execute. Your program should restart. If you click Continue you will see that, this time, it goes straight into findPrimes (pausing at your breakpoint) without prompting for a number in the Target Terminal.

    Now exit from nemiver.

    You can give command arguments to your program when launching nemiver. Do

    nemiver ./findPrimes 64
    

    Notice that nemiver does not remember the breakpoint that you had set in your prior session. Click on Continue and you can observe that your program does indeed make use of the command line argument “64”.

  11. Exit from nemiver.

  12. Give the command

    make clean

3 The Eclipse Debugger

Eclipse includes a debugging interface that makes many debugging tasks simple.

Example 2: Try This: Eclipse Debugger
  1. Launch the Eclipse IDE. You will probably find that your former Eclipse project is already open and waiting for you. Eclipse tends to keep projects open until you close them. (If you leave many projects open, Eclipse will take a long time to start up because it begins by recompiling all open projects.)

  2. Look at the file findPrimes.cpp. Near the bottom, you will find two statements that look like this:

    // bool* theSieve = NULL; // error
    bool* theSieve = new bool[maxNum]; // correct
    

    Comment out the second statement and remove the comment markers from the left of the first statement. We are again deliberately injecting a bug into the program for the purpose of illustration. Compile the resulting program.

  3. Via the Run menu, check the Run Configurations… You should find that there is an existing entry for a C++ application named “findPrimes”. Select it and check the Arguments. It probably recalls the integer you entered there earlier. If not, put a small integer, say, 50, in there. Click on Run and watch the program crash.

    The crash is actually quieter than with some other debuggers. It may simply stop running with no output at all.

  4. Next, run the program in the debugger. From the Run menu, select “Debug” or click the insect button. If you are asked about launchers, select “Using GDB…Create Process”. The program starts running, but pauses at the first statement in main. Above the source code, you can see the call stack on the left and the local variables and parameters on the right. Click the “Step Over” button to move forward one statement. Note that maxNum changes value and is highlighted in the data display to show you what has changed. Clicking on any of the variables in the data display will cause more detailed info about it to appear at the bottom of that pane.

  5. Click on the Resume button. The program runs until the crash. In the call stack area, you can see that it says that execution has been “suspended” because the program received a “signal”. That “signal” is the segmentation fault that we have seen before.

    Click on the various entries in the call stack to see the editor window change to the call locations. Note that the data display changes to show the variables in each function that you have selected.

  6. Click the Terminate button (the red square) to stop the execution of our program.

    Use the editor to repair the bug that we injected, and recompile. Run the program in the normal fashion, and observe that it completes correctly.

  7. Find the body of the findPrimes function, and locate the line that writes to cout. Set a breakpoint at this line by double-clicking in the left margin alongside that line. Click the insect/bug button to start the debugger. You should stop at the beginning of main(). Click Resume, and execution should stop at your new breakpoint.

  8. Looking at the code just in front of your breakpoint, you can see what the value of message is supposed to be. Check its value in the data display.

  9. Remove the breakpoint from this statement by double-clicking on the small blue breakpoint marker. “Click the ”Step return" button to finish execution of this function, stopping as soon as we return to the caller. Take note of how the Watches and Call Stack windows are updated.

  10. Exit from Eclipse.

4 The VSCode Debugger

Hearkening back to remote development, we can run the debugger remotely, displaying the results in a copy of the VSCode IDE running on our local PC. Unlike the other approaches covered in this lesson, we don’t need to be running X for this mode of debugging.

4.1 Launch Tasks

Before you can use VSCode to debug a C++ program, you need to configure a “launch task”.

  1. In the Explorer column, select one of your .cpp files.

  2. From the Run menu, select Add Configuration....

    This will create a new file in your project, .vscode/launch.json. Oddly, it won’t actually have a configuration. (I suspect this is a bug – it may change behavior in the near future.)

  3. Again, In the Explorer column, select one of your .cpp files, and then, from the Run menu, select Add Configuration....

    Then select C/C++ (gdb) Launch.

    This will add a new configuration to your .vscode/launch.json file.

  4. Click on .vscode/launch.json in the file listing to open it in the editor.

  5. In its current setup, the launch task tries to run a program named after whatever .cpp file you are editing and with no command-line parameters. You will often (usually?) want to change that.

    1. Edit the "name" line to make it a bit more descriptive. I like to put the name of the program being run at the end of the name. (Sometimes I will also add an indication of what input data I am using.) This helps if you wind up keeping more than one debug configuration.

    2. Edit the "program" line, changing the value to "${fileDirname}/myProgram" where “myProgram” is replaced by whatever name your compiled executable program uses.

    3. Optional, but recommended: Add a pre-launch instruction line after the "program" line to tell VSCode to recompile any changed code before launching the debugger:

      "preLaunchTask": "C/C++: make -k",
      

      replacing “C/C++: make -k” by the name of your default build task.

    4. If your program expects command-line parameters, edit the "args": line to provide these, with each parameter in quotation marks and separated by commas.

    Here is an example of an edited configuration for an executable named “myProgram” that expects to take the name of a file of test data (in this case, test0.in) and an integer as command-line parameters.

    {
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "(gdb) Launch myProgram",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}/myProgram",
            "preLaunchTask": "C/C++: make -k",
            "args": ["test0.in", "100"],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                },
                {
                    "description":  "Set Disassembly Flavor to Intel",
                    "text": "-gdb-set disassembly-flavor intel",
                    "ignoreFailures": true
                }
            ]
        }
    
    ]
    }
    

    Save your changes to that file.

    You can add additional debug configurations by repeating the above steps. For example, you could have different configurations to debug your program using different sets of input data by changing the "args:" line in each one.

  6. You can now launch the debugger

    • with the F5 key
    • or the menu item Run -> Start Debugging
    • or by clicking the “Run and debug” icon on the left and selecting the appropriate launcher from the menu next to the small green “run” triangle at the top of the left column.
  7. If your project generates more than one executable, you can repeat steps 1..3 above to add additional configurations to launch the other executables.

    If you want to debug your program with different command-line parameters, you can edit the launch.json file and change the "args" line accordingly.

    • If you want to hop back and forth between the same set of command line parameters, you can also repeat steps 1..3 to get a new configuration with different 'args". Just give each one a distinct "name so that you can remember which is which.

4.2 Try It Out

Example 3: Try This: VSCode Debugger
  1. On your local PC, launch VSCode and connect to linux.cs.odu.edu.

  2. Open the folder ~/playing/vscode/sieve. You should find your project still waiting for you as you left it.

    Compile the project.

  3. Follow the instructions above to create a launch task that will run the program findPrimes in the debugger with command argument “50”.

  4. Look at the file findPrimes.cpp. Near the bottom, you will find two statements that look like this:

    // bool* theSieve = NULL; // error
    bool* theSieve = new bool[maxNum]; // correct
    

    Comment out the second statement and remove the comment markers from the left of the first statement. We are again deliberately injecting a bug into the program for the purpose of illustration. Compile the resulting program.

  5. Go to the Terminal pane. Type any character to dismiss the compilation output. If the terminal disappears entirely, go to the Terminal menu and select New Terminal.

    In that terminal, run the program:

    ./findPrimes 50
    

    It should crash.

  6. Next, run the program in the debugger. From the Run menu, select “Start Debugging” or use the F5 key.

    It should be clear that the program starts to run, but crashes.

  7. Click on the Debug icon in the left column.

    In the VARIABLES pane you can see the values of the program variables at the moment of the crash.

    In the CALL STACK pane, you can see the sequence of function calls that brought you to the point of the crash. Try clicking on different entries in there and observe how both the source code display and the VARIABLES list changes.

  8. Near the top center of the window are the debugger controls . Hover your mouse over each of them to see what they do.

    Use the Stop button to close the current debugger session.

  9. Use the editor to repair the bug that we injected, and recompile. Run the program in the normal fashion, and observe that it completes correctly.

  10. Find the body of the findPrimes function, and locate the line that writes to cout. Set a breakpoint at this line by clicking in the margin the left of the line number.

    Run the debugger again. Execution should pause at your new breakpoint.

  11. Looking at the code just in front of your breakpoint, you can see what the value of message is supposed to be. Check its value in the VARIABLES display.

  12. Remove the breakpoint from this statement by clicking on the small red breakpoint marker. “Click the ”Step Out" button to finish execution of this function, stopping as soon as we return to the caller. Take note of how the VARIABLES and CALL STACK panes are updated.

  13. Use the Stop button to shut down the debugger.

  14. Exit from VSCode.