views:

278

answers:

3

I used to think that in C99, even if the side-effects of functions f and g interfered, and although the expression f() + g() does not contain a sequence point, f and g would contain some, so the behavior would be unspecified: either f() would be called before g(), or g() before f().

I am no longer so sure. What if the compiler inlines the functions (which the compiler may decide to do even if the functions are not declared inline) and then reorders instructions? May one get a result different of the above two? In other words, is this undefined behavior?

This is not because I intend to write this kind of thing, this is to choose the best label for such a statement in a static analyzer.

+10  A: 

See Annex C for a list of sequence points. Function calls (the point between all arguments being evaluated and execution passing to the function) are sequence points. As you've said, it's unspecified which function gets called first, but each of the two functions will either see all the side effects of the other, or none at all.

R..
+5  A: 

The expression f() + g() contains a minimum of 4 sequence points; one before the call to f() (after all zero of its arguments are evaluated); one before the call to g() (after all zero of its arguments are evaluated); one as the call to f() returns; and one as the call to g() returns. Further, the two sequence points associated with f() occur either both before or both after the two sequence points associated with g(). What you cannot tell is which order the sequence points will occur in - whether the f-points occur before the g-points or vice versa.

Even if the compiler inlined the code, it has to obey the 'as if' rule - the code must behave the same as if the functions were not interleaved. That limits the scope for damage (assuming a non-buggy compiler).

So the sequence in which f() and g() are evaluated is unspecified. But everything else is pretty clean.

Jonathan Leffler
+1  A: 

@dmckee

Well, that won't fit inside a comment, but here is the thing:

First, you write a correct static analyzer. "Correct", in this context, means that it won't remain silent if there is anything dubious about the analyzed code, so at this stage you merrily conflate undefined and unspecified behaviors. They are both bad and unacceptable in critical code, and you warn, rightly, for both of them.

But you only want to warn once for one possible bug, and also you know that your analyzer will be judged in benchmarks in terms of "precision" and "recall" when compared to other, possibly not correct, analyzers, so you mustn't warn twice about one same problem... Be it a true or false alarm (you don't know which. you never know which, otherwise it would be too easy).

So you want to emit a single warning for

*p = x;
y = *p;

Because as soon as p is a valid pointer at the first statement, it can be assumed to be a valid pointer at the second statement. And not inferring this will lower your score on the precision metric.

So you teach your analyzer to assume that p is a valid pointer as soon as you have warned about it the first time in the above code, so that you don't warn about it the second time. More generally, you learn to ignore values (and execution paths) that correspond to something you have already warned about.

Then, you realize that not many people are writing critical code, so you make other, lightweight analyses for the rest of them, based on the results of the initial, correct analysis. Say, a C program slicer.

And you tell "them": You don't have to check about all the (possibly, often false) alarms emitted by the first analysis. The sliced program behaves the same as the original program as long as none of them is triggered. The slicer produces programs that are equivalent for the slicing criterion for "defined" execution paths.

And users merrily ignore the alarms and use the slicer.

And then you realize that perhaps there is a misunderstanding. For instance, most implementations of memmove (you know, the one that handles overlapping blocks) actually invoke unspecified behavior when called with pointers that do not point to the same block (comparing addresses that do not point to the same block). And your analyzer ignore both execution paths, because both are unspecified, but in reality both execution paths are equivalent and all is well.

So there shouldn't be any misunderstanding on the meaning of alarms, and if one intends to ignore them, only unmistakable undefined behaviors should be excluded.

And this is how you end up with a strong interest in distinguishing between unspecified behavior and undefined behavior. No-one can blame you for ignoring the latter. But programmers will write the former without even thinking about it, and when you say that your slicer excludes "wrong behaviors" of the program, they will not feel as they are concerned.

And this is the end of a story that definitely did not fit in a comment. Apologies to anyone who read that far.

Pascal Cuoq
As someone who writes critical code, such an analyzer would provide more benefit if it told me about the full trace of `*p`'s invalidity -- that is, once you know it's wrong, don't ignore the path, make it all the same problem and tell me where it starts and the vastness of it's error inducement.
Mark E
@Mark This involves a whole set of other (specifically, backwards) techniques, I'm afraid: "What inputs can lead `p` to be invalid here?". "All in good time" is the only answer I'm afraid I can provide at the moment.
Pascal Cuoq
Understandably this is difficult, but what I'm suggesting is that you should continue to propagate the error forwards, and mark it as the *same* error, rather than a new one or not at all.
Mark E
BTW, have you heard of [sixgill](http://sixgill.org) or [SATURN](http://saturn.stanford.edu/)?
Mark E
I didn't know about these tools, although the names behind them are familiar. The design space for such verification tools is very large. It's always good to hear about different approaches.
Pascal Cuoq
@Pascal: Thank you for taking the time to write this. I am clearly playing out of my league here. Obviously a very hard problem. I've read your whole comment twice and am starting to see what you're getting at. For what little it is worth, I withdraw my objection above.
dmckee
@Pascal is there a benchmark set of programs you use to test your framework? (it sounds like the community might be small enough that there's a shared set...)
Mark E
@Mark the tools are so different that the only way is an open "exposition" such as organized by NIST . This seems like good fun. http://rgaucher.info/post/2008/02/05/NIST-Static-Analysis-Tool-Exposition%3A-No-this-is-not-a-competition . Otherwise, there is http://se.cs.toronto.edu/index.php/Verisec_Suite , but we have verified that at least one case was not a bug and another had a bug in addition to the one you were supposed to find.
Pascal Cuoq