Search code examples
clldb

Address of local variable gives invalid address


I'm running into a bug in my code where the address of a local stack variable is invalid. I've been trying to debug this with lldb.

In the lldb prompt below, you can see that &dominance_frontier gives the address 0x0000000000000001 which then causes a SIGSEGV on hash_table_init call line 121. However &dominator_tree_adj, gives a valid address. I'm completely baffled as to why this might be the case.

Process 70998 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x0000000100006fd0 ir`ComputeDominanceFrontier(function=0x00006000030f8000) at dominators.c:121:9
   111          struct Array postorder_traversal = postorder (function->entry_basic_block);
   112          struct DFAConfiguration config = DominatorDFAConfiguration (function);
   113          struct DFAResult result = run_DFA (&config, function);
   114  
   115          HashTable dominator_tree_adj = ComputeDominatorTree (function, &result);
   116          printf("%p\n", &dominator_tree_adj);
   117          HashTable dominance_frontier;
   118          printf("%ld\n", sizeof(dominance_frontier));
   119  
   120  
-> 121          hash_table_init (&dominance_frontier);
   122          // Compute the transpose graph from the dominator tree adjacency list
   123          // Each node is guaranteed to have only one direct predecessor, since
   124          // each node can only have one immediate dominator. We will need this
   125          // in the DF algorithm below
   126          HashTable dominator_tree_transpose;
   127          hash_table_init (&dominator_tree_transpose);
   128  
   129          struct HashTableEntry *entry;
   130          size_t entry_iter = 0;
   131  
Target 0: (ir) stopped.
(lldb) p &dominance_frontier
(HashTable *) 0x0000000000000001
(lldb) p &dominator_tree_adj
(HashTable *) 0x000000016fdfef38

I've been compiling my code with make DEBUG=yes

OPT = -O3
FLAGS = -Wall -Wextra
CC = cc
OBJECTS =   ir_parser.o \
            main.o \
            threeaddr_parser.o \
            instruction.o \
            function.o \
            basicblock.o \
            constant.o \
            utils.o \
            value.o \
            array.o \
            mem.o \
            map.o \
            dfa.o \
            dominators.o


ifdef DEBUG
    OPT = -g
else
    OPT = -O3
endif


all: $(OBJECTS)
    $(CC) $(OPT) $(FLAGS) $^ -o ir

%.o: %.c *.h
    $(CC) $(OPT) $(FLAGS) -c $< -o $@


clean:
    rm *.o

This is the location of the segfault:

Process 10619 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x9)
    frame #0: 0x0000000100005060 ir`_hash_table_init(table=0x0000000000000001, size=10) at map.c:21:21
   11   // Open addressing, linear probe hash table.
   12   
   13   unsigned long uint64_t_hash_function (uint64_t key)
   14   {
   15           // Simple hash function for demonstration
   16           return key;
   17   }
   18   
   19   void _hash_table_init (HashTable *table, size_t size)
   20   {
-> 21           table->size = size;
   22           table->count = 0;
   23           table->buckets = ir_calloc (table->size, sizeof (HashTableEntry));
   24   }
   25   void hash_table_init (HashTable *table)
   26   {
   27           _hash_table_init (table, MAP_INIT_SIZE_CNT);
   28   }
   29   
   30   // Create a new hash table
   31   HashTable *hash_table_create (size_t size)
Target 0: (ir) stopped.

Input file:

fn test4(%1, %2) {

    alloca %9, 5
    add %3, %1, %2 
    store %9, %3
    cmp %4, %1, %2 
    jumpif 1, %4


    sub %5, %3, 1  
    jumpif 2, %5   

1:
    add %6, %1, 20 
    jump 3    


2:
    add %7, %2, 30 

    
3:
    sub %8, %3, %3 
}
./ir -f path/to/input/file

I'm using Apple clang version 15.0.0 (clang-1500.3.9.4)

Full code is here: https://github.com/CoconutJJ/compiler-optimization/blob/d3244e8c9f96e8180c924533abf8f5daac15238c/ir/dominators.c#L115


Solution

  • dominators.c

    HashTable ComputeDominanceFrontier (struct Function *function)
    ^^^^^^^^^
    

    dominators.h

    void ComputeDominanceFrontier (struct Function *function);
    ^^^^
    

    Oopsie.

    The function ComputeDominanceFrontier is called from main.c which includes dominators.h, so the caller thinks that it returns void. But according to the definition in dominators.c, it actually returns HashTable. This mismatch is the cause of the crash, as explained below.

    The compiler does not catch this because you did not include dominators.h into dominators.c, so when compiling dominators.c, it had no idea that some other source file was seeing a conflicting declaration. For this reason, when a header declares a global function, you should always make sure that header is included into the source file that defines the function. You can help enforce this with -Wmissing-prototypes, which gives a warning whenever a global function is defined without having previously been declared. See Compiler warning for function defined without prototype in scope?


    What happens is this: on arm64, like many other platforms, when a function returns a struct type that can't be returned in a register, it's returned by "hidden reference": the caller allocates space for the return value, and passes an extra hidden argument with the address of that space. On arm64 the extra argument is passed in register x8. Then the called function is responsible for copying its return value into that space.

    In this case, since main didn't know that ComputeDominanceFrontier was returning a struct type, it didn't allocate such space, and left x8 containing garbage (in your case, its value happened to be 1).

    If compiled naively, you would then expect to see a crash at the end of the function, when dominance_frontier is copied from the stack into the bogus return value address. And it sounds like that's what you did see in your Ubuntu test (which, let me guess, used gcc instead of clang?).

    However, the compiler can optimize this: instead of using stack space for dominance_frontier, use the return value space allocated by the caller. Then we don't need to copy that data before returning, because it was populated "in place". It appears that clang performs this optimization even when optimizations are "off". So as such, dominance_frontier doesn't really live on ComputeDominanceFrontier's stack frame, and &dominance_frontier isn't a stack address but rather the return value address passed by the caller - which in this case is garbage.