Embedded development comes with a wide variety of custom configurations, compilers, and build systems, which can make standard analysis approaches difficult to apply. To address this challenge, PVS-Studio provides a dedicated mechanism designed specifically for such environments. Let's take a closer look at how it works and see how you can use it in real cases.
Features of embedded systems
Embedded development in C and C++ powers an enormous range of devices—from electric kettles and toothbrushes to safety-critical systems such as medical equipment and aircraft control software. In these areas, the quality of software directly affects human safety. As a result, the code must be as reliable, predictable, and resilient to external factors as possible.
The need for thorough code verification in embedded systems continues to grow for several reasons:
- Embedded systems interact directly with users and external data, so they must correctly handle all input.
- Memory is often severely limited, making careful resource management essential.
- The software must remain stable under all operating conditions and provide a high level of fault tolerance.
- Many other factors also contribute to these requirements.
Embedded software targets a wide variety of platforms and use cases. Each project may rely on a different compiler, toolchain, or build system. The situation often becomes more challenging due to a "zoo" of custom-built scripts that are difficult to maintain.
PVS-Studio static analyzer offers several mechanisms specifically designed for embedded projects. They collect all the necessary information for analysis directly from the compilation process as it runs.
Specialized analysis mechanisms
All of the mechanisms described here follow the same general workflow. You start them before building your project, and they monitor every run of supported compilers throughout the build process:
- C and C++ compilers from the GNU Compiler Collection (gcc.exe, g++.exe) and compatible derivatives;
- Clang C and C++ compilers (clang.exe) and compatible derivatives;
- Borland C++;
- QCC;
- Keil MDK ARM Compiler 5/6;
- IAR C/C++ Compiler for ARM;
- Texas Instruments ARM Compiler;
- GNU Arm Embedded Toolchain;
- Texas Instruments Code Composer Studio, C6000-CGT, C2000-CGT (support is coming in October);
- GNU toolchain for RISC-V.
Note. If you'd like us to support a compiler that isn't currently supported, we'd be happy to evaluate adding it. You can send us a request to support a specific compiler or contact us about any issues you're experiencing via our feedback form.
After monitoring finishes, the server generates intermediate files and then runs the static analyzer.
Each mechanism targets a specific environment or use case. Let's look at them one by one.
Compilation monitoring CLMonitor.exe (for Windows)
CLMonitor.exe is a monitoring server that tracks compiler runs. Start it before building your project. Once monitoring is enabled, the server intercepts every run of supported compilers.
The compilation monitoring system follows a straightforward workflow, illustrated below:
How to start compilation monitoring
To start the monitoring process, run the following command:
CLMonitor.exe monitor
CLMonitor.exe will run in the background and track all supported compilers. To stop the process, run one of the commands described below.
You can also track only compiler runs that were started by a specific process identified by its PID. To do this, start CLMonitor.exe in the trace mode with the trace and --parentProcessID (-p) arguments.
The command for running CLMonitor.exe in this mode may look like this:
CLMonitor.exe trace –-parentProcessID 10256
If you want CLMonitor.exe to track only the build started from the same console, you can run CLMonitor.exe with the --attach (-a) argument:
CLMonitor.exe monitor –-attach
Building the project
After starting compilation monitoring, build your project using the usual workflow. Once the project build is complete, stop compilation monitoring so that you can analyze the collected data.
How to stop compilation monitoring
You can run the analysis immediately after building the project using the following command:
CLMonitor.exe analyze -l D:\ptest.plog
You can also pass additional parameters when starting the analysis:
-
-lis the path to the final analyzer report file; -
-uis the path to the suppress file; -
-cis the path to the.pvsconfiganalysis configuration file; -
--intermodularenables the intermodular analysis mode.
If you encounter issues while building the project and need to stop compilation monitoring without analyzing the resulting files, you can do so using the following command:
CLMonitor.exe abortTrace
The CLMonitor.exe monitoring server enables saving captured compilation data in a separate dump file. This allows you to run the analysis without rebuilding the project.
To save the dump file, use the following command:
CLMonitor.exe saveDump -d D:\monitoring.zip
where -d is the path to the final dump file.
To run the analysis using a saved dump file, use the following command:
CLMonitor.exe analyzeFromDump -l d:\ptest.plog -d d:\monitoring.zip
This command supports all the flags described above that are used to start the analysis.
The Wrap Compilers interception mode (for Windows)
As we continue the discussion of embedded system specifics, it's worth mentioning another challenge involved in analyzing embedded projects. Such projects often consist of quickly compiling C source files, and CLMonitor.exe may not have enough time to detect all source files.
To ensure the interception of all compilation processes, the monitoring server can switch to a more aggressive mode using the Image File Execution Options (IFEO) mechanism on Windows.
The Wrap Compilers intercept mode runs a special handler before each compilation process begins. It sends the necessary information to the monitoring server and then continues running the compiler.
Note. This mode requires access to editing the path in the Windows Registry:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options.You can grant the required access to the monitoring server by running it as administrator or by giving it write permissions to the registry branch above—or for the entire registry.
To enable the interception mode in the console version of the monitoring tool, pass the --wrapCompilers (-W) flag to the monitoring server with a list of compilers, for example:
CLMonitor.exe trace --wrapCompilers gcc.exe,g++.exe
Compiler tracing (for Linux)
The compile_commands.json file is used to analyze projects on the Linux system. You can find more information on the relevant documentation page.
If generating the compile_commands.json file is not possible, you can use the compilation trace mode. To do so, install the strace utility of the 4.7 version or later. It enables the analyzer to collect essential information about project compilation during the build process.
You can build the project and track its compilation process using the following command:
pvs-studio-analyzer trace -- build_command -o /path/to/strace_out
where:
-
build_commandis the command used to build the project; -
-ois the path to the compilation trace file. If no flag is specified, a file namedstrace_outis saved in the CWD.
As a result of the tracing, a file named strace_out will be created by default.
Once you have obtained the strace_out compilation trace file, you can run the analysis using the following command:
pvs-studio-analyzer analyze -f /path/to/strace_out
You can also pass additional parameters when starting the analysis:
-
-fis the path to the file containing the compilation trace results; -
-lis the path to the final analyzer report file; -
-uis the path to the suppress file; -
-cis the path to the.pvsconfiganalysis configuration file; -
--intermodularenables the intermodular analysis mode.
Visual Studio Code
You can also use compilation monitoring with the PVS-Studio plugin for Visual Studio Code.
To start compilation monitoring, use the PVS-Studio: Run compiler monitoring for C and C++ command in the Visual Studio Code Command Palette (Ctrl + Shift + P).
When monitoring starts, the plugin window containing the table will display an indicator showing that monitoring is running:
Once compiler calls are intercepted, their number will be displayed in the monitoring indicator, and the start button will appear.
You can start the project analysis by clicking the button or using the PVS-Studio: Stop monitoring and start analysis command in the Visual Studio Code command palette.
The first time you run monitoring on a project, the plugin will prompt you to edit the ./.PVS-Studio/CLMonitorAnalyzerConfig.jsonc configuration file.
Click Edit to set the following parameters:
- the path to a monitoring dump file (by default,
./.PVS-Studio/lastMonitoring.zip); - the path to the configuration file or directory containing
.pvsconfigfiles.
Click Continue to run the analysis using the default settings.
If the source code files and the build configuration haven't been changed, you can use the dump file to run the analysis. To do this, select the PVS-Studio: Start analysis from compiler monitoring dump file command from the Visual Studio Code command palette.
In PVS-Studio plugin for Visual Studio Code, you can use the Wrap Compilers mode. To enable it, open the Monitoring (C and C++) tab in the plugin settings and specify the names of the compiler executable files you want to monitor.
Note. To enable monitoring in this mode, restart Visual Studio Code with administrator privileges.
A standard for improving reliability
Fixing bugs in embedded systems after a device ships is both difficult and expensive. This may require product recalls, firmware updates, reshipping, or replacing devices entirely in the worst case. Such issues come at a high cost, both financially and in terms of reputation.
To minimize risks, errors must be detected and fixed at the development stage. When there are many errors, it's much more effective to classify and track them using industry standards.
For example, the IEC 61508-7 industry standard requires using a subset of the C and C++ languages, coding standards, and static code analyzers when designing systems with a high safety integrity level (SIL).
The automotive industry relies on the ISO 26262, which establishes functional safety requirements for road vehicles. The standard also specifies requirements for verifying software modules and recommends using static code analysis as part of the verification process.
MISRA C and MISRA C++ have become the industry standard for defining safe subsets of the C and C++ languages.
The MISRA standards provide guidelines for developing safe and reliable C and C++ software in safety-critical fields, including automotive, aerospace, medical devices, and industrial automation. These are industries where the cost of failure is exceptionally high.
MISRA improves software safety by preventing bugs and vulnerabilities and by restricting complex language features that can cause unexpected program behavior.
Its authors have scrutinized international C and C++ standards and listed every possible way to make a mistake. The MISRA C and MISRA C++ standards provide guidelines to help reduce errors, improve code readability, and enhance maintainability.
Manually enforcing code quality in large projects is both difficult and time-consuming. For this reason, MISRA strongly encourages the use of automated code quality tools.
PVS-Studio supports code analysis to ensure compliance with multiple MISRA standards. At the time of writing, it covers the following Mandatory and Required rules:
- MISRA C 2012 — 80%;
- MISRA C 2023 — 85%;
- MISRA C++ 2008 — 33%;
- MISRA C++ 2023 — 35%.
In the PVS-Studio 7.41 release, we completed work on covering the MISRA C 2023 standard, achieving 85% coverage. We're not stopping there: This year, we've already begun expanding our support for the MISRA C++ 2023 standard.
For more information on the classification of warnings according to the MISRA C and MISRA C++ standards, see the relevant section of the documentation.
When working with MISRA standards, the MISRA Compliance report is particularly useful. It assesses whether a project is compliant with MISRA C and/or MISRA C++, considering all deviations and re-categorizations.
Learn more about the MISRA Compliance report in the relevant section of the documentation.
Dangerous code fragments in embedded projects
We've looked at several ways to analyze projects for embedded systems using PVS-Studio. Now let's take a look at some real errors the analyzer found in open-source embedded projects.
We'll use several popular real-time operating systems (RTOSs) as examples. An RTOS is a specialized OS designed to ensure tasks are completed within strict timing constraints. These systems are widely used in microcontrollers, industrial equipment, electronic devices, and other embedded applications.
RT-Thread
RT-Thread was created by Richard Barry in 2003. Since 2017, it has been developed under Amazon Web Services. Its small kernel footprint and excellent portability across dozens of hardware platforms have made it a popular choice for microcontroller-based devices.
The project was checked as of the cfda3b3 commit.
Fragment N1
PVS-Studio warnings:
V1031 The 'memcmp' function is not declared. Passing data to or from this function can be affected. dhcp_server_raw.c 151
V1031 The 'strchr' function is not declared. Passing data to or from this function can be affected. dhcp_server_raw.c 718
V647 The value of 'int' type is assigned to the pointer of 'char' type. Consider inspecting the assignment: 'p = strchr(str_tmp, '.')'. dhcp_server_raw.c 718
#include <stdio.h>
#include <stdint.h>
// ....
static struct dhcp_client_node *
dhcp_client_find_by_mac
(struct dhcp_server *dhcpserver, const u8_t *chaddr, u8_t hlen)
{
struct dhcp_client_node *node;
for (node = dhcpserver->node_list; node != NULL; node = node->next)
{
if (memcmp(node->chaddr, chaddr, hlen) == 0) // <=
{
return node;
}
}
return NULL;
}
// ....
void dhcpd_start(const char *netif_name)
{
// ....
char str_tmp[4 * 4 + 4] = DHCPD_SERVER_IP;
char *p = str_tmp;
ip4_addr_t ip_start, ip_end;
p = strchr(str_tmp, '.'); // <=
if (p)
{
p = strchr(p + 1, '.'); // <=
if (p)
{
p = strchr(p + 1, '.'); // <=
}
}
// ....
}
Two interesting warnings stem from the same mistake, which fundamentally changes the behavior of the entire source file. The analyzer reports that the memcmp and strchr functions aren't declared. A quick look at the included headers shows that the file is missing <string.h>.
This code still compiles in C because an undeclared function is assumed to return int by default. As a result, the analyzer also reports a second issue: an int value is assigned to a char pointer.
Such code can lead to incorrect program behavior, as demonstrated in the article "A nice 64-bit error in C". The fix is straightforward: simply add #include <string.h> at the beginning of the file.
Fragment N2
PVS-Studio warning: V614 Potentially uninitialized pointer 'GPIOx' used. HAL_GPIO.c 48
typedef enum
{
GPIOA,
GPIOB,
GPIOC,
GPIOD,
}enum_GPIOx_t;
void HAL_GPIO_IRQHandler(enum_GPIOx_t fe_GPIO, uint32_t fu32_GPIO_Pin)
{
GPIO_TypeDef *GPIOx; // <=
switch (fe_GPIO)
{
case GPIOA:
case GPIOB:
{
GPIOx = GPIOAB;
}break;
case GPIOC:
case GPIOD:
{
GPIOx = GPIOCD;
}break;
default: break; // <=
}
if (fe_GPIO == GPIOB || fe_GPIO == GPIOD )
{
fu32_GPIO_Pin <<= 16;
}
if (GPIOx->RIS & fu32_GPIO_Pin) // <=
{
GPIOx->IC = fu32_GPIO_Pin;
/* user can call your application process function here */
/* ...... */
}
}
The analyzer warns that an uninitialized pointer is being used. The GPIOx variable is declared, but it's assigned a value in only four switch branches. In the default branch, it remains uninitialized, so using the pointer afterward results in undefined behavior.
In C, the enum parameter can receive any integer value. If this happens, execution enters the default branch, and the pointer remains uninitialized.
To fix the issue, we'll add appropriate handling to the default branch. For example, return from the function when fe_GPIO contains an unknown value:
default:
return;
Fragment N3
PVS-Studio warning: V570 The 'RTC_DateStruct->RTC_WeekDay' variable is assigned to itself. hk32f0xx_rtc.c 986
void RTC_GetDate(uint32_t RTC_Format, RTC_DateTypeDef *RTC_DateStruct)
{
uint32_t tmpreg = 0;
// ....
/* Check the input parameters format */
if (RTC_Format == RTC_Format_BIN)
{
/* Convert the structure parameters to Binary format */
RTC_DateStruct->RTC_Year =
(uint8_t)RTC_Bcd2ToByte(RTC_DateStruct->RTC_Year);
RTC_DateStruct->RTC_Month =
(uint8_t)RTC_Bcd2ToByte(RTC_DateStruct->RTC_Month);
RTC_DateStruct->RTC_Date =
(uint8_t)RTC_Bcd2ToByte(RTC_DateStruct->RTC_Date);
RTC_DateStruct->RTC_WeekDay =
(uint8_t)(RTC_DateStruct->RTC_WeekDay); // <=
}
}
This error is quite interesting. I reformatted the code snippet because the original was too wide, which makes the issue immediately obvious here. Developers most likely copied the lines converting the value type from BCD to binary, but in the last line they forgot to call RTC_Bcd2ToByte (this is a classic mistake). As a result, RTC_WeekDay is assigned to itself, which is pointless.
The fixed code:
RTC_DateStruct->RTC_WeekDay =
(uint8_t) RTC_Bcd2ToByte(RTC_DateStruct->RTC_WeekDay);
Fragment N4
PVS-Studio warning: V595 The 'cond' pointer was utilized before it was verified against nullptr. Check lines: 346, 353. pthread_cond.c 346
rt_err_t _pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex,
rt_int32_t timeout)
{
rt_err_t result = RT_EOK;
rt_sem_t sem;
rt_int32_t time;
sem = &(cond->sem); // <=
if (sem == RT_NULL)
{
return -RT_ERROR;
}
time = timeout;
if (!cond || !mutex) // <=
{
return -RT_ERROR;
}
// ....
}
Developers used the cond pointer before checking it for NULL. Such errors are quite common in projects.
Two options are possible here:
- The check is redundant because a valid object is always passed to the function input.
- The check is necessary, but the compiler might remove it during optimization because it recognizes that the pointer is used further up in the code. In this case, a dereference of a null pointer occurs, resulting in undefined behavior. This article provides a more detailed look at a similar case, where the address of an object is obtained relative to a null pointer, followed by dereferencing the pointer.
To fix the error, we'll add a check at the beginning of the function. For example:
rt_err_t _pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex,
rt_int32_t timeout)
{
rt_err_t result = RT_EOK;
rt_sem_t sem;
rt_int32_t time;
if (!cond || !mutex)
{
return -RT_ERROR;
}
sem = &(cond->sem);
if (sem == RT_NULL)
{
return -RT_ERROR;
}
time = timeout;
// ....
}
FreeRTOS
FreeRTOS, which originated in China in 2006, is developed by the RT-Thread Development Team. It's widely used in IoT devices, home appliances, and industrial equipment. It offers a well-developed ecosystem with a wide range of built-in components, coming close to being a full-fledged OS.
The project was checked as of the c73a397 commit.
Fragment N1
PVS-Studio warning: V557 Array overrun is possible. The value of 'uxTimerID' index could reach 21. TimerDemo.c 1167
static uint8_t ucAutoReloadTimerCounters[configTIMER_QUEUE_LENGTH + 1] = { 0 };
// ....
static void prvAutoReloadTimerCallback( TimerHandle_t pxExpiredTimer )
{
size_t uxTimerID;
uxTimerID = ( size_t ) pvTimerGetTimerID( pxExpiredTimer );
if( uxTimerID <= ( configTIMER_QUEUE_LENGTH + 1 ) ) // <=
{
( ucAutoReloadTimerCounters[ uxTimerID ] )++;
// ....
}
This error is particularly sneaky and often slips through code reviews. Few reviewers stop to check an index boundary when the code appears to work correctly. Tests are also unlikely to catch it, which makes the error even harder to find. However, accessing an array out of bounds results in undefined behavior.
The issue stems from using a non-strict comparison <= to set the bounds of the uxTimerID index. As a result, uxTimerID can take the configTIMER_QUEUE_LENGTH + 1 value, which is also the size of the array. Accessing an element at that index results in going outside the array bounds.
The fixed code:
if( uxTimerID < ( configTIMER_QUEUE_LENGTH + 1 ) )
Fragment N2
PVS-Studio warning: V547 Expression 'i + 1 > (4 + 32)' is always false. trcSnapshotRecorder.c 831
static uint8_t writeInt8(void * buffer, uint8_t i, uint8_t value)
{
TRACE_ASSERT(buffer != (void*)0, "writeInt8: buffer == NULL", 0);
if (i >= MAX_ARG_SIZE)
{
return 255;
}
((uint8_t*)buffer)[i] = value;
if (i + 1 > MAX_ARG_SIZE)
{
return 255;
}
return ((uint8_t) (i + 1));
}
The error is that the second check, if (i + 1 > MAX_ARG_SIZE), is always false after the first check, if (i >= MAX_ARG_SIZE).
Once i passes the first check, i + 1 can no longer exceed MAX_ARG_SIZE. It's likely that one of the checks is redundant, or the second condition was intended to be different.
Zephyr
Zephyr launched in 2016 with Intel's support and is now being developed under the Linux Foundation. It targets IoT, embedded devices, and automotive systems, and features a modular architecture with a wide range of built-in subsystems.
The project was checked as of the c6da464 commit.
Fragment N1
PVS-Studio warning: V547 Expression 'conv->pad0_value > 0' is always true. cbprintf_complete.c 1224
static char *encode_float(/*....*/)
{
// ....
if ((decexp < 0) && (precision > 0)) {
conv->pad0_value = -decexp;
if (conv->pad0_value > precision) {
conv->pad0_value = precision;
}
precision -= conv->pad0_value;
conv->pad_postdp = (conv->pad0_value > 0); // <=
}
// ....
}
The analyzer reports that conv->pad0_value > 0 is always true. As a result, the conv->pad_postdp variable always receives the same value. Here's why the condition is always true:
- As soon as execution enters the first
ifblock,conv->pad0_valueis assigned the-decexpvalue. Sincedecexp < 0, the result is always positive. - If execution enters the nested
ifblock, the value remains positive becauseprecision > 0. - As a result,
conv->pad0_valueis always greater than zero.
Most likely, the condition is redundant and can be removed. Alternatively, the initialization of pad0_value needs to be moved outside the if block. The final decision is up to the developers.
Fragment N2
PVS-Studio warning: V557 Array overrun is possible. The value of 'keep_cnt ++' index could reach 16. cbprintf_packaged.c 1143
int cbprintf_package_convert(/*....*/)
{
// ....
__ASSERT_NO_MSG(keep_cnt < sizeof(keep_str_pos));
if (keep_cnt < sizeof(keep_str_pos)) {
keep_str_pos[keep_cnt++] = arg_idx;
keep_str_pos[keep_cnt++] = arg_pos;
}
// ....
}
This is a particularly interesting case. The developers correctly checked the array bounds but overlooked how the post-increment operator affects the index. Here's what happens:
- the
keep_cnt < sizeof(keep_str_pos)condition allowskeep_cntto reachN - 1, whereNis the array size; - the first post-increment accesses the array at the
N - 1index and incrementskeep_cnttoN; - the second post-increment accesses the array at the
Nindex and incrementskeep_cnttoN + 1.
Accessing outside the array bounds results in undefined behavior. We can fix the code by rewriting the condition as follows:
if (keep_cnt + 1 < sizeof(keep_str_pos))
Fragment N3
PVS-Studio warning: V779 Unreachable code detected. It is possible that an error is present. sched.c 345
#define z_except_reason(reason) do { \
__EXCEPT_LOC(); \
z_fatal_error(reason, NULL); \
} while (false)
#define k_panic() z_except_reason(K_ERR_KERNEL_PANIC)
void z_thread_halt(/*....*/)
{
// ....
if ((thread == _current) && !arch_is_in_isr()) {
if (z_is_thread_essential(thread)) {
k_spin_unlock(&_sched_spinlock, key);
k_panic(); // <=
key = k_spin_lock(&_sched_spinlock); // <=
}
// ....
}
The analyzer detected unreachable code. The k_panic macro calls z_fatal_error, which terminates the system and never returns the control flow. As a result, the line that follows it is never executed.
Unfortunately, it's hard to recommend the right solution without more information. It's possible that this line simply doesn't belong here.
Fragment N4
PVS-Studio warning: V795 Please note that the size of the 'time_t' type is not 64 bits. After year 2038, the program will work incorrectly. clock.c 47
static void timespec_from_ticks(uint64_t ticks, struct timespec *ts)
{
uint64_t elapsed_secs = ticks / CONFIG_SYS_CLOCK_TICKS_PER_SEC;
uint64_t nremainder = ticks % CONFIG_SYS_CLOCK_TICKS_PER_SEC;
*ts = (struct timespec){
.tv_sec = (time_t)elapsed_secs,
/* For ns 32 bit conversion can be used since its smaller than 1sec. */
.tv_nsec = (int32_t)k_ticks_to_ns_floor32(nremainder),
};
}
The problems of the future are already knocking at the door. In just 12 years, on January 19, 2038, this project will run into the classic Year 2038 problem.
The root cause is that time_t will no longer behave as expected. Its behavior will depend on the platform because the type stores the number of seconds since January 1, 1970. After the cutoff date, the value will overflow, causing incorrect time calculations.
How to fix issues
To keep dangerous code patterns out of your codebase, it's important to take a comprehensive approach to software quality. This may include code reviews, manual and automated testing, dynamic analysis, and other verification techniques.
Static code analysis is one of the most effective and cost-efficient ways to catch issues early in the development process. Try PVS-Studio on your project for free and explore pricing for the full version.
If you're still looking for a reason to adopt static analysis, here are five reasons why it is important for business.





Top comments (0)