I have a very large C++ application running on a *nix environment, which is occasionally setting a particular date attribute on a structure, way off what it should be. I know which structure and which attribute is going wrong, but what I need to do is to capture the stack trace where the value is being set incorrectly.
My initial approach would be to create an instrumented build adding debug where the value is changed. If the attribute was private in a class, I could just manipulate the mutator to log where the issue was happening. Unfortunately, it's been implemented as a public attribute in a struct and there are literally hundreds of places where I would need to refactor to replace direct use with a mutator, or other instrumented code. It would take me days to do this as I don't have access to a refactoring tool that would simplify the approach. Indeed, as the code is just a temporary instrumented version, refactoring is just a means to an end.
I'm wondering whether GDB has some means to help me with this. Ideally, I'd like to:
- Run the code in the debugger
- Add watchpoints, but not on a specific variable but any instance of this structure, to catch when the attribute is changed.
- Ideally, add a criterion that means it only triggers if the value set is, say, less than a certain amount.
- Capture the stack trace and ideally some of the variable values at this point.
Is this even possible with GDB? If not, are there other alternative approaches that might speed this up dramatically?
-
1Is your test case deterministic enough that the dud value occurs in the same place every time? (you may need to disable ASLR and some other protections) - in which case the classic hardware watchpoint would work.pjc50– pjc502022年02月21日 13:08:03 +00:00Commented Feb 21, 2022 at 13:08
-
Yes, it is deterministic but only within the context of a very complex process, where static analysis has not proved fruitful. The problem with a classic watchpoint is that I'm observing many instances of the class in question being created, passed around, destroyed etc, so just providing a memory address isn't going to be enough. Given that gdb is really more suited to C than C++, so will presumably find detecting instantiation of instances tricky, let alone changes of member variables, I think this is going to be a big ask.Component 10– Component 102022年02月21日 15:12:15 +00:00Commented Feb 21, 2022 at 15:12
-
3I would not throw the idea of making the attribute private and get it behind a mutator too quickly over board. Maybe you can find a way for a global search/replace to accomplish this. maybe you can restrict the replacement of the public struct by a more encapusulated class to a smaller areas of the code.Doc Brown– Doc Brown2022年02月21日 15:28:07 +00:00Commented Feb 21, 2022 at 15:28
-
2Change the variable's type to something that blows up if assigned the "wrong" value and acts like the other type otherwise.Mat– Mat2022年02月22日 06:23:47 +00:00Commented Feb 22, 2022 at 6:23
-
"I don't have access to a refactoring tool that would simplify the approach." There are so many high quality and free tools out there, I highly doubt this constraint is fixedwhatsisname– whatsisname2022年02月22日 07:10:16 +00:00Commented Feb 22, 2022 at 7:10
2 Answers 2
If this is on x86_64 Linux (perhaps a big assumption), then you could use Undo's UDB to debug this. As UDB records the execution of the application while it runs, you would be able to set a breakpoint where you detect the bad value, and then run until the error is detected. When stopped at that breakpoint (and you know the memory address of the bad value), you can then set a watchpoint at the address of the bad data, and run the application backwards, until it hits that breakpoint. From there you can explore more than just the call stack.
There is a recent blog on undo.io which covers how to do something similar to this on SQLite with VSCode, although it will also work from the commandline UDB (based on GDB).
Disclaimer: I do work for Undo, but I genuinely believe the free trial version would likely be sufficient to help you with this. There is a performance impact for recording, but it could save a lot of time diagnosing your issue and lead you directly to the bug.
My initial approach would be to create an instrumented build adding debug where the value is changed. If the attribute was private in a class, I could just manipulate the mutator to log where the issue was happening. Unfortunately, it's been implemented as a public attribute in a struct and there are literally hundreds of places where I would need to refactor to replace direct use with a mutator, or other instrumented code.
One of the key features of C++'s type system is that you can write classes which behave syntactically the same as built-in types.
So, instead of moving the field behind a mutator, just replace the field with a class instance which has the appropriate assignment operator (this is your mutator). Add whatever other operators and constructors are necessary. If you do this correctly, no calling code needs to change at all, only to be recompiled.
You can see an example of a suitable assignment operator in the BoundedValue
validating type wrapper I wrote elsewhere:
template <typename T, int Tmin, int Tmax> class BoundedValue
{
T val_;
public:
enum { min_value=Tmin, max_value=Tmax };
// ...
// run-time checked assignment:
BoundedValue& operator= (T const& val) {
BOUNDED_VALUE_ASSERT(min_value, max_value, val); // your check here
val_ = val;
return *this;
}
};
-
This is a really interesting approach. I am just wondering how many operator overloads one will have to implement in such a type to make it behave, for example, almost like an
int
. Or maybe adding implicit type conversion to "int" will be sufficient?Doc Brown– Doc Brown2022年02月25日 09:06:37 +00:00Commented Feb 25, 2022 at 9:06 -
Depends entirely on how it's used - in the simplest case, an assignment operator and a cast operator are enough. Worst case, if the type is used explicitly in expressions (with type deduction for example), you could indeed need a load of operators.Useless– Useless2022年02月28日 10:12:14 +00:00Commented Feb 28, 2022 at 10:12
-
1Again, it depends how you use it, and you haven't shown that in your question.
wrapper.method()
won't work because you can't overloadoperator.
and the left-hand-side of anx.y()
expression doesn't participate in any implicit conversions. Converselyx + y
does participate in implicit conversion.Useless– Useless2022年02月28日 19:07:43 +00:00Commented Feb 28, 2022 at 19:07 -
1So, if you're explicitly calling methods directly on the object, your only option is to manually forward those methods. But, if the "value" is an object with methods and internal state, how was it getting mutated in the first place? Were you actually talking about mutating a public
value.inner_value
? Because then that inner value is the one you should be wrapping ...Useless– Useless2022年02月28日 19:09:56 +00:00Commented Feb 28, 2022 at 19:09 -
1tl;dr you really need to decide whether the attribute you care about is a single primitive or a user-defined type. Primitives don't have methods and all their build-in operations are overloadable. UDTs may have methods and can't be wrapped so easily/generically.Useless– Useless2022年02月28日 19:34:30 +00:00Commented Feb 28, 2022 at 19:34