15

So, the problem:

When I run the same C++ code in Visual Studio, with the same input and parameters, I get either the correct output, or an output that is completely messed up (99% of values go to zero).

Now, the code is some very complex, hard to read legacy code that is used for over 20 years and developed by many different people, containing 100k lines of code. I also made some minor changes for one project, that probably caused this unstable behaviour. Not even sure that it's induced by me, but I guess it must be the case. I was thinking of memory issues, maybe because I assigned more elements to some variable that has not that many elements allocated in memory. This normally gives an error, but... . Or might be something else low-level/mid-level, but not sure what.

enter image description hereenter image description here

The problem here is, I have 3 options:

  1. roll back to a stable version and introduce my changes one by one and test again
  2. find the root cause in some systematic manner
  3. Guess and be lucky that I find an error that I'm not even sure what is might look like

The problem, respectively:

  1. Sometimes the program works well for 20 runs at a time. So, it's gonna take forever to test whilst adding each change one by one and testing each new change... .
  2. I have no idea how to systematically tackle these kind of issues (I'm not really a programmer, I just have a lot of experience with high level programming like MATLAB, R etc.)
  3. Luck might not be on my side.

Anyone has any idea how to help with option 2? Like, what kind of issues could possible cause behaviour like that? How can I tackle this, e.g. if it does not work during a run, does it make sense to dig into the code (even thought next run it might work perfectly), should I look at warnings, errors in the output window, etc. ?

*I also have this message in Output-->General:

Unable to read data from the transport connection: An established connection was aborted by the software in your host machine.*

I know you don't have all info, but I cannot share my 100k lines of code program here, not,if I would, would you be able to understand much of it without hours of digging. So it's a general question.

Update: when compiling/building the .exe and running, the wrong output does not seem to happen.

Cheers, thanks!

UPDATE: SOLVED, and thanks for the advice!

So, how did I solve it:

  • I automated generating lot's of output
  • I rolled back until I could find a version that was stable

I already had some things in mind that could cause the error, and this matched with the changes made since the latest stable version. I then deactivated some usual suspects, and the output became stable again. The line I deactivated corrected some wrong input, but only for the product I was working on. I still didn't know exactly why, because the part of the code I deactivated was not directly related to the actual line of error. But somehow it "solved it" (as in, 2 bugs (wrong input for the new product, and one out of bounds memory bug I had created through recent code changes) did actually compensate for each other). But I was close and then I found that from the product I was working on j.m_res_inv[i] went out of bounds, so I created an if to subtract one from the maximum index.

 if (b->aidnprdl == FIB_VARIABLE || b->aidnprdl == AGI_VARIABLE)
 {
     for (int i = 0; i < b->nbre_sc - 1; i++) // don't assign random memory values for a subcontract that does not exist yet ;) and make b->nbre_sc equal the number of actual branche21 subcontracts in the input data
     {
         b->m_res_inv[i] = j.m_res_inv[i];
         b->res_inv_br21 += j.m_res_inv[i];
     }
 }
 else
 {
     for (int i = 0; i < b->nbre_sc; i++) // original code: b->nbre_sc equals number of branche21 subcontracts
     {
         b->m_res_inv[i] = j.m_res_inv[i];
         b->res_inv_br21 += j.m_res_inv[i];
     }
 }

What I still don't understand:

  • Why does C/C++ not crash when this happens? The random memory location is probably not containing the same variable type (double, int) as needed by j.m_res_inv[i] .
  • Even if it works, why does it not put just a random value there?
  • Strange thing is, when unstable, every output value (like, an .csv of 2000 rows, 300 columns) would go to zero. This probably depends on particularities of this program but still I would expect one "weird" value not to corrupt each and every computation that in theory, are unrelated to this value (mathematically). Maybe there is some particularity in the program that when somewhere a negative value is encountered it stops computing. Kinda hard to find out was was happening I find.

Anyhow, thanks a lot for all the good advice!

p.s. I marked one answer as definitive, but it's the combination of answers that was useful for me!

5
  • 3
    Both C and C++ are low-level languages. What you did is called Undefined Behavior in both, and in both languages anything can happen. Crashing is just one of the possible outcomes. So is wiping your disk.
    – MSalters
    Commented Aug 15 at 10:06
  • To say the same thing as MSalters a little differently: you have only ever programmed in managed languages. In most programming languages there is a runtime system that will manage memory for you and crash your program when you make a mistake like trying to use a value of the wrong type. Neither C nor C++ has this on offer. The compiler is allowed to assume things like integer overflow never happens, all array accesses are within the bounds of the array, etc. and the type information is erased during compilation. Once something "unexpected" like that happens all bets are off. Commented Aug 15 at 11:07
  • 3
    Your "solution" reads like a horrible maintenance bomb waiting to be tripped someday. You're hiding the fact that when b->aidnprdl is one of FIB_VARIABLE or AGI_VARIABLE, the number of items in b->m_res_inv (or j.m_res_inv?) is one less than b->nbre_sc, which otherwise seems to be the number of items, deep within the code. Who is supposed to remember this? Commented Aug 15 at 13:25
  • 1
    This code is a perfect example of an unnecessary DRY violation. Why the heck did you copy-paste the loop? Why not something along the lines of int upperbound = b->nbre_sc;, then if (b->aidnprdl == FIB_VARIABLE || b->aidnprdl == AGI_VARIABLE) upperbound-- and finally for (int i = 0; i < upperbound; i++) ? Or even better: why is m_res_inv not a std::vector, so it knows it's correct size?
    – Doc Brown
    Commented Aug 16 at 11:54
  • std::vector is never used in the code, I guess because it was written in C 20 years ago by a non-programmer I concur with Mosner that it's pretty confusing, use of upperbound can make it more clear. Also, use of b->nbre_sc as the number of items, also in the AGI_VARIABLE case is probably not ideal, but avoids having to alter the code in many places
    – babipsylon
    Commented Aug 19 at 8:39

6 Answers 6

33

Sometimes the program works well for 20 runs at a time. So, it's gonna take forever to test whilst adding each change one by one and testing each new change...

First, you need to work on this. Find a scenario which gives you a much higher probability of crashing the program:

  • maybe by finding more critical test data,

  • maybe by adding asserts or validations in sections where the wrong behaviour shows up and can be identified programmatically

  • as a last resort, if you know one from 20 executions of the program shows a crash, automate the repeated execution / restart of the program up to, lets say, 100 times

Since we do not have any idea what your program does exactly, there is no way to give you more specific advice, but reproduceability is always mandatory to find the root cause of the bug - there is no shortcut around this. Without a reproducible scenario, one will not be able to verify a program change has really fixed the bug sucessfully, regardless which method one uses to identify a possible solution.

  • roll back to a stable version and introduce my changes one by one and test again
  • find the root cause in some systematic manner

Rolling back to a stable version, then introducing changes one-by-one (and re-run the test scenario from above, of course) is a very systematic manner to find the root cause of a bug. In case all of the changes were added as individual commits in a Git repo, one may be able to speed this process up by utilizing git-bisect. But even if that is not the case, changing one step at a time and then running a test which reliably shows the presence or absence of a certain bug is definitely the most effective way to chase this kind of issue.

3
  • 5
    Thanks! Luckily it's fairly reproducible, and I automated it as Martin suggested. I the bug was not there before, I will find it yes by rolling back and reintroducing. There must definitely be some interaction between what I changed and the existing code, that is, if I didn't just introduce the bug completely by myself also, so, won't harm.
    – babipsylon
    Commented Aug 13 at 13:38
  • If you don't or can't use git-bisect, you can still apply the binary search concept manually. Don't apply (or revert) one commit at a time, instead check out the middle commit between known good and known bad commits, and repeat until you find the commit at fault. That can save a massive amount of time if there are lots of commits to search through. Commented Aug 14 at 17:34
  • @CrisLuengo: your comment assumes the OP has commited each invidual change. I wrote about a scenario where one made several changes without committing in between. Of course, one can still try to apply "half of all changes" first, then apply or revert the half of this.
    – Doc Brown
    Commented Aug 14 at 21:22
34

Cherish this bug. Do nothing to harm it. There is no reason to suppose it is new. It could well be a bug which has been in the software all along, and which has now been revealed as a result of your changes. Now that the bug is visible, you have a chance of finding the root cause.

  1. Roll back to a stable version - Definitely not. The bug may already have been there.

  2. Find the bug - yes, this is what you must do.

  3. Accidentally make a change which seems to make it go away - no, on no account do this. To take an example: if the crash comes because something is receiving a null pointer when it shouldn't, do not remove the crash by testing for a null value.

The problem you have is with the 19 times out of 20 that the bug does not happen.

First, automate

Arrange a "test harness" which runs the program N times and counts how many times it crashes. For instance, if your approximate figure is right, N=100 should report 5 crashes.

For all your future work, choose a value of N which will reliably give at least one crash.

Next, log

Create a log file. Insert, at the start of one or two likely-looking functions, a call to write a message to the log file, giving file or function name or line number, plus perhaps an indication of an argument or two.

Your test will now yield N log files, some of them associated with a crash and some not.

Check that your N=100 (or whatever) still does bring the expected number of crashes. This is important because many of the possible causes of a crash will be to do with uninitialised memory, and any change at all might accidentally stop the bug from happening.

Beyond and before

Are all N logs identical? If there were no bug, they ought to be.

If they are in fact identical, even as between the crashing and non-crashing runs, then you have not logged enough. Log more, especially close to where you think the crash may be happening.

Remember that storage is cheap and comparison tools are fast. There is no harm in adding and adding log calls until each log file is 100MB in size or even more.

Once you have got near the "sore spot", you will probably find that there are three cases:

  1. Runs normally with a normal log output.
  2. Produces an abnormal log output but does not crash.
  3. Produces an abnormal log output and crashes.

The point here is that 2 and 3 combined will probably be much more frequent than 3 alone. (An example is when a byte is uninitialised and can have any random left-over value, and a value of 0 causes the crash.)

You do have to make a judgement as between "normal" and "abnormal". You might take, as a working hypothesis, that if many logs are identical and only a few are different, the majority are the "normal" ones.

Work backwards

Your strategy now reverses itself. Earlier on, you were working forwards in time, closer to the crash, to find a time when the logs disagreed. Now you go backwards in time, starting from the first moment when the logs disagree and going back earlier and earlier to find where the disagreement started.

Do not correct!

The biggest temptation is this: you find a point where something is obviously wrong (eg. is zero when it shouldn't be) and you modify the program so that it avoids a crash in that case. On no account do this.

You may, if you wish, put some sort of "if zero, then return" self-protection here, but you must turn it off for now. You need to find the cause, not cure the symptom. I usually put the self-protection in but turn it off with an #ifdef. Once you have found the real cause, you can turn this protective code back on. But not until then.

6
  • 4
    This is really good advice. I might combine it with rolling back now and then, as the point where I introduced some code that might trigger the (maybe pre-existing) bug, might give good info into finding the actual bug itself I think.
    – babipsylon
    Commented Aug 13 at 12:49
  • There must definitely be some interaction between what I changed and the existing code, that is, if I didn't just introduce the bug completely by myself.
    – babipsylon
    Commented Aug 13 at 13:17
  • 7
    @MartinKochanski I suggest using valgrind. It finds invalid memory access, will point you to what line of code/the stack trace lead to that invalid memory access. It's a very powerful opensource tool. And will take very little time to run, but might save hours of debugging valgrind.org/docs/manual/quick-start.html...
    – Questor
    Commented Aug 13 at 17:57
  • 2
    And I just remembered that OP is using windows. Us Dr. Memory instead, it is not as good. But it's your only option unless you can build your project in Linux instead.
    – Questor
    Commented Aug 13 at 18:03
  • 4
    It's not the only option. Address Sanitizer quite good and comes with Visual Studio.
    – ojs
    Commented Aug 13 at 19:06
16

Update: when compiling/building the .exe and running, the wrong output does not seem to happen.

That is a major clue. When you're debugging, you're almost certainly debugging a debug build.

Debug builds link in the debug run-time libraries - one of the features of Window's debug run-time libraries is they overwrite memory when it's freed. So code that works in a release build because the freed memory it accesses still contains the expected data will fail in a debug build because the freed memory was overwritten with garbage.

As mentioned in some comments, you can use Dr Memory or Visual Studio's Address Sanitizer to find memory access errors like that.

I strongly suspect you'll find a very large number of problems. Every one found is a bug that has been latent ever since the code was written. And every single one must be fixed.

3
  • 3
    Yes I'm surprised none of the other answers are suggesting address sanitiser or similar (such as Valgrind on Linux or Dr Memory on Windows); if there's a memory issue that will immediately tell you the problem and exactly where it's happening, which is going to take a lot less time than trying to painstakingly debug the issue without using such a tool. Commented Aug 14 at 12:14
  • 2
    I have the feeling many of the people who answered don't have actual experience working with recent versions of Visual Studio and the rest reject the idea as "specific debugging help".
    – ojs
    Commented Aug 14 at 13:13
  • @ojs "Use these tools" is specific? Oooh-kay. Commented Aug 14 at 15:02
8

There's already a lot of good advice here in answers and comments, a final answer might be a meld of different things. Since you added that the error condition happens only when debugging and not when you build and run, it sounds like brute-force approaches to reliably reproduce the error may not work. I see you're going to try, so good luck, that's valuable experience to gain.

If you come up empty-handed though here's another idea. I'd look at the differences between what happens when you build and debug versus when you build and run. You mentioned the error condition leads to most values getting clobbered down to 0. So as an example of what I'm talking about, the first thing I'd probably do is look at VS's 'security checks', specifically the /sdl- switch (Project Properties | C/C++ | General). One thing it does when on is pre-initialize memory before use. If it's currently off in your project config then I'd see how things behave with it on, and vice-versa. If that doesn't increase reproducibility I'd look at similar differences between run and debug configurations. One difference I'd look at sooner than later is what DimitrijeCiric mentioned--possible timing issues (a debug run may introduce a slowdown that sometimes reveals the timing issue), and his advice sounds solid to me.

Also, I'm not sure that reverting your changes is a bad thing, I think you could treat it as another stimulus in isolation to see how it changes system behavior. Especially if you're nervous about your changes you can revert and see how things look, it won't bite. It doesn't sound like you have enough insight into the bug to have it obscured by more variability at this point.

Congrats by the way. You were a self-proclaimed "not a programmer", but if you weren't one before I'd argue you're discovering the one in you now. This is the typical intro.

1
  • 1
    Hi! Thanks for the encouraging words! I will take note for my next memory leak issue :). Luckily the bug had a high probability of occurrence, so I could easily test versions. In the end I solved it mostly just by thinking about the changes I made (a lot, I should commit after every feature :) ) and then deactivating them and go over each related line of code and think about if it made sense or would go out of bounds or not. I was a bit lucky maybe, but I was surprised I found it so quickly :).
    – babipsylon
    Commented Aug 14 at 14:15
4

One key advice for dealing with such problems has not been mentioned yet: Run the code using the valgrind memory checker if possible!

The point is, the C famliy of languages, along with some other languages, are not memory safe: While they do define what valid memory accesses are, they do not include any checks or safety nets to save the day. They trust you, the programmer. Absolutely. They say: You are the human, you are the master, you are more knowledgeable than I, you tell me what to do and I'll do it. And they follow through with that. You access an array out-of-bounds, they do it. You use a free'd object, they do it. You use a variable before you've assigned a value to it, they do it. You are in power, and it's you that is to blame for anything that goes wrong. And if you do screw up, an immediate crash is actually the best possible outcome that can happen. Much worse are unpredictable garbage results like you experienced, or even just subtly wrong results that can't even tell it's wrong. The upside is that these languages do not waste their time with pointless bounds checking...

Now valgrind is a tool that runs any code on a virtual CPU that does perform all the memory checks that are still possible on an already compiled C/C++/... program, and generates appropriate error messages for them. And it can check a lot. The expectation is, that for any memory corrupting code, valgrind is likely to point you directly to where the first memory corruption is happening, and thus dramatically shorten the time it takes you for moving from random observations to deterministic causes.

However, valgrind is not perfect. It will not find every kind of memory corruption. And conversely, even more anoyingly, it will also find shady code in linked libraries that happens to work fine anyways. For some codes, this may render valgrind pretty much useless. And since it runs the code on a simulated CPU instead of the real one, it incurs a hefty slowdown of something like 50x-ish. Nevertheless, it's the first thing you should think about when you are confronted with memory corruption: Is it possible to run this code under valgrind?

3
  • How do you use Valgrind with Windows and Visual Studio? Regarding "not mentioned before", softwareengineering.stackexchange.com/a/454567/375157 and comments to softwareengineering.stackexchange.com/a/454558/375157 might be of interest.
    – ojs
    Commented Aug 16 at 11:54
  • @ojs Indeed, I overlooked Andrew's answer. Probably because I'm not familiar with closed source software. And because I was looking for an answer that first gave the advise to do a memchecker run on the misbehaving executable. I think that there's little value in guessing at anything when debugging memory corruption, I want to get some real info on where the undefined behavior actually starts first. Andrew's point about not debugging a debugging build is good as well, though. Commented Aug 16 at 13:24
  • Thx, will look into it (on the next memory issue ;p ) .
    – babipsylon
    Commented Aug 19 at 8:34
0

For me, a legacy system is a system that has no tests. So, in the long run, to reduce a change of a bug, implement testing at various level: unit, functional, integration tests.

For a quick fix, there are few options:

Not the answer you're looking for? Browse other questions tagged or ask your own question.