Writing Robust Embedded-C Code
(Author: Rohit Sivakumar)
1.
Always use snprintf instead of
*sprintf. Using
sprintf causes buffer overruns if the destination size is less than the source
size resulting in segmentation faults due to buffer overflows and missed
‘\0’ (NULL) character termination.
char arr[3];
sprintf(arr,
"%s:%s", "Test", b);
//causes segmentation fault
*Same point holds true for some string
function and hence use strncpy and strncat
2.
When using snprintf, ensure to
force-terminate the end of buffer by a NULL termination.
char arr[4];
//causes arr to
hold smaller string without NULL termination causing faults
snprintf(arr,
sizeof(arr), "%s:%s", "Test", “1” );
INSTEAD use it as:
char arr[4] ;
snprintf(arr,
sizeof(arr), "%s:%s", "Test", “1” );
arr[sizeof(arr)-1]
= ‘\0’; //Force-terminate the last char with a NULL.
3.
Never leave dangling pointers in your
code. After you
finish using a dynamic pointer, always free it and assign it to NULL to avoid
accidental freeing up again.
Remember: You never free up a
pointer on stack; you free it up only on Heap.
char * p;
p=
(char*)malloc(sizeof(char));
.... //some use of arr dynamic pointer
free(p) ; //usage of dynamic pointer is over so
freeing up
p= NULL; //Always
assign the freed pointer to NULL.
//else-where in
the code trying to again accidentally do free(p)
free(p); //Ensures freeing up p again won’t crash
the system.
Accessing
a pointer to a deleted object results in an access violation exception and
hence assigning it to NULL ensures full safety to such a situation.
4.
Never forget the NULL character when
assuming string array sizes or when computing the length of the string.
char arr[]=
“Rohit”;
n = strlen(arr);
//returns only 5 by excluding the terminating NULL char.
5.
Never use non-reentrant functions
(generally string lib functions like strtok(...) in a recursive looping
constructs like ISRs. A non-reentrant function remembers
a local copy of its action. Hence when non-reentrant functions are again called
in ISRs, it will lose the correctness of its operation.
6.
Always use correct data-types when
dealing with signed and unsigned specifiers . A signed number range is different
from an unsigned range.
eg: signed
char range is -127 to +128
unsigned char range is: 0 to 255.
Remember: 0 to 255 with 0 inclusive in count
is 256. 0xFF is not 256.
Unsigned
integers produce faster C code.
7.
Correct way to initialize all the
elements of structure to zero (0) is:
Correct method:
struct strSchool = {{0}};
Incorrect method:
struct strSchool;
memset(&strSchool,
0, sizeof(strSchool));
8.
Ensure there are no left-overs in
your code. This means:
a. Your code must be clean of warnings
b. Your code must be well-commented
c. Your code must be free-of commented/non-used variables/c-code.
d. If you need code that is to included
for debugging then don’t leave it commented in the release mode instead use
#define DEBUG preprocessor.
9.
If your variables are global and
shared between threads or ISRs or main and ISRs, ensure to use the specifier
“volatile”.
10.
Always initialize a local variable to avoid
program crashes. It is optional for a global variable to remain uninitialized
as it is automatically equated to zero.
int m; //if m was global then it
is automatically equated to 0. So m=0;
int m; //if m was local then it
has garbage value unless initialized.
11.
When working with dynamic pointers, always
perform a NULL check.
int * pInt;
pInt = (int*) malloc(sizeof(int)*10);
if(pInt != NULL)
{
. . .
//do what your business logic
needs.
}
else
{
//we have an error terminate
from here.
}
12.
It is wrong to assume that a return
value of a bool function(...) is always true! It is a common mistake the
programmers do to assume that the function always returns true and never check
the return value before proceeding to the next step whose outcome is dependent
on the return value of the called bool function.
13.
Use assert(...) very carefully. assert(...) is used in debug mode for
checking pre-conditions and post-conditions. When a program is compiled in
release mode, asserts are removed in the pre-processing stage.
So it is incorrect to do this:
int * pInt;
pInt = (int*) malloc(sizeof(int));
assert(pInt
!= NULL); //Available in debug mode only.
pInt = 10;
....
Correct
way to do this:
int * pInt;
pInt = (int*) malloc(sizeof(int));
assert(pInt
!= NULL); //This code gets removed in release.
if(pInt == NULL)
return ERROR
else
pInt = 10;
....
14.
If your variables and functions are
only to be used in one-specific file then ensure you add the specifier
“static” to limit its scope to that file.
a. You don’t want to leave things as
global. They may seem very good to be like that, accessible from any-where but
are the most common cause of errors in a robust code.
15.
Don’t use magic numbers as far as
possible.
a. Instead use const <data-type>
specifier if you want to fix the value as a constant and also take advantage of
compiler’s type-checking feature.
const int nLicenses = 50; //Fixed magic number with a name
b. If you want to only use a meaningful
name and not the advantage specified in a above then use preprocessor directive
: #define. If you are using #defines then ensure to name in CAPITAL for easy
readability.
#define
TOTAL_LICENSES (50) //Fixed magic number with a name
16.
Compare constants in if loop constructs by
keeping the constants on left-side to
ensure robustness and to free up from = vs == errors!
Eg: if(x=10) or
if (x==10) //both compile to be
correct.
//Did you mean to compare or equate?
Suggested
Method: if(10 == x)
//You can never equate a
variable to a number so compiler knows and lets you only compare equality.