05.06.2013 Views

Differences between static analysis and model checking - IAR Systems

Differences between static analysis and model checking - IAR Systems

Differences between static analysis and model checking - IAR Systems

SHOW MORE
SHOW LESS

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

y Anders Holmberg, <strong>IAR</strong> <strong>Systems</strong><br />

Are <strong>static</strong> <strong>analysis</strong> <strong>and</strong> <strong>model</strong> <strong>checking</strong> at odds?<br />

A viewpoint on two methods of software verification<br />

This article will look briefly at some of the differences <strong>between</strong> <strong>static</strong> <strong>analysis</strong> tools in general <strong>and</strong><br />

domain specific <strong>model</strong> <strong>checking</strong>. For the purpose of this discussion a <strong>static</strong> <strong>analysis</strong> tool is a tool that<br />

analyzes source code to detect potential run-time problems with the code. For domain specific <strong>model</strong><br />

<strong>checking</strong> we will use the verification engine built into the <strong>IAR</strong> visualSTATE state machine design tool as<br />

an example.<br />

An exhaustive discussion on <strong>static</strong> <strong>analysis</strong> tools for source code <strong>analysis</strong> will naturally not fit into this<br />

article; the field is subject to lots of research <strong>and</strong> a lot of groundbreaking results have been achieved in<br />

the last few years. However, if you look closely you run into a classification problem because there a<br />

many different technologies with varying strengths <strong>and</strong> weaknesses used <strong>and</strong> sometimes combined into<br />

the same tool set... At one end of the spectrum you find pattern based checkers like MISRA checkers,<br />

source code metrics tools <strong>and</strong> some well-known tools that can do a fair amount of useful <strong>checking</strong> on<br />

the fuzzy line <strong>between</strong> syntax <strong>and</strong> language semantics.<br />

On the other end you have tools that use techniques called “symbolic execution” <strong>and</strong> “abstract<br />

interpretation” <strong>and</strong> even “<strong>model</strong> <strong>checking</strong>” to draw very advanced, <strong>and</strong> surprisingly accurate conclusions<br />

about the safety <strong>and</strong> integrity of your code base.<br />

In essence most advanced commercial <strong>static</strong> <strong>analysis</strong> tools will analyze your source code <strong>and</strong> try to<br />

draw conclusions about several security <strong>and</strong> integrity properties of your code. Typical checks include:<br />

• Null pointer dereference <strong>and</strong> use after free<br />

• Division by zero<br />

• Buffer overruns<br />

• Variable use before initialization<br />

• Memory leaks<br />

• Dead code, i.e. code that will never be executed<br />

• Etc…<br />

Some of these tools let the user write checks for either simple pattern based properties, or in the more<br />

advanced tools, checks for temporal properties as well as assertive checks. A temporal property might<br />

be “Function A() must always be called before function B(), even if other functions are called in<br />

<strong>between</strong>.”<br />

To complicate the picture, some tool sets on the market combine <strong>static</strong> <strong>analysis</strong> with the generation of<br />

test cases <strong>and</strong> test harnesses for automatic testing. The testing phase can then complement the <strong>static</strong><br />

<strong>analysis</strong> by testing for example memory properties in runtime as well as testing the functional aspects of<br />

the application.


Testing, testing…<br />

With recent advances in the capabilities of <strong>static</strong> <strong>analysis</strong> tools it can be argued that any software<br />

development shop that take the issue of software integrity <strong>and</strong> safety seriously should have at least one<br />

such tool available <strong>and</strong> use it regularly. However, even advanced <strong>static</strong> <strong>analysis</strong> techniques will not find<br />

all issues with your code. For example, even if a tool finds many true crash bugs in your program that<br />

you would have been hard pressed to find with traditional testing it will not help you decide if you have<br />

actually implemented the right functionality. In a way you can say that <strong>static</strong> <strong>analysis</strong> can help you<br />

determine whether you have built your thing the right way, but not if you have built the right thing…<br />

Another built-in problem for <strong>static</strong> <strong>analysis</strong> tools is that they are dependent on the language semantics,<br />

which will have the effect that some checks are either very difficult to create or impossible, even if you<br />

know what the code is trying to achieve. All commercial <strong>analysis</strong> tools also have a problem with “false<br />

positives”, i.e. reported errors that are in fact no problems, which is a symptom of necessary<br />

simplifications in the <strong>analysis</strong> phase. That is by no means an indication that you should avoid these<br />

tools, but rather that to be able to give any results at all in a reasonable amount of time the tools have to<br />

make certain assumptions.<br />

The state of affairs<br />

We will now take a look at an example where <strong>static</strong> <strong>analysis</strong> cannot really help you. Consider the<br />

following state machine design. It is of course a made up example <strong>and</strong> created solely for this article, but<br />

it serves well to illustrate the underlying problem.<br />

Figure 1: State machine<br />

If you look closely at the design you will notice that there might be a problem with the transition from<br />

state Strange to state C. The transition is only enabled if the variable ‘x’ is below 10, so if current state is<br />

Strange <strong>and</strong> x does not fulfill the condition the machine gets stuck. This might be intentional, but<br />

probably not. This kind of situation is called a dead-end. In the example it is quite easy to spot this just<br />

by a quick glance at the <strong>model</strong>, but it is no means trivial or even possible if the machine is very complex;<br />

especially if hierarchy <strong>and</strong> parallelism is involved. (For this example we ignore the question of where ‘x’<br />

is updated <strong>and</strong> how.)<br />

If we have a specialized <strong>model</strong> checker that knows about state machine semantics it is possible to spot<br />

problems like this automatically, even for very complex designs. The reason is that the <strong>model</strong> checker in<br />

this case cares only for the properties of the state machine taken as an abstraction, blissfully ignoring<br />

how the state machine might be implemented in code. Or to put it another way: we are mapping a well<br />

defined state machine semantics onto the much more expressive C language—if we assume for a<br />

moment that the translation from the design into code is correct it is also a reasonable assumption that<br />

Page 2


we can check the design at the design level instead of the code level. By <strong>checking</strong> the design at the<br />

<strong>model</strong> level we also have the advantage of knowing what we are <strong>checking</strong> <strong>and</strong> can thus invent<br />

specialized checks. As we noted above, it is close to impossible for a general <strong>static</strong> <strong>analysis</strong> tool to<br />

figure out the exact high-level intent of any piece of code we feed to it. What’s more, even if the <strong>static</strong><br />

analyzer is powerful enough to figure out that it is a state machine it is analyzing, there are checks that<br />

are very difficult to perform in the source code context. We will now take a look at such an example<br />

based on the state machine in Figure 1.<br />

The code to success<br />

Take a look at the following code:<br />

Figure 2: State machine code<br />

The code represents a part of the state machine design translated to straight C code. The pattern is<br />

pretty obvious; each event has its own case label in the switch statement <strong>and</strong> for each event we check<br />

what state is currently active by <strong>checking</strong> the value of the variable ‘CS’ (Current State) against each<br />

state name that has an outgoing transition triggering on the event. If we find a match the corresponding<br />

action is performed, which for most of the transitions in this <strong>model</strong> is just to set the working state to the<br />

new state. Did you notice how we also checked the condition ‘x


When that transition fires we call the action function before setting the new state. When we are done<br />

processing an event, we set the current state to the working state. (It might seem unnecessary to<br />

separate current state <strong>and</strong> working state, but that way we can get the benefit of <strong>checking</strong> in runtime for<br />

conflicting transitions. The code for that is not shown here.)<br />

This code is very close to what you would get by code generating the design in visualSTATE, but I’ve<br />

granted myself some artistic freedom in removing details that are only relevant in real production code.<br />

The verification engine in visualSTATE can among other things find the following properties in a state<br />

machine:<br />

• Transitions that are in conflict. I.e. two or more transitions out of a state that can be activated<br />

by the same event <strong>and</strong> does not have mutually exclusive guard conditions.<br />

• Reachability: Can we reach a certain state, <strong>and</strong> can we activate a certain transition?<br />

• Dead-ends. A dead-end is a state that you can enter but never exit. However, a state can be<br />

either a dead-end already from the start or become one during the execution life span, i.e. a<br />

state that has been possible to enter <strong>and</strong> exit multiple times during execution might all of a<br />

sudden become a dead-end because some guard condition on an outgoing transition is now<br />

impossible to fulfill. The possible dead-end in state Strange is of the latter type.<br />

Can a general <strong>static</strong> <strong>analysis</strong> tool h<strong>and</strong>le these situations? No, <strong>and</strong> yes—read on…<br />

If we start with the question of reachability, what does it mean in the context of the generated C code?<br />

Let’s take transition reachability first, because that concept is fairly simple: Look at line 58-61. These<br />

lines implement the logic for the transition from state A to state C. It is not difficult to realize that if we<br />

reach the statement at line 60 that assigns the new state to the working state variable, we have indeed<br />

activated that transition. So from a <strong>static</strong> <strong>analysis</strong> point of view it should be enough to check that there is<br />

no dead code in the code that implements the state machine.<br />

However, it’s not obvious that a particular tool will be able to deduce with 100% certainty that a transition<br />

is really reachable due to imprecision in the <strong>analysis</strong>. But for the transitions it is at least easy to map the<br />

concept of reachability from the design to the code.<br />

What happens if we instead look at reachability of states? Now it gets interesting! What does it even<br />

mean in the code context for a state to be reachable? It means that the current state variable or the<br />

working state variables should at some point be equal to the state that we are interested in. This is a<br />

property that a general <strong>analysis</strong> tool will have no idea about. But it is, at least in theory, possible to reuse<br />

the argument for transition reachability—we can argue that if one or more transitions are not<br />

reachable or possible to activate, their destination states are unreachable if all transitions going to these<br />

states are unreachable. This might be feasible for small state machines, but will soon get out of h<strong>and</strong> as<br />

complexity grows because every warning about unreachable code would have to be cross-referenced to<br />

the design.<br />

A simpler but still rather messy way of <strong>checking</strong> state reachability is to extend the program with explicit<br />

checks for each state name before setting the current state to the value of the working state. Here is an<br />

example of how to do it:<br />

if (WS != STATE_UNDEFINED)<br />

{<br />

if (WS == State_B)<br />

{ ; /* Can we reach state B? */ }<br />

if (WS == State_C)<br />

{ ; /* Can we reach state C? */ }<br />

if (WS == State_A)<br />

{ ; /* Can we reach state A? */ }<br />

CS = WS;<br />

}<br />

Page 4


Note that the purpose of each test is just to make sure that we can reach a particular state, we do not<br />

want to change the semantics of the program. That is why we have an empty statement in each ifclause.<br />

The empty statement is just there to be reachable if the corresponding state is reachable.<br />

Depending on the tool <strong>and</strong> the power of the <strong>analysis</strong> engine this might or might not work. So state<br />

reachibility can also be seen as a kind of code liveness, which can be messy to map to the generated<br />

code. Note that in some <strong>static</strong> <strong>analysis</strong> tools you do not need to modify the program with the tests, but<br />

can instead express them in a dedicated checker. But the point is that you will have to explicitly name<br />

each interesting state in an assert-like way. And this will have to be extended <strong>and</strong> modified in lock-step<br />

with the state machine design.<br />

So, we’ve seen so far that questions of basic reachability are in theory possible to answer with the help<br />

of a <strong>static</strong> <strong>analysis</strong> tool. (I.e. the question “can we reach this state, or that transition?” can be mapped to<br />

“can we ever reach this particular statement?”) It is however an open question whether a particular tool<br />

in reality can figure out what’s going on for a complex design, due to <strong>analysis</strong> precision <strong>and</strong> available<br />

computing power etc. It can also involve a lot of tedious semi-manual work to annotate the code or<br />

create specialized checkers.<br />

Running into a dead-end<br />

Let us now look at a question that is far trickier to answer. We have already talked a bit about dead<br />

ends, i.e. states that can be entered but never exited because no guard condition on transitions leading<br />

out of the state can be fulfilled.<br />

Being a dead-end state can very well be a temporal property, so the fact that we have exited it a<br />

thous<strong>and</strong> times does not guarantee that the state is not a dead-end. It can be further complicated by the<br />

fact that a state might look like it has become a dead-end, because it is dependent on some property of<br />

a parallel region in the state machine that is not true at the moment, but might become true at a later<br />

time.<br />

How do we map the concept of a dead-end to a property of the implementation code? We cannot use<br />

the basic checks for reachability, as those only concern themselves with statements being reachable at<br />

least once. What does it mean in the C code if we enter a dead-end? It in essence means that if the<br />

code is processing a particular event, that event will just be ignored. Or rather, we will execute some<br />

tests like the following code, to determine if any transitions can fire. And it is perfectly normal that such a<br />

test does not evaluate to true—the fact that we are not taking any action based on a particular event<br />

does not generally mean that the state machine has entered a dead-end, because there might be other<br />

transitions out of a state triggering on different events. This means that we might have several, even a<br />

very large number, of invocations of the state machine code were nothing will happen—at least not for<br />

the transitions we are interested in...<br />

if ((CS == State_Strange)<br />

&& (x < 10))<br />

{<br />

WS = State_C;<br />

}<br />

If we want to try to capture the dead-end property in C semantics we will have to do something like the<br />

following (in pseudo code):<br />

if ((event = e && CS == State_under_test) && !guard1 && !guard2 && … && !guardn)<br />

{<br />

/* State_under_test *might* be a dead end, but only if all guards are eternally<br />

false */<br />

}<br />

We have to check for each state for all outgoing transitions triggering on a specific event whether their<br />

guards are all false. This situation might indicate a dead-end. But only if we can prove that all the guard<br />

Page 5


expressions can never be true after some point in time. This must be repeated for all events that can<br />

trigger a transition out of a state. And so far we have only checked one state…<br />

So given ordinary C code it is difficult to express the dead-end property in any meaningful way; in fact, is<br />

does not really get any simpler by realizing that the code is a pure state machine. And this is mainly<br />

because there is a gap <strong>between</strong> the semantics of the state machine abstraction <strong>and</strong> the implementation<br />

language.<br />

This gap is not unique for the state machine abstraction, so given that you work in a problem domain<br />

where some form of formal verification or <strong>model</strong> <strong>checking</strong> is available you might reap huge benefits by<br />

using it – together with a competent <strong>static</strong> <strong>analysis</strong> tool, of course!<br />

Page 6

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!