How to set up an ARM stack frame so GDB can traverse it?

2.1k views Asked by At

I'm doing a little project to use a Linux standard C library for ARM on bare metal (no OS). I'm using qemu-system-arm as the execution platform and GDB to debug. I have written a little system call handler to handle the SVC calls that the C library makes but am confused by the fact that my unhandled syscall function can't traverse the stack back to the caller even though the SVC handler can. The handler code is:

SVC_Handler:
    srsfd   sp!, #Mode_SYS      // Save LR_svc and SPSR_svc on the sys stack.
    cpsid   i, #Mode_SYS        // Switch to sys mode.
    push    {r4-r12, lr}        // Save registers.

    // In a system call.
    // r7 is the call number.
__in_syscall:                   // The stack frame is valid here.
    cmp     r7, #512
    blhs    Unhandled_SVC       // Jump if too big for a syscall.
    adr     r8, SVC_Table       // Get the system call table.
    str     r7, SysCall         // Save call number for error reporting.
    ldr     r7, [r8, r7, lsl #2]// Get the stystem call entry.
    blx     r7                  // Dispatch. Return value is in r0/r1
goback:
    pop     {r4-r12, lr}        // Restore registers.
    rfeia   sp!                 // And return.

SysCall:
    .word   0

// Unhandled system calls.
Unhandled_SVC:
    stmfd   sp!, {r12, lr}
    push    {r2-r5}                 // Push extra arguments.
    mov     r3, r1
    mov     r2, r0
    ldr     r1, SysCall             // And the system call number.
    ldr     r0, stringPtr           // Get the format string.
    bl      printf
    add     sp, #16                 // clean up the stack.

    mov     r0, #-ENOSYS       
    ldmfd   sp!, {r12, pc}

If I set a breakpoint at __in_syscall, I can see the stack frame just fine. If I enter Unhandled_SVC either via the branch or indirectly via a pointer in SVC_Table, GDB gets confused displaying the stack frame, even though the program executes correctly.

What am I missing?

This is part of my ELLCC embedded compiler project and the complete source is here.

1

There are 1 answers

2
artless noise On BEST ANSWER

tl;dr - probably you can not do what you want for your use case of a system call.

However, the following is useful for tracing ARM assembler that doesn't involve mode switching.


There are several gnu assembler or gas pseudo-ops which are used for stack tracing in assembler. That said, you can always create assembler that is beyond what a typical routine would be; for instance, a scheduler with context switching, etc.

  1. .fnstart - start of a function (text range)
  2. .fnend - end of a function (text range)
  3. .setfp - a location of a APCS stack frame.
  4. .save - a list of saved registers on stack.
  5. .pad - other reserved space on stack
  6. .movsp - incremented the stack.
  7. .cantunwind - don't attempt to unwind this function; when you went beyond normal.

Your current routine omits the pc register (for the SVC_Handler) routine and doesn't update the fp. This is fine, but you need to tell the debugger not to look at the arguments that could be in non-preserved registers. Specifically useful is the gas unwinding tutorial.

Your user sp and the system sp are different. So when you trace, it will only be in one stack. GDB won't know to jump modes and/or stack. One mechanism is to zero the frame pointer in the syscall entry so that it is a termination of the frame trace. You then need to setup a real stack frame in Unhandled_SVC. You would need to write a GDB macro to extract the kernel SVC call info and transition to the excepted stack, if you wanted to continue tracing.

See: ARM Link and frame pointer for some info on the ARM stack frame.