Debugging your { C #code }
Segmentation fault (core dumped)
That's the dreaded message the program crashed with, after hours of clacking the keyboard keys to build a custom BigNum library for C. The consequence? I had to spend another hour, dry-testing the code trying to pin-point the source of the fault. The crash, no doubt resulted from an illegal memory reference; this could mean anything from a NULL pointer access to an illegal array index to out of memory error. For source codes amounting to thousands of lines or more, hunting bugs is equivalent to finding the needle in a haystack. We could instead make our life much easier if we had some mechanism to track the source of such faults in C. The code excerpts in this article were compiled and tested with GCC. I'm not sure how compatible it is with other compilers, but I'm sure it wouldn't be a problem for other than some few pre-defined macros.
C++ has some help from its exception handling. Java does it better, thanks to its byte-code that retains most of the debugging information. Unfortunately, for C, its raw nature implies no such fancy features. Compilers come to the rescue, as C code can be compiled with the -g (GCC) or /D_DEBUG (MSVC) flags which build the executable with the debugging information, that can be later used with the MSVC debugger or Valgrind and GDB on any Linux platform. Do read on Valgrind and GDB if you're interested. No, I won't be discussing them here; I'd instead like to show you how to make your code fault-tolerant (to some extent) from the inside.
Most of the problems with C stem from improper handling of pointers, and can wreak havoc if used unwisely. Hence, you should be well versed with them, or else you're in for some nasty surprise. Let's start off by writing a header file which will include all the macros we'll use for debugging our code.
File: debug.h
#ifndef _DEBUG_H_
#define _DEBUG_H_
#include <stdio.h>
/* Logger macros */
#define LOG_ERR(S, ...) fprintf(stderr, "[%s (%d)] ERROR: " S "\n", __FILE__, __LINE__, __VA_ARGS__)
#define LOG_INFO(S, ...) fprintf(stdout, "[%s (%d)] INFO: " S "\n", __FILE__, __LINE__, __VA_ARGS__)
#define LOG_WARNING(S, ...) fprintf(stderr, "[%s (%d)] WARNING: " S "\n", __FILE__, __LINE__, __VA_ARGS__)
#endif
These are the basic logger macros we'll be using to log / print the debug info. The functional code is defined within a conditional directive, which in this case acts as an include guard. To keep it simple, include guards prevent the header files from being included more than once during compilation, which would result in errors such as “multiple definitions...”. The macros here are defined with two parameters, S basically being a string and '...' being variable arguments, also called varargs. If you're observant, you might have noticed that printf can be called with multiple arguments after the first argument. This is basically varargs behind the scenes. fprintf takes 3 arguments, the first being the stream to print the text to, the second being the text to be printed followed by varargs. The format specifiers %s and %d accept __FILE__ and __LINE__ as inputs respectively. Here, __FILE__, __LINE__ , and __VA_ARGS__ are pre-defined macros provided by GCC.
During compilation, __FILE__, __LINE__, and __VA_ARGS__ are replaced with the name of the file in which these macros are used, the line number and the list of arguments passed to the macros after S respectively. Within the format string itself, S has been included in-between two strings. S being a string, the compiler automatically concatenates the three strings into one. Take your time if it's overwhelming, then continue. Don't forget to check the links in the footnote.
File: debug.h (append after LOG_WARNING)
#define CHECK(A, L, M, ...) { if (!(A)) { LOG_ERR(M, __VA_ARGS__); goto L; } }
#define CHECK_PTR(P, L) CHECK((P), L, “Null pointer received for %s”, #P)
The macros above introduce condition and NULL pointer checks. On the first macro CHECK, the parameters are as follows:
A = The condition to be checked. If A is false, error is logged with the format string M;
L = The label to jump to, in case of an error. That is, when A is false;
M = The format string to be passed to LOG_ERROR to be printed;
... = Varargs for M;
The parameters on CHECK_PTR (alias for check pointer), are:
P = Pointer to checked for NULL. As in C, NULL is equivalent to 0, which in turn is equivalent to false;
L = The label to jump to in case of an error. That is, when P is NULL;
We'll soon see how these macros can be used to our advantage. CHECK_PTR internally calls CHECK with the given arguments. I hope all of it is self-explanatory, except the last argument #P. The # operator in a macro expansion is also called the stringification operator, in that, it converts any identifier to a string. For example, observe the
following code:
int _strlen(const char *str) {
CHECK_PTR(str, err_ptr);
int len = 0;
char *lptr = (char *) str;
/* Calculate len */
return len;
err_ptr: /* Label */
return 0;
}
The snippet above is a custom strlen function. Macros are expanded during compilation, hence, the CHECK_PTR above is expanded into:
CHECK((str), err_ptr, “Null pointer received for %s”, #str)
which further expands into:
{ if (!((str))) { LOG_ERR(“Null pointer received for %s”, “str”); goto err_ptr; } }
You can see how #str was converted into “str” internally. The statement above reads “if str is NULL, log error with the given message and then jump to the label 'err_ptr' ”. Hence, errors are checked early in the code, with debug info printed to the error stream. If _strlen was called with a NULL value, the following would be the output (assuming _strlen is defined in test.c in line 20):
[test.c (21)] ERROR: Null pointer received for str
Thus, now you know where the offending code resides, i.e. in line 21 of the file test.c. Well, that takes care of NULL pointers. We're left with array bounds now. Static arrays aren't a problem, but dynamic arrays (dynamically allocated, and not auto-resizable) can be tricky. To see why, read the code snippet below.
int arr[] = { 1, 2, 3, 4, 5 }; /* Static array of size 5 */
int *arr_ptr = (int *) malloc(5 * sizeof(int)); /* Dynamic array of size 5 */
/* Outputs 5 */
printf(“Number of elements in arr = %d\n”, (int)(sizeof(arr) / sizeof(*arr)));
/* Outputs 2, using a 64-bit compiler */
printf(“Number of elements in arr = %d\n”, (int)(sizeof(arr_ptr) / sizeof(*arr_ptr)));
It is obvious that the second printf prints the size of a pointer (8 bytes) divided by the size of an int (4 bytes). Thus one must be careful while calculating array sizes for arrays passed as pointers to functions. C simply can't calculate array sizes other than for static arrays. Hence, it becomes necessary to pass the array size along with an array to functions. Other tricks involve allocating memory for an extra element, usually the first, which would store the size.
Size can then be simply read from arr_ptr[0] or *arr_ptr. However, for the sake of completeness, let's write a rudimentary array bounds checker for static arrays.
File: debug.h
#define ARR_SZ(A) (sizeof(A) / sizeof(*A))
#define CHECK_URANGE(R, N, L, M, ...) CHECK((N) <= (R), L, M, __VA_ARGS__)
#define CHECK_ARR_BOUNDS(A, GS, L) CHECK_URANGE(ARR_SZ(A), GS, L, "Wrong array size for \"%s\", \
expected %d", #A, (int) ARR_SZ(A))
I'll be quick with this. CHECK_URANGE logs an error whenever N, the given size, exceeds R, the range upper bound.
Similarly, CHECK_ARR_BOUNDS logs an error if GS, the given size, exceeds the actual size of the static array. The given size here could be loop counter limit, or an array index. For indexes you must use CHECK_ARR_BOUNDS(arr, idx + 1, label).
It's time to finally test our debug macros.
File: test.c
#include <stdint.h>
#include <stdlib.h>
#include “debug.h”
void file_op(const char *file_name) {
CHECK_PTR(file_name, err_ptr);
FILE *fin = fopen(file_name, “r”);
CHECK(fin, err_file, “Unable to open file: %s”, file_name);
/* Do something with fin */
fclose(fin);
err_ptr:
err_file:
return;
}
void print_primes(uint64_t lim) {
uint8_t *arr = (uint8_t *) malloc(lim * sizeof(uint8_t));
CHECK_PTR(arr, err_ptr);
/* Generate prime-sieve */
/* Print primes */
free(arr);
err_ptr:
return;
}
int main(int argc, char *argv[]) {
/* Try to read from a non-existent file */
file_op(“abc.txt”);
/* Try to allocate an exceptionally large sieve */
print_primes(10000000000); // Should fail with 4GB RAM
return 0;
}
The program exits gracefully with the following output:
[test.c (9)] ERROR: Unable to open file: abc.txt
[test.c (21)] ERROR: Null pointer received for arr
Honestly, writing debug macros for small programs isn't really necessary, but it does make a difference on larger projects. Make it a habit to always validate your function arguments and return values. A little caution earlier on can prove to be a big time-saver later.
The other little known source of segmentation faults is from trying to modify a value in read-only memory location.
For example,
char stra[] = “Bats are not scary”;
char *strp = “Bats are not scary”;
stra[0] = 'C'; /* No problem */
strp[0] = 'C'; /* Error */
Whenever string literals are assigned to pointers, most compilers store the string in the read-only shared data segment, and assign the base address to the pointer. Thus, any attempt to modify the data would result in run-time errors.
It becomes even more confusing when you pass this pointer to a function that modifies it internally. In such cases, always create a duplicate to work on, and then return the modified string.
CHECK((str), err_ptr, “Null pointer received for %s”, #str)
which further expands into:
{ if (!((str))) { LOG_ERR(“Null pointer received for %s”, “str”); goto err_ptr; } }
You can see how #str was converted into “str” internally. The statement above reads “if str is NULL, log error with the given message and then jump to the label 'err_ptr' ”. Hence, errors are checked early in the code, with debug info printed to the error stream. If _strlen was called with a NULL value, the following would be the output (assuming _strlen is defined in test.c in line 20):
[test.c (21)] ERROR: Null pointer received for str
Thus, now you know where the offending code resides, i.e. in line 21 of the file test.c. Well, that takes care of NULL pointers. We're left with array bounds now. Static arrays aren't a problem, but dynamic arrays (dynamically allocated, and not auto-resizable) can be tricky. To see why, read the code snippet below.
int arr[] = { 1, 2, 3, 4, 5 }; /* Static array of size 5 */
int *arr_ptr = (int *) malloc(5 * sizeof(int)); /* Dynamic array of size 5 */
/* Outputs 5 */
printf(“Number of elements in arr = %d\n”, (int)(sizeof(arr) / sizeof(*arr)));
/* Outputs 2, using a 64-bit compiler */
printf(“Number of elements in arr = %d\n”, (int)(sizeof(arr_ptr) / sizeof(*arr_ptr)));
It is obvious that the second printf prints the size of a pointer (8 bytes) divided by the size of an int (4 bytes). Thus one must be careful while calculating array sizes for arrays passed as pointers to functions. C simply can't calculate array sizes other than for static arrays. Hence, it becomes necessary to pass the array size along with an array to functions. Other tricks involve allocating memory for an extra element, usually the first, which would store the size.
Size can then be simply read from arr_ptr[0] or *arr_ptr. However, for the sake of completeness, let's write a rudimentary array bounds checker for static arrays.
File: debug.h
#define ARR_SZ(A) (sizeof(A) / sizeof(*A))
#define CHECK_URANGE(R, N, L, M, ...) CHECK((N) <= (R), L, M, __VA_ARGS__)
#define CHECK_ARR_BOUNDS(A, GS, L) CHECK_URANGE(ARR_SZ(A), GS, L, "Wrong array size for \"%s\", \
expected %d", #A, (int) ARR_SZ(A))
I'll be quick with this. CHECK_URANGE logs an error whenever N, the given size, exceeds R, the range upper bound.
Similarly, CHECK_ARR_BOUNDS logs an error if GS, the given size, exceeds the actual size of the static array. The given size here could be loop counter limit, or an array index. For indexes you must use CHECK_ARR_BOUNDS(arr, idx + 1, label).
It's time to finally test our debug macros.
File: test.c
#include <stdint.h>
#include <stdlib.h>
#include “debug.h”
void file_op(const char *file_name) {
CHECK_PTR(file_name, err_ptr);
FILE *fin = fopen(file_name, “r”);
CHECK(fin, err_file, “Unable to open file: %s”, file_name);
/* Do something with fin */
fclose(fin);
err_ptr:
err_file:
return;
}
void print_primes(uint64_t lim) {
uint8_t *arr = (uint8_t *) malloc(lim * sizeof(uint8_t));
CHECK_PTR(arr, err_ptr);
/* Generate prime-sieve */
/* Print primes */
free(arr);
err_ptr:
return;
}
int main(int argc, char *argv[]) {
/* Try to read from a non-existent file */
file_op(“abc.txt”);
/* Try to allocate an exceptionally large sieve */
print_primes(10000000000); // Should fail with 4GB RAM
return 0;
}
The program exits gracefully with the following output:
[test.c (9)] ERROR: Unable to open file: abc.txt
[test.c (21)] ERROR: Null pointer received for arr
Honestly, writing debug macros for small programs isn't really necessary, but it does make a difference on larger projects. Make it a habit to always validate your function arguments and return values. A little caution earlier on can prove to be a big time-saver later.
The other little known source of segmentation faults is from trying to modify a value in read-only memory location.
For example,
char stra[] = “Bats are not scary”;
char *strp = “Bats are not scary”;
stra[0] = 'C'; /* No problem */
strp[0] = 'C'; /* Error */
Whenever string literals are assigned to pointers, most compilers store the string in the read-only shared data segment, and assign the base address to the pointer. Thus, any attempt to modify the data would result in run-time errors.
It becomes even more confusing when you pass this pointer to a function that modifies it internally. In such cases, always create a duplicate to work on, and then return the modified string.

Comments
Post a Comment