tags:

views:

339

answers:

14

Seldom during working on large scale projects, suddenly you are moved on to a project which is already in maintainance phase.You end up with having a huge code C/C++ code base on your hands, with not much doccumentation about the design.The last person who could give you some knowledge transfer about the code has left the company already and to add to your horrors there is not enough time to get acquainted with the code and develop an understanding of the overall module/s.In this scenario when you are expected to fix bugs(core dumps,functionality,performance problems etc) on the module/s what is the approach that you will take?

So the question is: What are your usual steps for debugging a not so familiar C/C++ code base when trying to fix a bug?

EDIT: Enviornment is Linux, but code is ported on Windows too so suggestions for both will be helpful.

+16  A: 

If possible, step through it from main() to the problematic area, and follow the execution path. Along the way you'll get a good idea of how the different parts play together.

It could also be helpful to use a static code analysis tool, like CppDepends or even Doxygen, to figure out the relations between modules and be able to view them graphically.

Assaf Lavie
+1 Doxygen has helped me much to understand the codebase.
bdhar
+8  A: 

Use a pen and paper, or images/graphs/charts in general, to figure out which parts belong where and draw some arrows and so on.

This helps you build and see the image that will then be refined in your mind as you become more comfortable with it.

I used a similar approach attacking a hellish system that had 10 singletons all #including each other. I had to redraw it a few times in order to fit everything, but seeing it in front of you helps.

It might also be useful to use Graphviz when constructing dependency graphs. That way you only have to list everything (in a text file) and then the tool will draw the (often unsightly) picture. (This is what I did for the #include dependencies in above syste,)

Marcus Lindblom
+1  A: 

You have to have a fully reliable IDE which has a lot of debbugging tools (breakpoints, watches, and the like). The best way to familiarize yourself with a huge code is to play around with it and see how data is passed from one method to another. Also, you can reverse engineer the code so could see the relationship of the classes. :D Good Luck!

Owen
+5  A: 

Some pointers:

  1. Debug from the part which seems more relevant to the workflow.
  2. Use debug strings
  3. Get appropriate .pdb and attach the core dump in debuggers like Windbg or debugdiag to analyze it.
  4. Get a person's help in your organization who is good at debugging. Even if he is new to your codebase, he could be very helpful. I had prior experience. They would give you valuable pointers.
  5. Per Assaf Lavie's advice, you could use static code analyzers.
  6. The most important thing: as you explore and debug, document everything as you progress. At least the person succeeding you would suffer less.
bdhar
+1 for documentation. Very important. Though I don't do it much, :-) always appreciate the people who do it.
Manoj R
+1  A: 
while (!codeUnderstood)
{
  Breakpoints();
  Run();
  StepInto();
  if(needed)
  { 
   StepOver();
  }
}
liaK
I think you mean `while (!codeUnderstood)`
Space_C0wb0y
@ Space_C0wb0y, Yeah.. Thanks.. :)
liaK
+1  A: 

For me, there is only one way to get to know a process - Interaction. Identify the interfaces of the process/system. Then identify the input/output relationship (these steps maybe not linear). Once you do that, you can start tinkering at the code with a fair amount of confidence because you know what it is "supposed to do" then it's just a matter of finding out "how it is actually being done". For me though, getting to know the interface (Not necessarily the user interface) of the system is the key. To put it bluntly - Never touch the code first!!!

nakiya
+3  A: 

Three things i don't see yet:

  1. write some unit tests which use the libraries/interfaces. demonstrate/verify your understanding of them and promote their maintainability.

  2. sometimes it is nice to create an special assertion macro to check that the other engineer's assumptions are in line with yours. you could:

    1. not commit their uses
    2. commit their uses, converting them to 'real' assertions after a given period
    3. commit their uses, allowing another engineer (more familiar with the project) to dispose or promote them to real assertions
  3. refactoring can also help. code that is difficult to read is an indication.

Justin
But those big, hairy ugly code bases often have these three counter properties: 1) Interdependent, intertwined code not suitable to write unit tests for. 2) Can only be started in a production environment, there is no testbed or similar, and to create one is a huge undertaking in itself. That means assertion macros will run on a production system = suicidal or with unknown side effects. 3) Refactoring under these circumstances is dangerous, because refactoring successfully implies you have a clue how the system works.
Amigable Clark Kant
@Amigable Clark Kant 1) yes - you won't always have all the above options, but you may have some of them some of the time. 2) refactoring isn't supposed to change the behavior. and you'd typically approach refactoring by the module, not the system. it's still a vaild means to understand and improve the codebase in many cases (agreed, defects can be one result of refactoring).
Justin
@Justin, all your points are good, for systems that don't totally stink. However, I have seen substantial code bases where there really are no "modules" to refactor. Just a _lot_ of spaghetti.
Amigable Clark Kant
+2  A: 

The first step should be try to read the code. Try to see the code where the bug is. Follow the code from main to that point ans try to see what could be wrong. Read the comments from the code(if any). Normally the function names are useful. Understand what each function does.
Once you get some idea of the code then you can start debugging the code. Put breakpoints where you don't understand the code or where you think the error can be. Start following the code line by line. Debugging is like sex. Initially painful, but slowly you start to enjoy it.

Manoj R
@ManojR: loved the Debugging is like sex. Initially painful, but slowly you start to enjoy it" comment, just sex is always desirable and debugging not always ;)
Als
+1  A: 

Not sure about C/C++, but coming from Java and C#, unit testing will help. In Java there's JUnit and TestNG libraries for unit testing, in C# there's NUnit and mstest. Not sure about C/C++.

Read the book 'Refactoring: Improving the Design of Existing Code' by Martin Fowler, Kent Beck, et al. Will be quite a few tips in there I'm sure that will help, and give you some guidance to improving the code.

One tip: if it aint broke, don't fix it. Don't bother trying to fix some library or really complicated function if it works. Focus on parts where there's bugs.

Write a unit test to reproduce the scenario where the code should work. The test will fail at first. Fix the code until the unit test passes successfully. Repeat :)

Once a majority of your code, the important bits that are too complex to manually debug and fix, is under automated unit tests, you'll have a safety harness of regression tests that'll make you feel more confident at changing the existing code base.

jamiebarrow
+1  A: 

I don't try to get an overview of the whole system as suggested by many here. If there is something which needs fixing I learn the smallest part of the code I can to fix the bug. The next time there is an issue I'm a little more familiar and a little less daunted and I learn a little more. Eventually I'm able to support the whole shebang.

If management suggests I do a major change to something I'm not familiar with I make sure they understand the time scales and if things a really messy suggest a rewrite.

Patrick
+1  A: 

Usually the program in question will produce some kind of output ( log, console printout, dialog box ).

  1. Find the closest place to your problem in the program output
  2. Search through the code base and look for the text in that output
  3. Start putting your own printouts, nothing fancy, just printf( "Calling xxx\n" );, so you can pinpoint exactly to the point where the problem starts.
  4. Once you pinpointed the problem spot, put a breakpoint
  5. When you hit the breakpoint, print a stacktrace

Now you can see what players you have and start the analysis of how you've got to the wrong place.

Hopefully the names of the methods on the call stack are more meaningful than a, b and c ( seen this ), and there is some sort of comments, method documentation more meaningful than calling a ( seen this many times ).

If the source is poorly documented, don't be afraid to leave your comments once you have figured out what's going on. If program design permits it create a unit test for the problem you've fixed.

Alexander Pogrebnyak
+4  A: 

As others have already suggested, writing unit-tests is a great way to get into the codebase. There are a number of advantages to this approach:

  1. It allows you to test your assumptions about how the code works. Adding a passing test proves that your assumptions about that small piece of code that you are testing are correct. The more passing tests you write, the better you understand the code.

  2. A failing unit test that reproduces the bug you want to fix will pass when you fix the bug and you know that you have succeeded.

  3. The unit tests that you write act as documentation for the future.

  4. The unit tests you write act as regression tests as more bugs are fixed.

Of course adding unit tests to legacy code is not always an easy task. Happily, a gentleman by the name of Michael Feathers has written an excellent book on the subject, which includes some great 'recipes' on adding tests to code bases without unit tests.

WELC

Johnsyweb
A truly excellent book that one.
Michael Anderson
+2  A: 

cscope + ctags are available on both Linux and Windows (via Cygwin). If you give them a chance, these tools will become indispensable to you. Although, IDEs like Visual Studio also do an excellent job with code browsing facilities as well.

In a situation like yours, because of time constraints, you are driven by symptoms. I mean that you don't have time to reconstruct the big picture / design / architecture. So you focus on the symptoms and work outwards, and each time reconstruct as much of the big picture as you need for that particular problem. But do not make "local" decisions in a hurry. Have the patience to see as much of the big picture as needed to make a good quality decision. And don't get caught in the band-aid syndrome i.e. put any old fix in that will work. It is your job to preserve the underlying architecture / design (if there is one, and to whatever extent that you can discover it).

It will be a struggle at first, as your mind "hunts" excessively. But soon the main themes in the design / architecture will emerge, and all of it will start to make sense. Think, by not thinking, grasshoppa :)

Ziffusion
+1  A: 

Thanks for the nice answers, quite a number of points to take up. I have worked on such situation a number of times and here is the usual procedure i follow:

  1. Check the crash log or trace log. Check relevant trace if just a simple developer mistake if cannot evaluate in one go, then move on to 2.
  2. Reproduce the bug! This is the most important thing to do. Some bugs are rare to occur and if you get to reproduce the bug nothing like it. It means you have a better % of cracking it.
  3. If you cant reproduce a bug, find a alternative use case, situation where in you can actually reproduce the bug. Being able to actually debug a scenario is much more useful than just the crash log.
  4. Head to version control! Check if the same buggy behavior exists on previous few SW versions. If NOT..Voila! You can find between what two versions the bug got introduced and You can easily get the code difference of the two versions and target the relevant area.(Sometimes it is not the newly added code which has the bug but it exposes some old leftovers.Well, We atleast have a start I would say!)
  5. Enable the debug traces. Run the use case of the bug, check if you can find some additional information useful for investigation.
  6. Get hold of the relevant code area through the trace log. Check out there for some code introducing the bug.
  7. Put some breakpoints in the relevant code. Study the flow. Check the data flows.Lookout for pointers(usual culprits). Repeat till you get a hold of the flow.
  8. If you have a SW version which does not reproduce the bug, compare what is different in the flows. Ask yourself, Whats the difference?
  9. Still no Luck!- Arghh...My tricks have exhausted..Need to head the old way. Understand the code..and understand the code and understand it till you know what is happening in the code when that particular use case is being executed.
  10. With newly developed understanding try debugging the code and sure the solution is around the corner.
  11. Most important - Document the understanding you have developed about the module/s. Even small knitty gritty things. It is sure going to help you or someone just like you, someday..sometime!
Als