Você está na página 1de 7

C Runtime Checks in Embedded

Systems Development
A White Paper

Markus Heiling
Software Development Engineer, C/C++ Compiler Development

Intended Audience: Software development engineers using C/C++ compilers.

Summary of Contents: This document discusses runtime checks for C applications with special
regard to the requirements of embedded systems development.

Introduction
Runtime checks are features provided by a C/C++ compiler and/or a C-Runtime Library to help the
developer track down problems at the source code line level. Runtime checks are performed when
the program is executed. Runtime checks should not be confused with static checks or analysis,
which is performed at compilation time. The checks and analysis performed by a C/C++ compiler
are a valid help in development, but they are not capable of solving the more complex, hidden
problems which can be easily introduced during an application’s implementation and coding phase.

Runtime checks are performed when an application is executed. To execute the application, the
C/C++ compiler must be able to implement the application code with a noticeable amount of
additional code. While this code will not be visible to the developer at the source code level, it will
increase the amount of binary code created and the application’s execution time. However, this
increase of precious resources, especially in regards to an embedded system, is only temporary.

1
Runtime checks are used only in the debugging phase of an application and are not recompiled
when the application is built for release. The increase of code size and execution time during the
debugging phase is a required tradeoff for the time used to debug the application and isolate the
problem.

Practice shows that a decrease in the time needed to find the problem is more important and cost-
effective than trying to debug an application which is built for release. When a developer must find
the problem location in a release application, the developer must implement the application code
manually. Manual implementation also changes the original application. In many cases, manual
implementation cannot be avoided. Since manual implementation cannot be avoided, the developer
can use the automatic implementation of the C/C++ compiler to cover more error checking
scenarios and transparently inject the application code, thus saving development time.

Code Implementation
This section explains how the C/C++ compiler implements the application code by examining a
small example.

The following code example performs a read access to the global array 'numbers' by using the
array index 'index' passed to the 'readNumber' function.

int numbers[10];
int readNumber(unsigned int index)
{
return numbers[index];
}

This code will function without flaws provided that the array index is within the valid range of 0-9.
If the 'readNumber' function is invoked with a value outside the range of 0-9, the code will have
unpredictable results. Depending on how the result of the 'readNumber' function is further used
within the application, this flaw may lead to a serious problem. It is important to note that such
flaws may pass manual and automatic testing of an application because of the nature of this flaw.
The result returned by 'readNumber' depends on the index value (assuming that it is out of range)
as well as the memory contents which are actually read. Invalid indexes may not be detected by test
suites. Invalid array indexes are detected by implemented code.

To help the developer find the problem, the C/C++ compiler analyzes and implements the code at
runtime to verify that the array index is valid.

The implemented code will appear as follows:


int readNumber(unsigned int index)
{
// START: RTC-Instrumentation inserted by the C/C++ Compiler
if (index >= sizeof(numbers)/sizeof(numbers[0]))
rtc_arrayAccessOutOfBounds(__FILE__, lineOfAccess);
// END: RTC-Instrumentation
return numbers[index];
}

2
Everything between the START and END comment marks is inserted by the C/C++ compiler.
Analyzing the code at compilation time reveals that the application wants to access an array
variable whose size is known. Thus the read access to that array can be implemented to secure the
access and trigger an error condition and output the corresponding line of source code.

Depending on the runtime system and the implementation of the 'rtc_arrayAccessOutOfBounds'


function, the error output can look like the following:
RTC: Array access out of bounds in file test.c, line 16

If the C/C++ compiler cannot perform the code implementation, the developer has to achieve the
same accurate result by manually adding similar code fragments. Adding similar code fragments
increases the source code base of the application and makes the code base more difficult to
maintain. Most importantly the C/C++ compiler automatically detects each statement and
expression in the C source code where this kind of implementation is required. This is a time
consuming task for the developer but relatively easy for the C/C++ compiler.

Conditions of Implementation
Beyond the check of array access bounds shown above, the following list of runtime checks
features can be used to ease problem tracking in an application.

‰ Divide by Zero. Detecting integer division by zero.

‰ NULL Pointer Dereference. Detecting a NULL pointer dereference.

‰ Array Bounds. Detection of array index out of array bounds.

‰ Stack Overflow. Detection of stack overflow.

‰ Stack Frame Corruption. Detection of damage of the stack frame of a function.

‰ Switch Statement. Detection that no case has been hit.

‰ Uninitialized Variable Usage. Use of uninitialized variables.

‰ Assignment Bounds. Storing a value into a bit field variable that is too small.

‰ Heap Checking. Allocation/deallocation of memory, heap block consistency checks, memory


leak detection.

Those features are explained in more detail in the following sections.

Zero Divisibility Check


The zero divisability check is done by inserting code that checks whether or not the divisor is not
zero. If the divisor is zero, an error reporting function is called from the runtime system.

The application code performs an integer division.

3
int div(int a, int b) {
return a / b;
}

The C/C++ compiler implements the application code to check that the divisor is not equal to zero.

int div(int a, int b) {


// START: RTC-Instrumentation
if (b == 0)
rtc_divByZero(__FILE__, lineOfDivision);
// END: RTC-Instrumentation
return a / b;
}

NULL Pointer Dereference Detection


The NULL pointer dereference check is done for pointer accesses with pointers where the base
address is not known. When the base address of the range a pointer is pointing to is known, the
array bounds check is done instead. The assumption is made that no object may be dynamically
allocated at the address zero. Thus, if a pointer has a zero value, the check will fail and an error
reporting the runtime system function is called.

The application dereferences a pointer, 'ptrNumbers', whose base object is not known to the
C/C++ compiler. Since the C/C++ compiler has no chance to determine the validity of a memory
range for a pointer, it does not use the "Array Bounds" runtime check. The C/C++ compiler
implements the code with a NULL-pointer check. The NULL-pointer check is part of the "Array
Bounds" check.

int readPtr(int *ptrNumbers)


{
return *ptrNumbers;
}

The implemented code is as follows:

int readPtr(int *ptrNumbers)


{
// START: RTC-Instrumentation
if (ptrNumbers == 0)
rtc_nullPtrAccess(__FILE__, lineOfReadAccess);
// END: RTC-Instrumentation
return *ptrNumbers;
}

Array Index Out of Array Bounds Detection


The array bounds check is done for pointers where the base address and size are known. The
C/C++ compiler detects access via an out of bounds pointer of the object.

4
The implementation of the code for this type of check is more complicated than the zero divisibility
or NULL pointer reference checks. For each pointer, a small local or global data structure is created,
which stores the base address and the size of the pointer’s object.

char array[12];
char *ptr = array;

For the object ‘array’ a data structure is not necessary since the ‘array’ variable is the object and
the C/C++ compiler knows the base address and the size. When a pointer assignment is done ('ptr
= array' in this example), the internal data structure for the pointer ‘ptr’ is created and initialized
with the base address and the size as follows:

ptr_start = &array;
ptr_size = 12;

When the pointer, ‘ptr,’ is dereferenced, it is verified that the pointer is within the range [ptr_start,
ptr_start+ptr_size]. If the pointer is not within this range, an error reporting function is called
from the runtime system.

As a side effect of this technique, the mechanism automatically detects NULL-pointer dereferences.
Thus there is no need to insert additional code to check explicitly for NULL pointers.

Stack Overflow and Stack Frame Corruption Detection


The stack overflows and stack corruption checks are performed through stack initialization at the
function’s entry by calling an appropriate function from the runtime system. This function checks
whether or not the stack pointer is within the valid bounds and initializes the stack. The stack
layout of a function which is implemented with this feature is as follows:

Pre stack frame verification block


Stack frame of the function
Post stack frame verification block

The pre and post stack frame verification blocks are filled with a test pattern which can be verified
with an according function from the runtime system. The most common stack corruptions occur
when overwriting stack frames within a function can be detected. The pattern used to fill these
blocks is the current value of the stack pointer at function entry. This will also point out problems
in function recursions which are using a corrupt stack frame.

The stack frame will be filled with a test pattern, such as 0xCC, which can be used to detect access
to uninitialized memory.

At function exit, another function from the runtime system is called. The function verifies that the
pre and post stack frame validation blocks were not modified within the function and the stack
pointer is the same as it was at the function’s entry point.

5
As a side effect, the stack frame corruption runtime check feature can be used to also perform the
stack overflow checking, thus no extra implementation code is required.

Case Detection within a Switch Statement


The check inserts a call to a function within the runtime system in the default case of a switch
statement when the implementation of a default case is not provided by the user. This displays a
warning when there is a switch statement where no case that fits the parameter is reached, when the
user does not provide implementation of a default case.

A switch statement with a missing 'default' clause is implemented as follows:

void foo(int i)
{
switch (i) {
case 1: /* do something */ break;
case 2: /* do something */ break;
// START: RTC-Instrumentation
default: rtc_unhandledSwitchCase(__FILE__, lineOfSwitch);
// END: RTC-Instrumentation
}
}

Use of Uninitialized Variables


This check instruments the code so that every write access to a variable sets a flag. The flag is
cleared when the variable gets into scope. The read access checks a flag that tells the runtime check
framework if the variable has already been used. If the variable is uninitialized when used, a call to
an error reporting function from the runtime system is performed.

Storing a Value into a Bit Field Variable that is too Small


When bit fields are used, the bit field access is implemented to detect if the value that assigned will
fit into the bit field. If the value is too large for the bit field, an error reporting function from the
runtime system is called.

Allocation/Deallocation of Memory, Heap Block Consistency Checks, Memory


Leak Detection
Heap checking is done by the runtime check support in the C runtime library. Every memory
allocation inserts a guard area around the allocated block of memory, and every memory
deallocation will check these guard areas for memory consistency. Additionally, there are functions
that can be used to make a snapshot of the current memory allocation layout and another function
which verifies that the current layout is congruent with this snapshot. This way an easy memory
leak detection feature can be implemented.

6
Conclusion
Using a C/C++ compiler capable of instrumenting code with additional runtime checks can
noticeably reduce the efforts of finding application errors during the development phase.
Additionally, this technique offers the possibility of verifying the application in the testing phase
for the correct use of resources and assumptions made in development phase.

Contact:
Mentor Graphics 739 North University Blvd.
Embedded Systems Division Mobile, Alabama 36608

Phone: 251.208.3400
Fax: 251.343.7074
Toll free: 800.468.6853
Email: embedded_info@mentor.com
Web: http://www.mentor.com/embedded

Você também pode gostar