For the MEGAphone, I wanted to make my software debugging on the MEGA65 easier. Stack back-traces are a great way to help debug errors, but we don't have gdb or lldb or anything like that on the MEGA65. m65dbg and related tools can help here, but they don't have an easy way to provide the complete call stack.
To solve this for my needs, I added function instrumentation using the following in my Makefile:
COPT_M65= -Iinclude -Isrc/telephony/mega65 -Isrc/mega65-libc/include
COMPILER=llvm
COMPILER_PATH=/usr/local/bin
CC= $(COMPILER_PATH)/mos-c64-clang -mcpu=mos45gs02 -Iinclude -Isrc/telephony/mega65 -Isrc/mega65-libc/include -DLLVM -fno-unroll-loops -ffunction-sections -fdata-sections -mllvm -inline-threshold=0 -fvisibility=hidden -Oz -Wall -Wextra -Wtype-limits
# Uncomment to include stacktraces on calls to fail()
CC+= -g -finstrument-functions -DWITH_BACKTRACE
LD= $(COMPILER_PATH)/ld.lld
CL= $(COMPILER_PATH)/mos-c64-clang -DLLVM -mcpu=mos45gs02
HELPERS= src/helper-llvm.c
LDFLAGS += -Wl,-Map,bin65/unicode-font-test.map
LDFLAGS += -Wl,-T,src/telephony/asserts.ld
# Uncomment to include stacktraces on calls to fail()
CC+= -g -finstrument-functions -DWITH_BACKTRACE
Then for the build target, I run it twice, first to generate a map file with the memory addresses of all the functions in it, and then generate a C structure with the address of each function and its name listed:
# For backtrace support we have to compile twice: Once to generate the map file, from which we
# can generate the function list, and then a second time, where we link that in.
bin65/unicode-font-test.llvm.prg: src/telephony/unicode-font-test.c $(NATIVE_TELEPHONY_COMMON)
mkdir -p bin65
rm -f src/telephony/mega65/function_table.c
echo "struct function_table function_table[]={}; int function_table_count=0;" > src/telephony/mega65/function_table.c
$(CC) -o bin65/unicode-font-test.llvm.prg -Iinclude -Isrc/mega65-libc/include src/telephony/unicode-font-test.c src/telephony/attr_tables.c src/telephony/helper-llvm.s src/telephony/mega65/hal.c src/telephony/mega65/hal_asm_llvm.s $(SRC_TELEPHONY_COMMON) $(SRC_MEGA65_LIBC_LLVM) $(LDFLAGS)
tools/function_table.py bin65/unicode-font-test.map src/telephony/mega65/function_table.c
$(CC) -o bin65/unicode-font-test.llvm.prg -Iinclude -Isrc/mega65-libc/include src/telephony/unicode-font-test.c src/telephony/attr_tables.c src/telephony/helper-llvm.s src/telephony/mega65/hal.c src/telephony/mega65/hal_asm_llvm.s $(SRC_TELEPHONY_COMMON) $(SRC_MEGA65_LIBC_LLVM) $(LDFLAGS)
The tool that generates the function list is fairly simple:
#!/usr/bin/env python3
import sys
import re
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} <mapfile> <output.c>")
sys.exit(1)
mapfile, outfile = sys.argv[1], sys.argv[2]
entries = []
in_text = False
with open(mapfile) as f:
for line in f:
if ".text" in line and line.strip().endswith(".text"):
in_text = True
continue
if ".rodata" in line:
break
if not in_text:
continue
# match lines like: " a7b a7b 196b 1 main"
m = re.match(r"\s*([0-9a-fA-F]+)\s+[0-9a-fA-F]+\s+[0-9a-fA-F]+\s+\d+\s+(\S+)$", line)
if m:
addr = int(m.group(1), 16)
name = m.group(2)
# skip synthetic names if you want
if name.startswith("bin") or name.endswith(".o:"):
continue
entries.append((addr, name))
with open(outfile, "w") as out:
out.write("/* auto-generated from map file */\n")
out.write("const struct function_table function_table[] = {\n")
for addr, name in entries:
out.write(f" {{ 0x{addr:04x}, \"{name}\" }},\n")
out.write("};\n")
out.write(f"const unsigned function_table_count = {len(entries)};\n")
Then in an include file, I have:
#ifdef WITH_BACKTRACE
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
#define fail(X) mega65_fail(__FILE__,__FUNCTION__,STR(__LINE__),X)
void mega65_fail(const char *file, const char *function, const char *line, unsigned char error_code);
#else
#define fail(X)
#endif
struct function_table {
const uint16_t addr;
const char *function;
};
#endif
The last bit of setup then is to have a C file that includes the function table and implements the helper functions:
#include "includes.h"
extern const unsigned char __stack;
#ifdef WITH_BACKTRACE
#include "function_table.c"
#endif
void dump_backtrace(void);
#ifdef WITH_BACKTRACE
__attribute__((no_instrument_function))
void mega65_uart_print(const char *s)
{
while(*s) {
asm volatile (
"sta $D643\n\t" // write A to the trap register
"clv" // must be the very next instruction
:
: "a"(*s) // put 'error_code' into A before the block
: "v", "memory" // CLV changes V; 'memory' blocks reordering across the I/O write
);
// Wait a bit between chars
for(char n=0;n<2;n++) {
asm volatile(
"ldx $D012\n"
"1:\n"
"cpx $D012\n"
"beq 1b\n"
:
:
: "x" // X is clobbered
);
}
s++;
}
}
__attribute__((no_instrument_function))
void mega65_uart_printhex(const unsigned char v)
{
char hex_str[3];
hex_str[0]=to_hex(v>>4);
hex_str[1]=to_hex(v&0xf);
hex_str[2]=0;
mega65_uart_print(&hex_str[0]);
}
__attribute__((no_instrument_function))
void mega65_uart_printptr(const void *v)
{
mega65_uart_print("0x");
mega65_uart_printhex(((unsigned int)v)>>8);
mega65_uart_printhex(((unsigned int)v));
}
__attribute__((no_instrument_function))
void mega65_fail(const char *file, const char *function, const char *line, unsigned char error_code)
{
POKE(0x0428,PEEK(0x02));
POKE(0x0429,PEEK(0x03));
mega65_uart_print(file);
mega65_uart_print(":");
mega65_uart_print(line);
mega65_uart_print(":");
mega65_uart_print(function);
mega65_uart_print("():0x");
mega65_uart_printhex(error_code);
mega65_uart_print("\n\r");
dump_backtrace();
while(PEEK(0xD610)) POKE(0xD610,0);
while(!PEEK(0xD610)) POKE(0xD021,PEEK(0xD012));
}
/*
Stack back-trace facility to help debug error locations.
*/
#define MAX_BT 32
struct frame { const void *site, *stack_pointer; };
static struct frame callstack[MAX_BT];
static uint8_t depth, sp;
__attribute__((no_instrument_function))
void __cyg_profile_func_enter(void) {
if (depth>=MAX_BT) depth--;
// Get SPL into sp variable declared above.
__asm__ volatile ("tsx" : "=x"(sp));
// Now convert that in
const uint8_t *stack_pointer = (void *)(0x0100 + sp);
void *call_site = (void *)((*((uint16_t *)&stack_pointer[1])) - 1);
callstack[depth] = (struct frame){ call_site, &__stack };
depth++;
}
__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void) {
if (depth) --depth; // simple, assumes well-nested calls
}
__attribute__((no_instrument_function))
void dump_backtrace(void) {
// For each frame, either:
// - print raw addresses, or
// - call your on-target addr2line() to print file:line + function
mega65_uart_print("Backtrace (most recent call first):\n\r");
unsigned char d= depth-1;
for(unsigned char d = depth-1;d!=0xff;d--) {
mega65_uart_print("[");
mega65_uart_printhex(d);
mega65_uart_print("] ");
// Find function in table
unsigned int func_num = 0;
while(func_num<(function_table_count-1) && function_table[func_num+1].addr < (uint16_t)callstack[d].site)
func_num++;
// Display offset from function
mega65_uart_printptr(callstack[d].site);
mega65_uart_print(" ");
mega65_uart_print(function_table[func_num].function);
mega65_uart_print("+");
mega65_uart_printptr((void*)((uint16_t)callstack[d].site - function_table[func_num].addr));
// Show stack pointer
mega65_uart_print(", SP=");
mega65_uart_printptr(callstack[d].stack_pointer);
mega65_uart_print("\n\r");
}
}
#endif
With all that in place, if you call fail(X) where X is an error code, the MEGA65's serial monitor interface will output something like this, and then wait for a keypress on the MEGA65's keyboard before contininuing:
src/telephony/contacts.c:44:mount_contact_qso():0x03
Backtrace (most recent call first):
[02] 0x312A mount_contact_qso+0x00C5, SP=0xD000
[01] 0x10DB main+0x0660, SP=0xD000
[00] 0x0A89 main+0x000E, SP=0xD000
So now I know that fail(3) was called from inside mount_contact_qso(), which was called from main().
No comments:
Post a Comment