C/C++ Stack & Heap Checking with gcc

Stack, heap, and memory issues in general are a chronic problem with C/C++.

For Linux, there’s Valgrind, which is a truly useful tool. But it requires that one explicitly run the application under development under Valgrind. And we were experiencing a segmentation fault issue that Valgrind wasn’t identifying. (Nobody’s perfect!)

It turns out that gcc itself includes debugging support for this class of issues. One huge advantage of these sorts of ‘built in tests’ (also true of assert()s) is that you get ‘free’ testing every time you run a debug build. It’s practically free testing! Who doesn’t want THAT?

Here’s a list of these options (not entirely mutually exclusive). To use, say, -fmydebugoption you would compile with

gcc -fmydebugoption main.c -o main

-fstack-check

If the two macros associated with this feature, namely STACK_CHECK_BUILTIN and STACK_CHECK_STATIC_BUILTIN are left at the default 0, this option inserts a NULL byte every 4kb (page) when the stack grows. By default only one 4K page, but when the stack can grow more than one page (the most dangerous case), every 4KB. See https://gcc.gnu.org/onlinedocs/gcc-4.8.4/gccint/Stack-Checking.html for details.

-fstack-protector

A family of checks involving the same concept, with different degrees of thoroughness/performance-cost. The idea is to put a ‘canary’ — a randomly generated number/signature at the end of each function call, and check that this canary is uncorrupted when the function exits. That is, if a function overwrites the end of a local array (of any type), it will also overwrite the ‘canary’ and when the function exits this can be checked, and the program aborted if necessary.

  • fstack-protector: Add a ‘canary’ (see ‘-fstack-protector’) to functions that call malloc/calloc/etc, and functions with buffers larger than 8 bytes.
  • fstack-protector-all: Add a ‘canary’ to every function (whether it has local arrays or memory allocations).
  • fstack-protector-string: Add a canary under any of the following circumstances:
    • Local variable‚Äôs address used as part of the right hand side of an assignment or function argument
    • Local variable is an array (or union containing an array), regardless of array type or length
    • Uses register local variables

    -finstrument-functions

    Generate instrumentation calls for entry and exit to functions. Just after function entry and just before function exit, the following profiling functions are called with the address of the current function and its call site. (On some platforms, __builtin_return_address does not work beyond the current function, so the call site information may not be available to the profiling functions otherwise.)


    void __cyg_profile_func_enter (void *this_fn,
    void *call_site);
    void __cyg_profile_func_exit (void *this_fn,
    void *call_site);

    The first argument is the address of the start of the current function, which may be looked up exactly in the symbol table.

    -fsanitize=address

    Enable AddressSanitizer, a fast memory error detector. Memory access instructions are instrumented to detect out-of-bounds and use-after-free bugs. The option enables -fsanitize-address-use-after-scope. See https://github.com/google/sanitizers/wiki/AddressSanitizer for more details. The option cannot be combined with -fsanitize=thread.

    -fsanitize=leak

    Enable LeakSanitizer, a memory leak detector. This option only matters for linking of executables and the executable is linked against a library that overrides malloc and other allocator functions. See https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer for more details. The run-time behavior can be influenced using the LSAN_OPTIONS environment variable. The option cannot be combined with -fsanitize=thread.

    -fsanitize=undefined

    A family of functions enabled by ‘sanitize=undefined’. Options include (not a complete list):

    • -fsanitize=integer-divide-by-zero:
      Detect integer division by zero as well as INT_MIN / -1 division.
    • -fsanitize=vla-bound
      This option instructs the compiler to check that the size of a variable length array is positive.

      For example:

      void fun(int n)
      {
      int arr[n];
      // ......
      }
      int main()
      {
      fun(6);
      }

    • -fsanitize=null
      This option enables pointer checking. Particularly, the application built with this option turned on will issue an error message when it tries to dereference a NULL pointer, or if a reference (possibly an rvalue reference) is bound to a NULL pointer, or if a method is invoked on an object pointed by a NULL pointer.
    • -fsanitize=return
      This option enables return statement checking. Programs built with this option turned on will issue an error message when the end of a non-void function is reached without actually returning a value. This option works in C++ only.
    • -fsanitize=signed-integer-overflow
      This option enables signed integer overflow checking. We check that the result of +, *, and both unary and binary – does not overflow in the signed arithmetics. Note, integer promotion rules must be taken into account. That is, the following is not an overflow:

      signed char a = SCHAR_MAX;
      a++;
    • -fsanitize=bounds
      This option enables instrumentation of array bounds. Various out of bounds accesses are detected. Flexible array members, flexible array member-like arrays, and initializers of variables with static storage are not instrumented.
    • -fsanitize=bounds-strict
      This option enables strict instrumentation of array bounds. Most out of bounds accesses are detected, including flexible array members and flexible array member-like arrays. Initializers of variables with static storage are not instrumented.
    • -fsanitize=object-size
      This option enables instrumentation of memory references using the __builtin_object_size function. Various out of bounds pointer accesses are detected.
    • -fsanitize=float-divide-by-zero
      Detect floating-point division by zero. Unlike other similar options, -fsanitize=float-divide-by-zero is not enabled by -fsanitize=undefined, since floating-point division by zero can be a legitimate way of obtaining infinities and NaNs.
    • -fsanitize=float-cast-overflow
      This option enables floating-point type to integer conversion checking. Check that the result of the conversion does not overflow. Unlike other similar options, -fsanitize=float-cast-overflow is not enabled by -fsanitize=undefined. This option does not work well with FE_INVALID exceptions enabled.
    • fsanitize=bool
      This option enables instrumentation of loads from bool. If a value other than 0/1 is loaded, a run-time error is issued.

    • -fsanitize=enum
      This option enables instrumentation of loads from an enum type. If a value outside the range of values for the enum type is loaded, a run-time error is issued.
    • -fsanitize=vptr
      This option enables instrumentation of C++ member function calls, member accesses and some conversions between pointers to base and derived classes, to verify the referenced object has the correct dynamic type.
    • -fsanitize=pointer-overflow
      This option enables instrumentation of pointer arithmetics. If the pointer arithmetics overflows, a run-time error is issued.

    -fno-omit-frame-pointer

    Generally speaking makes life easier for debuggers by putting frame pointers in a CPU register.

    Based on the above, I’m currently using


    gcc ... -fstack-check -stack-protector-strong -finstrument-functions -fsanitize=address -fsanitize=leak -fsanitize-undefined ...

Leave a Reply

Your email address will not be published. Required fields are marked *