Static Analysis in Embedded System Development
October 28, 2021
Story
Due to the rapid growth of embedded system industry, the code quality of the embedded devices became one of the prime concerns. Taking into account the specificity of embedded system development (difficult debugging, high cost of an error, etc.), developers need to use special tools to enhance their code quality.
A static code analyzer is one of these tools. This article describes static analysis and how it benefits in embedded systems.
Static Code Analysis
First, let's figure out what static code analyzers are and what functions they can perform.
A static code analyzer is software that analyzes a program without actually executing it. Static analysis tools perform a deeper check of the source code than compilers. Usually, compilers find only syntax errors.
How static analysis tools work:
- the input data of the analyzer is the source code (preferably compilable)
- the analyzer transforms the source code into a special model for further analysis (AST, semantic model, etc.);
- the analyzer searches for defects by applying a set of diagnostic rules to the model. The diagnostic rules are based on various methodologies;
- the analyzer saves all issued warnings in a format convenient to you;
- developers need only to study the report and fix all the defects;
- PROFIT!
Static analyzers can carry out a wide range of tasks. Let's cover the most common tasks of the analyzer:
- the error detection in program code. In this case, static analysis greatly complements code review. It allows you to find and fix a number of problems before you and your colleague get started with tiresome code review;
- the code quality enhancement in the broad terms. Code quality can include readability, maintainability, code complexity, level of cohesion, and other aspects that can directly or indirectly affect the number of errors. Thus, static analyzers help follow coding standards (both accepted within the company and generally accepted);
- code analysis as part of the Quality Gates mechanism in CI/CD. Analyzers not only warn about potential errors in code, but also work as a protection mechanism. They stop continuous delivery if the code quality level does not meet the specified requirements. Such code analyzers extend the compiler behavior and block the build if they detect errors or code fragments that do not comply with standards;
- collection of project metrics, statistics gathering, the graph and diagram constructions that reflect the "general health" of the project.
The Implementation Benefits
Static analyzers have proven to be extremely useful for embedded software. Let's look at the most obvious positive aspects of static analysis.
First, the use of static analysis reduces the likelihood of expensive, if not impossible, "flashing" of devices that were already released.
Errors in embedded systems software are extremely troublesome. The trouble is that the errors are impossible or almost impossible to correct once mass production has begun. Let's say, a company has already produced and delivered to the stores thousands of washing machines. However, turned out the machines don't work correctly in a certain mode. What should the company do? In general, the question is rhetorical and there are two real options:
- to go with the flow, receive negative customer feedback on various sites, and spoil the reputation. Of course, the company can release and send the manual addition saying "don't do this". However, this is a "weak" choice;
- to withdraw the machines from sale and start updating the firmware. It's an expensive choice.
It doesn't matter how many devices were released. It may be either problematic or even too late to fix an error. The rocket crashed — the error was detected, but it was too late. Patients died — the error was detected, but it won't bring people back. The missile defense system had targeting precision loss - the error was detected, but the damage is already done. Car breaks didn't work - errors were detected, but that won't help the car crash victims. The price of a programmatic error is terrifying, isn't it?
The conclusion is simple: code of embedded devices should be tested as thoroughly as possible. Especially if errors can lead to casualties or large financial losses.
Static code analysis is the process of detecting errors, but it doesn't guarantee it will find all the errors in code. However, a developer should use any opportunity to additionally check the code correctness. Static analyzers can point to various errors that remain even after several Code-Reviews.
If static analysis can help reduce the number of errors in the code of a device, that's awesome. Maybe the discovery of these particular errors will prevent loss of life. Or maybe the companies won't waste a lot of money and won't lose a good reputation due to customer complaints.
Secondly, static code analyzers significantly reduce the cost of software testing and the debugging process.
Static analysis allows you to find bugs already during coding, or during night builds. As a result, the search and fixing of most errors can be much cheaper.
Probably every developer had an unsuccessful attempt to "flash" the device. In the process, for example, the device wasn't set to proper voltage or completely burned out. What happened and where do you look for the problem? After all, not only a software error can be the root of the problem. It also may be an error in the hardware itself or in the poor-quality layout. Therefore, the process of finding an error can take a long time.
The saddest scenario:
- a developer is 100% sure that he wrote code correctly;
- circuit engineers and other colleagues responsible for the hardware are engaged in the project;
- there is a slow and exhausting search for the problem;
- the developer reviews the code again and suddenly discovers - a typo;
- super inefficient waste of energy and time of the teammates;
- it's embarrassing and unpleasant.
Such an error could pop up for the following reason. In the ongoing project, the developer used his old practices, which he needed to adapt to the project at a minimum. For example, he could write the following code fragment:
uchar Arr[3];
....
for (uchar idx = 0; idx != 4; idx++)
avg += Arr[idx];
avg /= 3;
And the background of this error is the following. The developer took his previous developments as a base, and the code was largely written using the Copy-Paste method. He didn't pay attention and forgot to replace 4 with 3 in one line. As a result, he got undefined behavior while accessing the index that lies outside the array bounds. Such code can be insidious. The program can work correctly during debugging. However, in real conditions, it may crush when the client runs it multiple times. It's great if a static analyzer finds such an error.
Therefore, to avoid the tortuous and exhausting debugging process, it's important to detect as many defective places as possible before you flash the device.
Thirdly, the use of static analysis insures developers who don't have much experience.
Program errors can be figuratively divided into two types. The developer knows about the errors of the first type. These errors appear in the code accidentally, due to inattention. Errors of the second kind show up in a situation when the developer simply does not know that it is impossible to write code this way. In other words, they can review such code as much as they want, but still won't find the error.
Static analyzers contain a knowledge base about various code patterns. Under certain conditions, these patterns lead to an error. Thus, they can point to an error that the developer himself would not have found. An example is the use of the 32-bit time_t type, which can lead to incorrect work of devices after 2038.
Another example is the program's undefined behavior, which occurs due to improper use of shift operators <</>>. These operators are very widely used in the code of microcontrollers. Unfortunately, developers often use these operators extremely carelessly. This makes programs unreliable and dependent on the particular version and settings of the compiler. At the same time, the program can work, but that's not because its code is written correctly, but because the developer is lucky.
With static analyzers, developers can hedge themselves against many such unpleasant situations. Additionally, you can use the analyzer to control the overall code quality. It is important when the project's team is growing or changing. In other words, the analyzer helps track whether a beginner has started writing bad code.
Fourth, modern static analyzers not only find the code errors and vulnerabilities, but also support coding standards for embedded systems. The standards increase the level of security, portability, and reliability of programs.
C and C++ are known as popular programming languages for embedded systems. Such standards as MISRA C, MISRA C++ and AUTOSAR C++ were developed for these languages. Each standard has a considerable number of rules and recommendations (MISRA C: 143, MISRA C++: 228, AUTOSAR C++: more than 350). It is simply impossible to comply to this number of rules and recommendations when coding without static code analyzers. These rules are coding patterns that developers need to avoid, thereby reduce the likelihood of a mistake. Currently, all the major players of static analysis (Coverity, Klockwork, PVS-Studio, ...) strive to increase the coverage of the standards as much as possible.
Coding Standards
The history of MISRA began a long time ago. Back then in early 90s, the "Safe IT" UK government program provided funding for various projects somehow related to security of electronic systems. The MISRA (Motor Industry Software Reliability Association) project itself was founded to create a guide for developing software of microcontrollers in land vehicles - in cars, mostly.
MISRA (as an organization) is a community of stakeholders from various auto and aircraft industries.
- Bentley Motor Cars;
- Ford Motor Company;
- Jaguar Land Rover;
- Delphi Diesel Systems;
- HORIBA MIRA;
- Protean Electric;
- Visteon Engineering Services;
- The University of Leeds;
- Ricardo UK;
- ZF TRW.
Very strong market players, aren't they? It is not surprising that their first language-related standard, MISRA C, gained widespread acceptance among developers of critical embedded systems. A little later, MISRA C++ appeared. Gradually, versions of the standards have been updated and revised to cover new features of languages. At the moment, the current versions are MISRA C: 2012 and MISRA C++: 2008.
MISRA's most distinctive features are its incredible attention to details and extreme meticulousness in ensuring safety and security. Authors did not just collect all C and C++ deficiencies in one place (as for example, the authors of CERT). They also carefully worked out the international standards of these languages and wrote out all possible ways to make a mistake. After, they added rules and recommendations on code readability. After all, it is harder to make a mistake in a simple and easy-to-read code, and easier to detect a bug during Code-Review.
Often, people who first encounter MISRA get the impression that the standard's purpose is to "ban this and ban that". In fact, it is so, but only partially.
The standard does have many rules that ban some actions. However, it's not meant to ban all out, but to list all possible ways that can somehow cause a security breach. For most rules, you choose yourself whether you need to follow them or not. Let me explain this in more detail.
The MISRA C rules are split into three main categories: Mandatory, Required and Advisory. Mandatory rules cannot be violated under any circumstance. For example, this section includes the rule: "don't use the value of an uninitiated variable". Required rules are less strict. They allow the possibility of deviation. But a developer needs to justify these deviations in written form and document them in detail. The rest of the rules belong to the Advisory category – they are non-obligatory.
MISRA C++ is a little different: there is no Mandatory category, and most of the rules belong to the Required category. Therefore, in fact, you have the right to violate any rule – just do not forget to document deviations. There is also the Document category. It includes mandatory rules (deviations aren't allowed) related to general practices such as "Each use of the assembler must be documented" or "An included library must comply with MISRA C++".
The standard contains both description of problem issues and tips on what one has to know before taking on a certain task: how to set up the development process according to MISRA; how to use static analyzers for checking the code for compliance; what documents one has to maintain, how to fill them out and so on.
At the moment, MISRA keeps evolving. For example, MISRA announced The MISRA C:2012 Third Edition (First Revision)" at the beginning of 2019. It is MISRA C:2012 edition that was updated and extended with new rules. At the same time, the upcoming release of "MISRA C: 2012 Amendment 2 – C11 Core", which is a revised standard of the 2012 year, was announced.
MISRA C++ does not stand still either. As you know, the last standard of MISRA C++ dates back to 2008, so the latest version of the language it covers is C++03. Because of this, there is another standard similar to MISRA, and its name is AUTOSAR C++. It was originally intended as a continuation of MISRA C++ and was intended to cover later versions of the language. Unlike its mastermind, AUTOSAR C++ gets updates twice a year and currently supports C++14. New C++17 and then C++20 updates are yet to come.
Conclusion
In this article, I wanted to show that using a static analyzer is definitely useful for any embedded project. The use of static analysis will help you to:
- cut down the time it takes to find and fix errors;
- reduce the likelihood of critical errors;
- reduce the need for firmware updates;
- monitor the overall code quality;
- monitor the performance of new team members;
- comply strictly with a certain software development standard;
- monitor the code quality of third-party modules/libraries.
Whether you need static analysis or not is up to you!
Maxim Stefanov is a C++, C#, and Java developer in PVS-Studio. For several years, he developed C++ and Java analyzers: wrote diagnostic rules, improved analysis technologies (Data Flow, symbolic execution). Currently, he develops utilities that simplify the use of the PVS-Studio static analyzer. Static code analysis enthusiast: speaks at conferences, write articles.