π₯Faultπ₯ handling for ESP32forth βββββββββββββββββββ January 21, 2023 What is Fault Handing? ββββββββββββββββββββββ π Reading an "illegal" addresses π€¬ Writing an "illegal" addresses π₯ Executing an "illegal" addresses π« Divide by zero β User interrupt What's an "illegal" address? ββββββββββββββββββββββββββββ π Unmapped memory β Memory with wrong permissions π Unaligned memory (sometimes) Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled. Core 1 register dump: PC : 0x400d4e00 PS : 0x00060a30 A0 : 0x800d5300 A1 : 0x3ffb2650 A2 : 0x3ffe4bd8 A3 : 0x3ffe7e44 A4 : 0x3ffe53f8 A5 : 0x0000007b A6 : 0x3ffe5bdc A7 : 0x3ffe5bd8 A8 : 0x800d51b9 A9 : 0x3f40d104 A10 : 0x3f40d02c A11 : 0x3ffe63dc A12 : 0x400d4dfe A13 : 0x3ffe7ca8 A14 : 0x00000001 A15 : 0x3ffe7ca8 SAR : 0x0000000a EXCCAUSE: 0x0000001c EXCVADDR: 0x0000007b LBEG : 0x4008a4c8 LEND : 0x4008a4d2 LCOUNT : 0x00000000 Backtrace:0x400d4dfd:0x3ffb26500x400d52fd:0x3ffb2800 0x400dbc35:0x3ffb2820 @guru.gifGoal: π₯ Turn faults into regular Forth exceptions : evil 0 @ ; π π π : ignore-evil ['] evil catch ." got: " . ; ignore-evil got: -1 ok : bad-read 0 @ ; : bad-write 123 0 ! ; : bad-call 0 call ; : bad-div 123 0 / ; And even failures outside Forth! (in theory) Alternatives? βββββββββββββ β Make C@, C!, @, ! etc. more expensive β Closed address space (Dr. Ting's Eforth) Parts to Fault Handling βββββββββββββββββββββββ β Interception of a fault - How to intercept bad things? β Resumption to a safe state - How to get back to Forth? Fault Detection in Theory βββββββββββββββββββββββββ β In hardware, close to an interrupt handler - In fact, often done as type of interrupt β Typically involves special intructions entry/exit - stashing of some registers - instruction like IRET β Challenging for high level languages - Current stack may be smashed - Global structures may be in bad state - It may be unwise to go deeper in the stack - Even C finds this hard Challenges of Abstraction βββββββββββββββββββββββββ β Contradictory goals: - Allow flexibility to resume - Allow "normal" code to be used β Interacts poorly with threads - Which thread handles the event? - Do the other threads keep running? Fault Detection by Platform βββββββββββββββββββββββββββ π§ Linux - Signals πͺ Windows - Structured Exceptions π² ESP32 Xtensa - User Exception + a Saga π² ESP32 RISC-V - Saga in progress Linux + Signals βββββββββββββββ π΄ Classic signal() π¦ Replaces by sigaction() #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); #include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }; int sigaltstack(const stack_t *ss, stack_t *old_ss); Resuming to Forth βββββββββββββββββ βΆοΈ Restore Forth registers from latest exception handler βΆοΈ Get CPU instruction pointer back into NEXT ( Exceptions ) variable handler : catch ( xt -- n ) fp@ >r sp@ >r handler @ >r rp@ handler ! execute r> handler ! rdrop rdrop 0 ; : throw ( n -- ) dup if handler @ rp! r> handler ! r> swap >r sp! drop r> r> fp! else drop then ; [ old handler ] β handler [ sp | ] [ fp | ] .... β [ old handler ] [ sp ] [ fp ] #define THROWIT(n) \ rp = *g_sys->throw_handler; \ *g_sys->throw_handler = (cell_t *) *rp--; \ UNPARK; tos = (n); How to GOTO anywhere? #include <setjmp.h> int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val); static cell_t *forth_run(cell_t *init_rp) { static const BUILTIN_WORD builtins[] = { #define Z(flags, name, op, code) \ name, ((VOC_ ## flags >> 8) & 0xff) | BUILTIN_MARK, \ sizeof(name) - 1, (VOC_ ## flags & 0xff), && OP_ ## op, PLATFORM_OPCODE_LIST TIER2_OPCODE_LIST TIER1_OPCODE_LIST TIER0_OPCODE_LIST #undef Z 0, 0, 0, 0, 0, }; if (!init_rp) { g_sys->DOCREATE_OP = ADDROF(DOCREATE); g_sys->builtins = builtins; π₯forth_faults_setup();π₯ return 0; } register cell_t *ip, *rp, *sp, tos, w; register float *fp, ft; rp = init_rp; UNPARK; π₯FAULT_ENTRY;π₯ NEXT; #define Z(flags, name, op, code) OP_ ## op: { code; } NEXT; PLATFORM_OPCODE_LIST TIER2_OPCODE_LIST TIER1_OPCODE_LIST TIER0_OPCODE_LIST #undef Z } #include <setjmp.h> #include <signal.h> static __thread jmp_buf g_forth_fault; static __thread int g_forth_signal; #define FAULT_ENTRY \ if (setjmp(g_forth_fault)) { THROWIT(-g_forth_signal); } static void forth_signal_handler(int sig) { g_forth_signal = sig; sigset_t ss; sigemptyset(&ss); sigprocmask(SIG_SETMASK, &ss, 0); longjmp(g_forth_fault, 1); } static void forth_faults_setup(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = forth_signal_handler; sigaction(SIGSEGV, &sa, 0); sigaction(SIGBUS, &sa, 0); sigaction(SIGINT, &sa, 0); sigaction(SIGFPE, &sa, 0); } SIGSEGV = memory violation SIGBUS = alignment violation SIGINT = Ctrl-C SIGFPE = floating point exception (integer divide by zero) Windows Structured Exceptions βββββββββββββββββββββββββββββ π₯ Windows specific mechanism for hardware + software exceptions π₯ Structred try, catch, finally leveraging call frame or unwind information π₯ Combines try, catch, finally leveraging call frame __try { .. code .. } __except ( .. expression .. ) { .. handler .. } __finally { .. unwind code .. } -1 = EXCEPTION_CONTINUE_EXECUTION 0 = EXCEPTION_CONTINUE_SEARCH 1 = EXCEPTION_EXECUTE_HANDLER static cell_t *forth_run(cell_t *init_rp) { static const BUILTIN_WORD builtins[] = { #define Z(flags, name, op, code) \ name, ((VOC_ ## flags >> 8) & 0xff) | BUILTIN_MARK, sizeof(name) - 1, \ (VOC_ ## flags & 0xff), (void *) OP_ ## op, PLATFORM_OPCODE_LIST TIER2_OPCODE_LIST TIER1_OPCODE_LIST TIER0_OPCODE_LIST #undef Z 0, 0, 0, }; if (!init_rp) { g_sys->DOCREATE_OP = ADDROF(DOCREATE); g_sys->builtins = builtins; return 0; } register cell_t *ip, *rp, *sp, tos, w; register float *fp, ft; rp = init_rp; UNPARK; for (;;) { __try { for (;;) { next: w = *ip++; work: switch (*(cell_t *) w & 0xff) { #define Z(flags, name, op, code) case OP_ ## op: { code; } NEXT; PLATFORM_OPCODE_LIST TIER2_OPCODE_LIST TIER1_OPCODE_LIST TIER0_OPCODE_LIST #undef Z } } } __except (1) { THROWIT(GetExceptionCode()); } } } What about Ctrl-Break? Windows Ctrl-Break/C ββββββββββββββββββ π₯ SetConsoleCtrlHandler - Must happen after AllocConsole π₯ Ctrl-Break/C triggers a handler on separate thread π₯ How to move back to the main thread, CRASH it! YV(windows, SetupCtrlBreakHandler, SetupCtrlBreakHandler()) \ static DWORD forth_main_thread_id; static uintptr_t forth_main_thread_resume_sp; static uintptr_t forth_main_thread_resume_bp; static void SetupCtrlBreakHandler(void) { forth_main_thread_id = GetCurrentThreadId(); SetConsoleCtrlHandler(forth_ctrl_handler, TRUE); CONTEXT context = { 0 }; context.ContextFlags = CONTEXT_CONTROL; GetThreadContext(GetCurrentThread(), &context); #ifdef _WIN64 forth_main_thread_resume_sp = context.Rsp; forth_main_thread_resume_bp = context.Rbp; #else forth_main_thread_resume_sp = context.Esp; forth_main_thread_resume_bp = context.Ebp; #endif } static BOOL WINAPI forth_ctrl_handler(DWORD fdwCtrlType) { HANDLE main_thread; CONTEXT context = { 0 }; if (fdwCtrlType == CTRL_C_EVENT || fdwCtrlType == CTRL_BREAK_EVENT) { // Using explicit instead of THREAD_ALL_ACCESS to be explicit as per docs. // THREAD_QUERY_INFORMATION seems to be required for reasons unknown on x64. main_thread = OpenThread(THREAD_QUERY_INFORMATION | THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME, FALSE, forth_main_thread_id); SuspendThread(main_thread); context.ContextFlags = CONTEXT_CONTROL; GetThreadContext(main_thread, &context); #ifdef _WIN64 context.Rip = 0; context.Rsp = forth_main_thread_resume_sp; context.Rbp = forth_main_thread_resume_bp; #else context.Eip = 0; context.Esp = forth_main_thread_resume_sp; context.Ebp = forth_main_thread_resume_bp; #endif SetThreadContext(main_thread, &context); ResumeThread(main_thread); CloseHandle(main_thread); return TRUE; } return FALSE; } ESP32 Panic Handler βββββββββββββββββββ π« Triggers for many/most failures π« Doesn't seem have documented knobs except gdb hook ESP32 Xtensa Exceptions βββββββββββββββββββββββ π₯ Xtensa Exception Option π₯ Described in Xtensa Instruction Set doc https://0x04.net/~mwk/doc/xtensa.pdf π₯ UserExceptionVector So I went spelunking β°οΈ with my new disassembler! $40080000 _WindowOverflow4 $40080040 _WindowUnderflow4 $40080050 _xt_alloca_exc β stuffed in gap $40080080 _WindowOverflow8 $400800c0 _WindowUnderflow8 $40080100 _WindowOverflow12 $40080140 _WindowUnderflow12 $40080180 _Level2Vector $400801c0 _Level3Vector $40080200 _Level4Vector $40080240 _Level5Vector $40080280 _DebugExceptionVector $400802c0 _NMIExceptionVector $40080300 _KernelExceptionVector $40080340 _UserExceptionVector β π₯ THIS! π₯ $400803c0 _DoubleExceptionVector xtensa_vectors.S π₯ _UserExceptionVector π₯ calls... π₯ _xt_user_exc π₯ Examines EXCCAUSE dispatches to... π₯ _call_loadstore_handler π₯ calls... xtensa_loadstore_handler.S π₯ LoadStoreErrorHandler π₯ but then goes back and... π₯ _xt_user_exc π₯ and calls a handled from: _xt_exception_table Which can be set with... typedef void (*xt_exc_handler)(XtExcFrame *); xt_exc_handler xt_set_exception_handler(int n, xt_exc_handler f); So I can... #include <setjmp.h> #include "soc/soc.h" #include <xtensa/xtensa_api.h> static __thread jmp_buf g_forth_fault; static __thread int g_forth_signal; static __thread uint32_t g_forth_setlevel; #define FAULT_ENTRY \ if (setjmp(g_forth_fault)) { THROWIT(-g_forth_signal); } static void IRAM_ATTR forth_exception_handler(XtExcFrame *frame) { g_forth_signal = frame->exccause; XTOS_RESTORE_INTLEVEL(g_forth_setlevel); longjmp(g_forth_fault, 1); } static void forth_faults_setup(void) { xt_set_exception_handler(EXCCAUSE_LOAD_STORE_ERROR, forth_exception_handler); xt_set_exception_handler(EXCCAUSE_PRIVILEGED, forth_exception_handler); xt_set_exception_handler(EXCCAUSE_UNALIGNED, forth_exception_handler); xt_set_exception_handler(EXCCAUSE_DIVIDE_BY_ZERO, forth_exception_handler); xt_set_exception_handler(EXCCAUSE_INSTR_ERROR, forth_exception_handler); xt_set_exception_handler(EXCCAUSE_ILLEGAL, forth_exception_handler); xt_set_exception_handler(EXCCAUSE_LOAD_PROHIBITED, forth_exception_handler); xt_set_exception_handler(EXCCAUSE_STORE_PROHIBITED, forth_exception_handler); xt_set_exception_handler(EXCCAUSE_INSTR_PROHIBITED, forth_exception_handler); uint32_t default_setlevel = XTOS_SET_INTLEVEL(XCHAL_EXCM_LEVEL); XTOS_RESTORE_INTLEVEL(default_setlevel); g_forth_setlevel = default_setlevel; } What about RISC-V? components/riscv/vectors.S -------------------------- .balign 0x100 .global _vector_table .type _vector_table, @function _vector_table: .option push .option norvc j _panic_handler /* exception handler, entry 0 */ .rept (ETS_T1_WDT_INUM - 1) j _interrupt_handler /* 24 identical entries, all pointing to the interrupt handler */ .endr j _panic_handler /* Call panic handler for ETS_T1_WDT_INUM interrupt (soc-level panic)*/ j _panic_handler /* Call panic handler for ETS_CACHEERR_INUM interrupt (soc-level panic)*/ #ifdef CONFIG_ESP_SYSTEM_MEMPROT_FEATURE j _panic_handler /* Call panic handler for ETS_MEMPROT_ERR_INUM interrupt (soc-level panic)*/ .rept (ETS_MAX_INUM - ETS_MEMPROT_ERR_INUM) #else .rept (ETS_MAX_INUM - ETS_CACHEERR_INUM) #endif //CONFIG_ESP_SYSTEM_MEMPROT_FEATURE j _interrupt_handler /* 6 identical entries, all pointing to the interrupt handler */ .endr components/esp_system/port/cpu_start.c π₯π₯π₯π₯π₯π₯π₯π₯ esp_cpu_intr_set_ivt_addr(&_vector_table); Missing Things ββββββββββββββ π So maybe RISC-V next time... π Document and standardize throw values π£ DEMO π£ QUESTIONS? π₯ Thank you!