diff --git a/.gitignore b/.gitignore index e366e5b..6e0e06f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build/ dist/ /toolchain +__pycache__ **/words/build-info.s .DS_Store diff --git a/arm/dev/gdb_backtrace.py b/arm/dev/gdb_backtrace.py new file mode 100644 index 0000000..747c750 --- /dev/null +++ b/arm/dev/gdb_backtrace.py @@ -0,0 +1,146 @@ +import gdb +from gdb_shared import * +from gdb.unwinder import Unwinder, FrameId + + +class ForthUnwinder(Unwinder): + """Unwinder for Forth return stack. + + GDB uses the unwinder to interpret the return stack. It seeds the process with a first pending_frame + that includes the state of all registers, the PC is set to whatever the current PC value is. + SP points at the top of the return stack. + The unwinder then builds and return unwind_info for the next pending frame (in the __call__ method). + The __call__ method is called repeatedly until a None is returned which ends the backtracce building process. + """ + + def __init__(self): + super().__init__("ForthUnwinder") + self.first_frame_done = False + + def __call__(self, pending_frame): + try: + sp = pending_frame.read_register("sp") + pc = pending_frame.read_register("pc") + + # Stop if we hit bottom of the stack + if not (RAM_lower_returnstack <= int(sp) < RAM_upper_returnstack): + return None + + if self.first_frame_done: + sp_ptr = sp.cast(gdb.lookup_type("unsigned int").pointer()) + ret_addr = sp_ptr.dereference() + else: + # Adjust SP for first frame (FORTHIP effectively extends RSP by one slot) + sp = gdb.Value(int(sp) - 4).cast(sp.type) + ret_addr = pending_frame.read_register("r9") + self.first_frame_done = True + + # Validate the return address + ret_addr_int = int(ret_addr) + if not (FlashStart <= ret_addr_int < FlashEnd): + return None + + # Create frame ID: use SP (as identity) and ret_addr (as code address) + frame_id = FrameId(int(sp), ret_addr_int) + unwind_info = pending_frame.create_unwind_info(frame_id) + + # Next frame's SP is current SP + 4 (pop one return address) + new_sp = gdb.Value(int(sp) + 4).cast(sp.type) + + # Next frame's PC is the return address we just popped + unwind_info.add_saved_register("pc", ret_addr) + unwind_info.add_saved_register("sp", new_sp) + + return unwind_info + except Exception as e: + print(f"[ForthUnwinder] Exception: {e}") + import traceback + traceback.print_exc() + return None + +# Register the unwinder +gdb.unwinder.register_unwinder(None, ForthUnwinder(), replace=False) + +# Keeping the code below in case we do need FrameDecorators later. +# This shows how to put it together. + +# Frame decorator to enhance Forth frame information +# class ForthFrameDecorator(gdb.FrameDecorator.FrameDecorator): +# """Decorator to enhance Forth frame display with symbol information.""" + +# def __init__(self, frame): +# super().__init__(frame) + +# def function(self): +# """Get function name from symbol.""" +# try: +# frame = self.inferior_frame() +# if frame is None: +# return None +# pc = frame.pc() +# # Look up the symbol at this address +# try: +# block = gdb.block_for_pc(pc) +# if block and block.function: +# return block.function.name +# except: +# pass +# # Try to get just the symbol name +# try: +# result = gdb.execute(f"info symbol 0x{pc:x}", to_string=True).strip() +# if result and "not in any" not in result: +# # Extract just the symbol name (before any +offset) +# parts = result.split() +# if parts: +# return parts[0] +# except: +# pass +# except: +# pass +# return None + +# def filename(self): +# """Get source filename if available.""" +# try: +# frame = self.inferior_frame() +# if frame is None: +# return None +# pc = frame.pc() +# sal = gdb.find_pc_line(pc) +# # Only return if we have valid debug info +# if sal and sal.symtab and sal.line > 0: +# return sal.symtab.filename +# except: +# pass +# return None + +# def line(self): +# """Get source line number if available.""" +# try: +# frame = self.inferior_frame() +# if frame is None: +# return None +# pc = frame.pc() +# sal = gdb.find_pc_line(pc) +# # Only return if we have valid debug info +# if sal and sal.line > 0: +# return sal.line +# except: +# pass +# return None + +# class ForthFrameFilter: +# """Filter to apply ForthFrameDecorator to all frames.""" + +# def __init__(self): +# self.name = "ForthFrameFilter" +# self.priority = 100 +# self.enabled = True +# gdb.frame_filters[self.name] = self + +# def filter(self, frame_iter): +# # Wrap each frame with our decorator +# for frame in frame_iter: +# yield ForthFrameDecorator(frame) + +# ForthFrameFilter() diff --git a/arm/dev/gdb_shared.py b/arm/dev/gdb_shared.py new file mode 100644 index 0000000..fda04ce --- /dev/null +++ b/arm/dev/gdb_shared.py @@ -0,0 +1,26 @@ +import gdb +import sys + +# Append the GDB search directory to Python module search path +# so that the dev/ files can be imported as modules. +# `info directories` returns something like: +# Source directories searched: /Users/martin/forth/amforth/arm/mcu/lm4f120/../../dev:$cdir:$cwd +# Grab the first directory in the list and append it to sys.path. +# Note that this file still has to be sourced explicitly. +sys.path.append(gdb.execute("show directories", to_string=True).split(':')[1].strip()) + +def get_sym_val(name, default): + try: + return int(gdb.parse_and_eval(f"&{name}")) + except gdb.error: + return default + +FlashStart = get_sym_val("flash.low", 0x00000000) +FlashEnd = get_sym_val("FlashEnd", 0x00040000) +RAM_lower_datastack = get_sym_val("RAM_lower_datastack", 0x20000000) +RAM_upper_datastack = get_sym_val("RAM_upper_datastack", 0x20000080) +RAM_lower_returnstack = get_sym_val("RAM_lower_returnstack", 0x20000080) +RAM_upper_returnstack = get_sym_val("RAM_upper_returnstack", 0x20000100) +RAM_lower_userarea = get_sym_val("RAM_lower_userarea", 0x20000100) +RAM_upper_userarea = get_sym_val("RAM_upper_userarea", 0x20000188) + diff --git a/arm/dev/gdb-amforth.py b/arm/dev/gdb_tui.py similarity index 64% rename from arm/dev/gdb-amforth.py rename to arm/dev/gdb_tui.py index bd6510a..3fdb74a 100644 --- a/arm/dev/gdb-amforth.py +++ b/arm/dev/gdb_tui.py @@ -2,17 +2,7 @@ # Loaded by tui-full.gdb import gdb - -# TODO: we should be able to read these from the symbol table -# FIXME: These are now wrong with amforth32.ld in the picture -FlashStart = 0x00004000 -FlashEnd = 0x00040000 -RAM_lower_datastack = 0x20000000 -RAM_upper_datastack = 0x20000080 -RAM_lower_returnstack = 0x20000080 -RAM_upper_returnstack = 0x20000100 -RAM_lower_userarea = 0x20000100 -RAM_upper_userarea = 0x20000188 +from gdb_shared import * # GdbCommandWindow can be used to define a TUI window # that invokes a GDB command to produce its contents. @@ -35,18 +25,18 @@ def render(self): return self._tui_window.write(self.get_contents(), True) -class ForthParameterStack(GdbCommandWindow): - title = "Parameter Stack" - gdb_command = ".s" +# class ForthParameterStack(GdbCommandWindow): +# title = "Parameter Stack" +# gdb_command = ".s" -class ForthReturnStack(GdbCommandWindow): - title = "Return Stack" - gdb_command = ".r" +# class ForthReturnStack(GdbCommandWindow): +# title = "Return Stack" +# gdb_command = ".r" def value(val): # If val is in the flash code range, treat it as address if FlashStart <= val and val < FlashEnd: - return val.format_string(format="a") + return address(val) dec = val.format_string(format="d") hex = val.format_string(format="x") if 0 <= val and val <= 0xFFFF: @@ -60,6 +50,9 @@ def value(val): else: return f"{dec} {hex}" +def address(val): + return val.format_string(format="a") + # ForthRegisterWindow is a custom register view # showing registers based on what they are used for in AmForth. class ForthRegisterWindow: @@ -118,7 +111,7 @@ def get_contents(self): self.addres_register(frame, "r10", "UP"), self.value_register(frame, "r11", "RLINDEX"), self.value_register(frame, "r12", "RLLIMIT"), - self.addres_register(frame, "sp"), + self.addres_register(frame, "sp", "RSP"), self.addres_register(frame, "lr"), self.addres_register(frame, "pc"), # xPSR on Cortex-M3 is named xpsr @@ -136,42 +129,79 @@ def render(self): self._tui_window.write(contents, True) # ForthParameterStack shows the contents of the PSP -# class ForthParameterStack: - -# def __init__(self, tui_window): -# self._tui_window = tui_window -# tui_window.title = "Forth Parameter Stack" -# gdb.events.before_prompt.connect(self.render) - -# def get_contents(self): -# frame = gdb.selected_frame() -# if frame is None: -# return "no frame selected" -# tos = frame.read_register("r6") -# # TODO: need to detect when stack is empty -# lines = [ f"r6/TOS:\t\t{value(tos)}" ] -# psp = frame.read_register("r7") -# # cast psp from int to int* so that we can dereference it -# psp = psp.cast(psp.type.pointer()) -# while psp < RAM_upper_datastack: -# addr = psp.format_string(format="x") -# lines.append(f"{addr}:\t{value(psp.dereference())}") -# psp += 1 -# return "\n".join(lines) - -# def render(self): -# if not self._tui_window.is_valid(): -# return -# try: -# contents = self.get_contents() -# except gdb.error as exc: -# contents = str(exc) -# self._tui_window.write(contents, True) +class ForthParameterStack: + + def __init__(self, tui_window): + self._tui_window = tui_window + tui_window.title = "Forth Parameter Stack" + gdb.events.before_prompt.connect(self.render) + + def get_contents(self): + frame = gdb.selected_frame() + if frame is None: + return "no frame selected" + tos = frame.read_register("r6") + # TODO: need to detect when stack is empty + lines = [ f"r6/TOS:\t\t{value(tos)}" ] + psp = frame.read_register("r7") + # cast psp from int to int* so that we can dereference it + psp = psp.cast(psp.type.pointer()) + while psp < RAM_upper_datastack: + addr = psp.format_string(format="x") + lines.append(f"{addr}:\t{value(psp.dereference())}") + psp += 1 + return "\n".join(lines) + + def render(self): + if not self._tui_window.is_valid(): + return + try: + contents = self.get_contents() + except gdb.error as exc: + contents = str(exc) + self._tui_window.write(contents, True) + +# ForthReturnStack shows the contents of the RSP +class ForthReturnStack: + + def __init__(self, tui_window): + self._tui_window = tui_window + tui_window.title = "Forth Return Stack" + gdb.events.before_prompt.connect(self.render) + + def get_contents(self): + frame = gdb.selected_frame() + if frame is None: + return "no frame selected" + w = frame.read_register("r8") # FORTHW + lines = [ f"r8/FORTHW:\t{address(w)}" ] + ip = frame.read_register("r9") # FORTHIP + lines.append(f"r9/FORTHIP:\t{address(ip)}") + rsp = frame.read_register("sp") + # cast rsp from int to int* so that we can dereference it + rsp = rsp.cast(rsp.type.pointer()) + while rsp < RAM_upper_returnstack: + addr = rsp.format_string(format="x") + lines.append(f"{addr}:\t{gdb.format_address(int(rsp.dereference()))}") + rsp += 1 + return "\n".join(lines) + + def render(self): + if not self._tui_window.is_valid(): + return + try: + contents = self.get_contents() + except gdb.error as exc: + contents = str(exc) + self._tui_window.write(contents, True) + gdb.register_window_type("fps", ForthParameterStack) gdb.register_window_type("frs", ForthReturnStack) gdb.register_window_type("fregs", ForthRegisterWindow) + + # GDB Python API Notes # # To read memory: gdb.selected_inferior().read_memory(addr, length) diff --git a/arm/dev/tui-full.gdb b/arm/dev/tui-full.gdb index 3dd18cd..5b61787 100644 --- a/arm/dev/tui-full.gdb +++ b/arm/dev/tui-full.gdb @@ -7,7 +7,9 @@ source amforth.gdb # This requires Python enabled GDB and the correct version of Python installed on the system. # ref: https://undo.io/resources/enhance-gdb-with-tui/ # Make sure the sourced files are on GDB search path -source gdb-amforth.py +source gdb_shared.py +source gdb_backtrace.py +source gdb_tui.py tui new-layout forth {-horizontal { {-horizontal src 2 asm 3 } 1 status 0 cmd 1 } 3 { fregs 2 fps 1 frs 1 } 1 } 1 # Enable the forth layout and set focus on the command window diff --git a/core/dev/Makefile b/core/dev/Makefile index 41de426..7044ae8 100644 --- a/core/dev/Makefile +++ b/core/dev/Makefile @@ -36,7 +36,7 @@ GDB ?= $(TC_DIR)/bin/$(CROSS)gdb GDBPY ?= $(TC_DIR)/bin/$(CROSS)gdb-py3 ## Build all files -all: build/amforth.bin build/amforth.hex build/amforth.lst +all: build/amforth.bin build/amforth.hex build/amforth.lst build/amforth.sal build/amforth.sym buildinfo: words/build-info.tmpl cat words/build-info.tmpl | sed "s/%d/ `date`/" | sed "s/%r/${REVISION}/" > words/build-info.s @@ -73,6 +73,16 @@ build/%.bin: build/%.elf build/%.lst: build/%.elf $(OBJDUMP) --show-raw-insn --insn-width=4 --all-headers --disassemble-all --source --syms $< >$@ +SORT_SYMS_BY_ADDRESS := grep -E '^[0-9a-f]{8} .*' | \ + awk '{ printf "0x%s\n",$$0; }' | \ + sort --key=1 --sort=numeric + +build/%.sal: build/%.elf + $(NM) -l $< | $(SORT_SYMS_BY_ADDRESS) >$@ + +build/%.sym: build/%.elf + $(OBJDUMP) --syms $< | $(SORT_SYMS_BY_ADDRESS) >$@ + clean: rm -f build/* words/build-info.s @@ -101,7 +111,7 @@ test: cat $(AMFORTH)/tests/core2.fr; \ echo '.s" TESTS FINISHED"' ; \ } | timeout -s TERM $(TIMEOUT) $(QEMU) -serial stdio | \ - tee tests.log | \ + tee build/tests.log | \ awk -f $(AMFORTH)/tests/results.awk ## A detailed dump of ELF sections for debugging linker issues diff --git a/core/macros.inc b/core/macros.inc index ce6a1a7..b0a4960 100644 --- a/core/macros.inc +++ b/core/macros.inc @@ -79,7 +79,15 @@ VE_\Label: PFA_\Label: .endm +# Use to terminate COLON and CODEWORD definition +.macro FINISH Label + .size _\Label, . - VE_\Label +.endm + .macro CODEWORD Name, Label + .global _\Label + .type _\Label, STT_FUNC +_\Label: HEADER Flag_visible, "\Name", \Label, PFA_\Label .endm @@ -107,6 +115,9 @@ VE_\Label: .endm .macro COLON Name, Label + .global _\Label + .type _\Label, STT_FUNC +_\Label: HEADER Flag_colon, "\Name", \Label, DOCOLON .endm