Get email delivery of the Cadence blog featured here
This blog starts with a description of a debugging session of a mysterious behavior we encountered. Unlike a good mystery book, I will tell you upfront who did it—deep_copy(). In the second part of the blog, we’ll recap e’s copying methods, and we’ll understand how to avoid such mischief.
The mystery started when we saw that, in one verification environment, a struct that was expected to perform a check after every interrupt performs this check suspiciously too often.
To get more information of what’s happening, we issued an echo event command on the event triggering the check, start_check. What we saw surprised us—it’s not that there is a struct performing many checks—we have many instances of this struct running in parallel. But we were sure we had instantiated this struct only once… where were all these instances coming from?
We probed the run using ida_probe, and decided to take a look at the env with Debug Analyzer. As you can see in the screenshot, there is one field of the type my_model under the env, as expected. So, what are all the other instances?
When we requested to view all instances of this type, we saw that a new struct of that type is created every few cycles.
Who created all these copies of my_model? And why? There is an easy way to find this out—we created a small file, extending in it the my_model struct:
The only reason we added this code, was to ensure my_model.init is implemented, so now we can set a break on it: “break my_model.init”. We ran the test again, and looking at the Callstack in the Source Browser:
Going up one level in Callstack, we can see who caused the creation of the my_model – a deep_copy().
To understand what happened here, let’s recall how copy() and deep_copy() work. When you copy a struct using copy(), the scalar fields are copied with simple assignments. As with “new_struct.field = original_struct.field”. Fields that are lists or structs are not duplicated, rather the reference is copied.
If we take this struct, for example:
Then coping a burst, the result will be a new burst struct, with the values of its address and legal fields identical to the original fields, and its transfer field is a reference to the same transfer sub-struct of the original struct.
Assume we now do some manipulation on the new struct, b2, like this:
This will result in:
If we want to avoid such a behavior, the new burst struct must have “its own copy of transfer”, so it can modify it without interfering with b1, the original struct. This is what deep_copy() is for. This routine performs a recursive copy—each field is copied by value.
Now we can modify any of b3 fields, including fields of b3.transfer—without affecting the original struct, b1. Going back to our “buggy environment”, with all the instances of the my_model—the burst struct is copied using deep_copy(), because the checking method modifies the transfer sub struct. But burst also has a field that is a reference to my_model—so with deep_copy, my_model is deep copied as well.
So? What can we do? We do not want to use copy(), because then altering the burst.transfer has undesired side effects. But using deep_copy() creates all these copies of my_model, which also has its side effects.
In such cases, deep_copy() is the approach. What do we do about it copying the my_model? Not problem—we can instruct it not to do so, using field attribute:
So now, most of the fields—including transfer—are deep copied, and for my_model field—the reference is copied.
As you can see, you can code what you want, getting deep copy, shallow copy, or a mixture. Just decide what behavior you want to achieve, the features are there.
Detailed information about copy(), deep_copy() and field attributes is available in CDNSHelp.