assemblygdbarm64stack-frameframe-pointer

Arm64 Assembly: How to properly manage frame pointer?


I implemented recursive fibonacci as an exercise, and the program seems to work perfectly except for one thing: when stepping through the function with gdb, the "backtrace" command just doesn't work

Here's the source

# fib.s
.data

.text
.globl fib
fib:
stp x29, x30, [sp, #-16]!
mov x29, sp
# Recursive compute fib(n). Even though this is a really dumb way to do Fibonacci,
# this exercise is practically mandatory
# x0: uint64_t -> Result in x0
mov x1, #1
cmp x0, x1
ble root
# Gotta do some recursive shenanigans
stp x19, x20, [sp, #-16]!
sub x0, x0, 1
mov x19, x0
bl fib
mov x20, x0
mov x0, x19
sub x0, x0, 1
bl fib
add x0, x0, x20
ldp x19, x20, [sp], #16
root:
ldp x29, x30, [sp], #16
ret
# main.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

extern uint64_t fib(uint64_t);

int main() {
        uint64_t i;
        printf("Enter a number please: ");
        if (scanf("%lu", &i) == EOF) {
                printf("HOLY MOLY That's not a number");
                exit(1);
        }
        printf("Fib(%lu) = %lu\n", i, fib(i));
        exit(0);
}

and steps to build

mkdir -p build
gcc -g -Wall -Wextra -c main.c -o build/ul5_1.o
as -g fib.s -o ul5_2.o
ld -lc --entry main build/ul5* -o build/l5.o

But when you start stepping through the code with gdbtui (provide 4 when the program asks for it), you'll see that bt only works as expected up until you hit that second stp. After that though, bt will start showing the following error

Backtrace stopped: Previous frame identical to this frame (corrupt stack?)

I thought fib conforms to AAPCS64, and I think the stack management is correct: so what am I missing/doing wrong?


Solution

  • Your code isn't wrong, but it sets up its stack frame differently from C-compiled code, and this is probably confusing gdb.

    You pushed x29, x30 to the stack at entry, and then later pushed the call-preserved registers x19, x20 below that. But the typical stack frame setup has x29, x30 at the bottom of the stack frame, and all local variables (including spilled registers) above that. This means in particular that the saved frame pointer can always be found at [sp], and the return address at [sp, #8], and it might be that gdb expects to see this.

    The advantage of this scheme is that it results in slightly more efficient code. It would look like

    fib:
        stp x29, x30, [sp, #-32]! // includes 16 bytes for locals
        mov x29, sp
        // more code
        stp x19, x20, [sp, #16]   // note, no writeback
        // code
        ldp x19, x20, [sp, #16]
        ldp x29, x30, [sp], #32
        ret
    

    This way, only the initial stp of x29, x30 has to use the more expensive writeback indexed store instruction. All other accesses to the stack frame don't need to adjust sp. In particular, they don't create a write dependency on sp, making it possible for them to execute concurrently out-of-order.

    If I change your code to use that scheme, then in my tests bt works as expected at all points in the code.

    I believe it is possible, using .cfi directives or similar, to annotate the code to inform a debugger (as well as throw/catch stack unwinding in languages where it exists) when sp points somewhere other than its usual location. This would be needed even in C-compiled code if, for instance, you called a function with a large number of arguments, such that they have to be passed on the stack. I'm not familiar with the details, but perhaps someone else is?