rockbox/lib/mipsunwinder/backtrace-mips32.c
Aidan MacDonald 981e972839 mips: add native backtrace implementation
Should make debugging crashes on native MIPS targets far easier.
This is by no means a 100% complete or robust implementation but
it seems to handle the vast majority of functions.

Change-Id: Id5f430270e02b5092b79026b6876675c784aa649
2022-05-22 07:16:11 -04:00

236 lines
6.3 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* MIPS32 backtrace implementation
* Copyright (C) 2022 Aidan MacDonald
*
* References:
* https://yosefk.com/blog/getting-the-call-stack-without-a-frame-pointer.html
* https://elinux.org/images/6/68/ELC2008_-_Back-tracing_in_MIPS-based_Linux_Systems.pdf
* System V ABI MIPS Supplement, 3rd edition
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include "backtrace.h"
#include "lcd.h"
#include <stdint.h>
#include <stdbool.h>
#include <stdalign.h>
#include <stdarg.h>
#include <stdio.h>
#define MIN_ADDR 0x80000000ul
#define MAX_ADDR (MIN_ADDR + (MEMORYSIZE * 1024 * 1024) - 1)
static bool read_check(const void* addr, size_t alignment)
{
if(addr < (const void*)MIN_ADDR ||
addr > (const void*)MAX_ADDR)
return false;
if((uintptr_t)addr & (alignment - 1))
return false;
return true;
}
static bool read32(const void* addr, uint32_t* val)
{
if(!read_check(addr, alignof(uint32_t)))
return false;
*val = *(const uint32_t*)addr;
return true;
}
#if 0
static bool read16(const void* addr, uint16_t* val)
{
if(!read_check(addr, alignof(uint16_t)))
return false;
*val = *(const uint16_t*)addr;
return true;
}
static bool read8(const void* addr, uint8_t* val)
{
if(!read_check(addr, alignof(uint8_t)))
return false;
*val = *(const uint8_t*)addr;
return true;
}
#endif
static long extract_s16(uint32_t val)
{
#if 1
/* not ISO C, but gets GCC to emit 'seh' which is more compact */
return (int32_t)(val << 16) >> 16;
#else
val &= 0xffff;
if(val > 0x7fff)
return (long)val - 0x10000l;
else
return val;
#endif
}
/* TODO - cases not handled by the backtrace algorithm
*
* 1. functions that save the frame pointer will not be handled correctly
* (need to implement the algorithm specified by the System V ABI).
*
* 2. GCC can generate functions with a "false" stack pointer decrement,
* for some examples see read_bmp_fd and walk_path. those functions
* seem to be more difficult to deal with and the SysV algorithm will
* also get confused by them, but they are not common.
*/
int mips_bt_step(struct mips_bt_context* ctx)
{
/* go backward and look for the stack pointer decrement */
uint32_t* pc = ctx->pc;
uint32_t insn;
long sp_off;
while(true) {
if(!read32(pc, &insn))
return 0;
/* addiu sp, sp, sp_off */
if((insn >> 16) == 0x27bd) {
sp_off = extract_s16(insn);
if(sp_off < 0)
break;
}
/* jr ra */
if(insn == 0x03e00008) {
/* end if this is not a leaf or we lack register info */
if(ctx->depth > 0 || !(ctx->valid & (1 << MIPSBT_RA))) {
mips_bt_debug(ctx, "unexpected leaf function");
return 0;
}
/* this is a leaf function - ra contains the return address
* and sp is unchanged */
ctx->pc = (void*)ctx->reg[MIPSBT_RA] - 8;
ctx->depth++;
return 1;
}
--pc;
}
mips_bt_debug(ctx, "found sp_off=%ld at %p", sp_off, pc);
/* now go forward and find the saved return address */
while((void*)pc < ctx->pc) {
if(!read32(pc, &insn))
return 0;
/* sw ra, ra_off(sp) */
if((insn >> 16) == 0xafbf) {
long ra_off = extract_s16(insn);
uint32_t save_ra;
/* load the saved return address */
mips_bt_debug(ctx, "load ra from %p+%ld", ctx->sp, ra_off);
if(!read32(ctx->sp + ra_off, &save_ra))
return 0;
if(save_ra == 0) {
mips_bt_debug("hit root");
return 0;
}
/* update bt context */
ctx->pc = (void*)save_ra - 8;
ctx->sp -= sp_off;
/* update saved register info */
ctx->reg[MIPSBT_RA] = save_ra;
ctx->valid |= (1 << MIPSBT_RA);
ctx->depth++;
return 1;
}
++pc;
}
/* sometimes an exception occurs before ra is saved - in this case
* the ra register should contain the caller PC, but we still need
* to adjust the stack frame. */
if(ctx->depth == 0 && (ctx->valid & (1 << MIPSBT_RA))) {
ctx->pc = (void*)ctx->reg[MIPSBT_RA] - 8;
ctx->sp -= sp_off;
ctx->depth++;
return 1;
}
mips_bt_debug(ctx, "ra not found");
return 0;
}
#ifdef MIPSUNWINDER_DEBUG
static void rb_backtrace_debugf(void* arg, const char* fmt, ...)
{
static char buf[64];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
unsigned int* line = arg;
lcd_putsf(4, (*line)++, "%s", buf);
lcd_update();
}
#endif
void rb_backtrace_ctx(void* arg, unsigned* line)
{
struct mips_bt_context* ctx = arg;
#ifdef MIPSUNWINDER_DEBUG
ctx->debugf = rb_backtrace_debugf;
ctx->debug_arg = line;
#endif
do {
lcd_putsf(0, (*line)++, "%02d pc:%08lx sp:%08lx",
ctx->depth, (unsigned long)ctx->pc, (unsigned long)ctx->sp);
lcd_update();
} while(mips_bt_step(ctx));
lcd_puts(0, (*line)++, "bt end");
lcd_update();
}
void rb_backtrace(int pcAddr, int spAddr, unsigned* line)
{
(void)pcAddr;
(void)spAddr;
struct mips_bt_context ctx;
mips_bt_start(&ctx);
mips_bt_step(&ctx); /* step over this function */
rb_backtrace_ctx(&ctx, line);
}