Why I Learned to Love Undefined Behavior in C++

Erich Keane
C++ Compiler Engineer
Erich.Keane@intel.com

Disclaimer

The views, thoughts, and opinions expressed in this presentation belong solely to the author and do not represent the author's employer, organization, or any other group/individual in any fashion.

Erich Keane

  • BS in CS, Wentworth Institute of Technology (2007)
  • Joined Intel in 2007
    • Technology Mfg Group (TMG): Burn in Lab Tool Developer (C#)
    • Client Computing Group(CCG): Platform Product Developer (C#)
    • Wireless Product Division (WPRD): IOT Developer (C/C++)
    • Software Solutions Group: Compiler Front-End Developer
  • Member/Contributor to ISO WG21/PL22.16: International C++ Committee

My C++ Career

  • Learned in 2002 in a High-School AP-CS Class
  • 2004: 100-Level CS Class
  • 2012: Joined WPRD, first Professional C++ Class
    • Coworker introduced Scott Meyers, Herb Sutter, Andrew Koenig Books (Modern C++!)
  • 2016: Joined Intel Compiler Team as a Front-End Engineer
  • 2016: Became Intel's Alternate representative at ISO C++ Committee Meetings

What is Undefined Behavior?

  • Wikipedia: undefined behavior (UB) is the result of executing computer code whose behavior is not prescribed by the language specification to which the code adheres, for the current state of the program
  • Runtime Behaviors that are assumed to never happen, compiler can ignore these situations and still emit valid code
  • Usually logic-errors
  • Executing UB can result in the program doing ANYTHING.
  • Compiler often will erase conditions/code blocks/etc if it can only be reached by UB.
  • Compiler often will erase conditions/code blocks/etc if executing it WILL cause UB.

Why Undefined Behavior?

  • Differing behavior between Processors, don't want to unjustly punish one's behavior.
    • Null Pointer Dereference, various processor behaviors:
      • Allow access to junk data
      • Interrupt
      • Lock-up Machine
    • Typical Overflow Processor Behaviors:
      • Wrap (2's Complement, 1's Complement, BCD?)
      • Trap
      • Seg-Fault
    • Still an issue today: ARM/x86/PPC all implement simple things differently.
  • Save compiler from having to emit checks that pass 99.9%+ of the time.  Results in faster programs.

UB => Faster Code (Examples)

  • Access Uninitialized Data
  • Null-Pointer Dereference/Array out of Bounds
  • Signed Overflow
  • Infinite Loops
  • Advanced Example: "Erase My Harddrive"

Note: All Code examples from Clang 7.0 Branch (svn commit 328076) and generated on Godbolt.org

Access Uninitialized Data

void foo() {
  int i;  // uninitialized
  // Perhaps other code here...
  // This check is UB, no way of 
  // telling what 'i's value is.
  if (i == 7) 
    do_thing();
}
  • C/C++ Do not guarantee initialization of values, thus accessing them is UB.
  • Initializing memory can be
    expensive, particularly when
    done a lot.
// Assumes 'i = 0' above.
foo(): # @foo()
  push rbp
  mov rbp, rsp
  sub rsp, 16
  mov dword ptr [rbp - 4], 0
  cmp dword ptr [rbp - 4], 7
  jne .LBB0_2
  call do_thing()
.LBB0_2:
  add rsp, 16
  pop rbp
  ret
// As above.
foo(): # @foo()
  push rbp
  mov rbp, rsp
  sub rsp, 16
// SAVED INSTRUCTION!
  cmp dword ptr [rbp - 4], 7
  jne .LBB0_2
  call do_thing()
.LBB0_2:
  add rsp, 16
  pop rbp
  ret

Null-Pointer Deref/Array OOB

  • Null-Pointer dereference and out-of-bounds array access are undefined in C/C++.  Both are REALLY expensive to check.
// C++ Dereference:
int i = object->getInt();
double k = object->getDouble();

// With checking (psuedo code, Java-like)
if (object == nullptr)
  throw new NullPointerException("object");
int i = object->getInt();
// MUST check again, above could have 
// invalidated the pointer.
if (object == nullptr)
  throw new NullPointerException("object");
double k = object->getDouble();
// C/C++ Array syntax:
auto a = SomeArray[i];

// With Checking: Size now has to be stored!
if (i >= SomeArray.size())
  throw new IndexOutOfBoundsException(
                        SomeArray, i);
auto a = SomeArray[i];

// An example of OOB Opt in Practice:
// http://en.cppreference.com/w/cpp/language/ub
int table[4] = {};
bool exists_in_table(int v)
{
  // return true in one of the first 4 iters
  // or UB due to out-of-bounds access
  for (int i = 0; i <= 4; i++)
    if (table[i] == v) return true;
  return false;
}
// Can be compiled to: 
exists_in_table(int):
        movl    $1, %eax
        ret

Signed Overflow

  • C/C++ do not define signed integer overflow.  This permits trip-count calculations (loop unrolling) as well as other assumptions.
bool bzip(uint32_t i1,uint32_t i2,
        unsigned char *data) {
  unsigned char c1,c2;
  c1 = data[i1];c2=data[i2];
  if (c1 != c2) return c1 > c2;   
  i1++;i2++; 
  c1 = data[i1];c2=data[i2];
  if (c1 != c2) return c1 > c2;   
  i1++;i2++;
  /// continues...
}
// Unsigned
mov cl, byte ptr [rdx + rcx]
  cmp byte ptr [rdx + rax], cl
  jne .LBB0_5
  lea eax, [rdi + 1]
  lea ecx, [rsi + 1]
  mov cl, byte ptr [rdx + rcx]
  cmp byte ptr [rdx + rax], cl
  jne .LBB0_5
  lea eax, [rdi + 2]
  lea ecx, [rsi + 2]
  mov cl, byte ptr [rdx + rcx]
  cmp byte ptr [rdx + rax], cl
  jne .LBB0_5
// Signed
mov al, byte ptr [rdx + rcx]
  cmp byte ptr [rdx + r8], al
  jne .LBB0_5


  mov al, byte ptr [rdx + rcx + 1]
  cmp byte ptr [rdx + r8 + 1], al
  jne .LBB0_5


  mov al, byte ptr [rdx + rcx + 2]
  cmp byte ptr [rdx + r8 + 2], al
  jne .LBB0_5

Infinite Loops are UB

  • C Committee N1528 "Why undefined behavior for infinite loops?"
  • Exclusively to permit optimizations and parallelization.
// From N1509:
for (p = q; p != 0; p = p -> next)
    ++count;
for (p = q; p != 0; p = p -> next)
    ++count2;
// Could be optimized as:
for (p = q; p != 0; p = p -> next) {
        ++count;
        ++count2;
}

Advanced Example: rm -rf /

#include <cstdlib>
typedef int (*Function)();
static Function Do;
static int EraseAll() {
  return system("rm -rf /");
}
void NeverCalled() {
  Do = EraseAll;  
}
int main() {
  return Do();
}
NeverCalled():# @NeverCalled()
        retq
main:# @main
        movl    $.L.str, %edi
        jmp     system # TAILCALL
.L.str:
        .asciz  "rm -rf /"
  • A popular example from the internet.  "main" returns the result of calling a function pointer "Do". The ONLY time this variable can be set is to EraseAll(), by NeverCalled().
  • Since using an uninitialized (or even nullptr) function pointer is UB, and we assume UB doesn't happen, SOMEONE ELSE must have called "NeverCalled", thus "Do" is system("rm -rf /");

Q/A

Erich.Keane@intel.com
Made with Slides.com