1#!/usr/bin/env python 2# coding=utf-8 3# 4# A script which parses ESP-IDF panic handler output (registers & stack dump), 5# and then acts as a GDB server over stdin/stdout, presenting the information 6# from the panic handler to GDB. 7# This allows for generating backtraces out of raw stack dumps on architectures 8# where backtracing on the target side is not possible. 9# 10# Note that the "act as a GDB server" approach is somewhat a hack. 11# A much nicer solution would have been to convert the panic handler output 12# into a core file, and point GDB to the core file. 13# However, RISC-V baremetal GDB currently lacks core dump support. 14# 15# The approach is inspired by Cesanta's ESP8266 GDB server: 16# https://github.com/cesanta/mongoose-os/blob/27777c8977/platforms/esp8266/tools/serve_core.py 17# 18# Copyright 2020 Espressif Systems (Shanghai) Co. Ltd. 19# 20# Licensed under the Apache License, Version 2.0 (the "License"); 21# you may not use this file except in compliance with the License. 22# You may obtain a copy of the License at 23# 24# http://www.apache.org/licenses/LICENSE-2.0 25# 26# Unless required by applicable law or agreed to in writing, software 27# distributed under the License is distributed on an "AS IS" BASIS, 28# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29# See the License for the specific language governing permissions and 30# limitations under the License. 31# 32 33 34import argparse 35import binascii 36import logging 37import struct 38import sys 39from builtins import bytes 40from collections import namedtuple 41 42# Used for type annotations only. Silence linter warnings. 43from pyparsing import (Combine, Group, Literal, OneOrMore, ParserElement, # noqa: F401 # pylint: disable=unused-import 44 ParseResults, Word, nums, srange) 45 46try: 47 import typing # noqa: F401 # pylint: disable=unused-import 48except ImportError: 49 pass 50 51# pyparsing helper 52hexnumber = srange('[0-9a-f]') 53 54 55# List of registers to be passed to GDB, in the order GDB expects. 56# The names should match those used in IDF panic handler. 57# Registers not present in IDF panic handler output (like X0) will be assumed to be 0. 58GDB_REGS_INFO_RISCV_ILP32 = [ 59 'X0', 'RA', 'SP', 'GP', 60 'TP', 'T0', 'T1', 'T2', 61 'S0/FP', 'S1', 'A0', 'A1', 62 'A2', 'A3', 'A4', 'A5', 63 'A6', 'A7', 'S2', 'S3', 64 'S4', 'S5', 'S6', 'S7', 65 'S8', 'S9', 'S10', 'S11', 66 'T3', 'T4', 'T5', 'T6', 67 'MEPC' 68] 69 70 71GDB_REGS_INFO = { 72 'esp32c3': GDB_REGS_INFO_RISCV_ILP32, 73 'esp32h2': GDB_REGS_INFO_RISCV_ILP32 74} 75 76PanicInfo = namedtuple('PanicInfo', 'core_id regs stack_base_addr stack_data') 77 78 79def build_riscv_panic_output_parser(): # type: () -> typing.Any[typing.Type[ParserElement]] 80 """Builds a parser for the panic handler output using pyparsing""" 81 82 # We don't match the first line, since "Guru Meditation" will not be printed in case of an abort: 83 # Guru Meditation Error: Core 0 panic'ed (Store access fault). Exception was unhandled. 84 85 # Core 0 register dump: 86 reg_dump_header = Group(Literal('Core') + 87 Word(nums)('core_id') + 88 Literal('register dump:'))('reg_dump_header') 89 90 # MEPC : 0x4200232c RA : 0x42009694 SP : 0x3fc93a80 GP : 0x3fc8b320 91 reg_name = Word(srange('[A-Z_0-9/-]'))('name') 92 hexnumber_with_0x = Combine(Literal('0x') + Word(hexnumber)) 93 reg_value = hexnumber_with_0x('value') 94 reg_dump_one_reg = Group(reg_name + Literal(':') + reg_value) # not named because there will be OneOrMore 95 reg_dump_all_regs = Group(OneOrMore(reg_dump_one_reg))('regs') 96 reg_dump = Group(reg_dump_header + reg_dump_all_regs) # not named because there will be OneOrMore 97 reg_dumps = Group(OneOrMore(reg_dump))('reg_dumps') 98 99 # Stack memory: 100 # 3fc93a80: 0x00000030 0x00000021 0x3fc8aedc 0x4200232a 0xa5a5a5a5 0xa5a5a5a5 0x3fc8aedc 0x420099b0 101 stack_line = Group(Word(hexnumber)('base') + Literal(':') + 102 Group(OneOrMore(hexnumber_with_0x))('data')) 103 stack_dump = Group(Literal('Stack memory:') + 104 Group(OneOrMore(stack_line))('lines'))('stack_dump') 105 106 # Parser for the complete panic output: 107 panic_output = reg_dumps + stack_dump 108 return panic_output 109 110 111def get_stack_addr_and_data(res): # type: (ParseResults) -> typing.Tuple[int, bytes] 112 """ Extract base address and bytes from the parsed stack dump """ 113 stack_base_addr = 0 # First reported address in the dump 114 base_addr = 0 # keeps track of the address for the given line of the dump 115 bytes_in_line = 0 # bytes of stack parsed on the previous line; used to validate the next base address 116 stack_data = bytes(b'') # accumulates all the dumped stack data 117 for line in res.stack_dump.lines: 118 # update and validate the base address 119 prev_base_addr = base_addr 120 base_addr = int(line.base, 16) 121 if stack_base_addr == 0: 122 stack_base_addr = base_addr 123 else: 124 assert base_addr == prev_base_addr + bytes_in_line 125 126 # convert little-endian hex words to byte representation 127 words = [int(w, 16) for w in line.data] 128 line_data = bytes(b''.join([struct.pack('<I', w) for w in words])) 129 bytes_in_line = len(line_data) 130 131 # accumulate in the whole stack data 132 stack_data += line_data 133 134 return stack_base_addr, stack_data 135 136 137def parse_idf_riscv_panic_output(panic_text): # type: (str) -> PanicInfo 138 """ Decode panic handler output from a file """ 139 panic_output = build_riscv_panic_output_parser() 140 results = panic_output.searchString(panic_text) 141 if len(results) != 1: 142 raise ValueError("Couldn't parse panic handler output") 143 res = results[0] 144 145 if len(res.reg_dumps) > 1: 146 raise NotImplementedError('Handling of multi-core register dumps not implemented') 147 148 # Build a dict of register names/values 149 rd = res.reg_dumps[0] 150 core_id = int(rd.reg_dump_header.core_id) 151 regs = dict() 152 for reg in rd.regs: 153 reg_value = int(reg.value, 16) 154 regs[reg.name] = reg_value 155 156 stack_base_addr, stack_data = get_stack_addr_and_data(res) 157 158 return PanicInfo(core_id=core_id, 159 regs=regs, 160 stack_base_addr=stack_base_addr, 161 stack_data=stack_data) 162 163 164PANIC_OUTPUT_PARSERS = { 165 'esp32c3': parse_idf_riscv_panic_output, 166 'esp32h2': parse_idf_riscv_panic_output 167} 168 169 170class GdbServer(object): 171 def __init__(self, panic_info, target, log_file=None): # type: (PanicInfo, str, str) -> None 172 self.panic_info = panic_info 173 self.in_stream = sys.stdin 174 self.out_stream = sys.stdout 175 self.reg_list = GDB_REGS_INFO[target] 176 177 self.logger = logging.getLogger('GdbServer') 178 if log_file: 179 handler = logging.FileHandler(log_file, 'w+') 180 self.logger.setLevel(logging.DEBUG) 181 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 182 handler.setFormatter(formatter) 183 self.logger.addHandler(handler) 184 185 def run(self): # type: () -> None 186 """ Process GDB commands from stdin until GDB tells us to quit """ 187 buffer = '' 188 while True: 189 buffer += self.in_stream.read(1) 190 if len(buffer) > 3 and buffer[-3] == '#': 191 self._handle_command(buffer) 192 buffer = '' 193 194 def _handle_command(self, buffer): # type: (str) -> None 195 command = buffer[1:-3] # ignore checksums 196 # Acknowledge the command 197 self.out_stream.write('+') 198 self.out_stream.flush() 199 self.logger.debug('Got command: %s', command) 200 if command == '?': 201 # report sigtrap as the stop reason; the exact reason doesn't matter for backtracing 202 self._respond('T05') 203 elif command.startswith('Hg') or command.startswith('Hc'): 204 # Select thread command 205 self._respond('OK') 206 elif command == 'qfThreadInfo': 207 # Get list of threads. 208 # Only one thread for now, can be extended to show one thread for each core, 209 # if we dump both cores (e.g. on an interrupt watchdog) 210 self._respond('m1') 211 elif command == 'qC': 212 # That single thread is selected. 213 self._respond('QC1') 214 elif command == 'g': 215 # Registers read 216 self._respond_regs() 217 elif command.startswith('m'): 218 # Memory read 219 addr, size = [int(v, 16) for v in command[1:].split(',')] 220 self._respond_mem(addr, size) 221 elif command.startswith('vKill') or command == 'k': 222 # Quit 223 self._respond('OK') 224 raise SystemExit(0) 225 else: 226 # Empty response required for any unknown command 227 self._respond('') 228 229 def _respond(self, data): # type: (str) -> None 230 # calculate checksum 231 data_bytes = bytes(data.encode('ascii')) # bytes() for Py2 compatibility 232 checksum = sum(data_bytes) & 0xff 233 # format and write the response 234 res = '${}#{:02x}'.format(data, checksum) 235 self.logger.debug('Wrote: %s', res) 236 self.out_stream.write(res) 237 self.out_stream.flush() 238 # get the result ('+' or '-') 239 ret = self.in_stream.read(1) 240 self.logger.debug('Response: %s', ret) 241 if ret != '+': 242 sys.stderr.write("GDB responded with '-' to {}".format(res)) 243 raise SystemExit(1) 244 245 def _respond_regs(self): # type: () -> None 246 response = '' 247 for reg_name in self.reg_list: 248 # register values are reported as hexadecimal strings 249 # in target byte order (i.e. LSB first for RISC-V) 250 reg_val = self.panic_info.regs.get(reg_name, 0) 251 reg_bytes = struct.pack('<L', reg_val) 252 response += binascii.hexlify(reg_bytes).decode('ascii') 253 self._respond(response) 254 255 def _respond_mem(self, start_addr, size): # type: (int, int) -> None 256 stack_addr_min = self.panic_info.stack_base_addr 257 stack_data = self.panic_info.stack_data 258 stack_len = len(self.panic_info.stack_data) 259 stack_addr_max = stack_addr_min + stack_len 260 261 # For any memory address that is not on the stack, pretend the value is 0x00. 262 # GDB should never ask us for program memory, it will be obtained from the ELF file. 263 def in_stack(addr): # type: (int) -> typing.Any[bool] 264 return stack_addr_min <= addr < stack_addr_max 265 266 result = '' 267 for addr in range(start_addr, start_addr + size): 268 if not in_stack(addr): 269 result += '00' 270 else: 271 result += '{:02x}'.format(stack_data[addr - stack_addr_min]) 272 273 self._respond(result) 274 275 276def main(): # type: () -> None 277 parser = argparse.ArgumentParser() 278 parser.add_argument('input_file', type=argparse.FileType('r'), 279 help='File containing the panic handler output') 280 parser.add_argument('--target', choices=GDB_REGS_INFO.keys(), 281 help='Chip to use (determines the architecture)') 282 parser.add_argument('--gdb-log', default=None, 283 help='If specified, the file for logging GDB server debug information') 284 args = parser.parse_args() 285 286 panic_info = PANIC_OUTPUT_PARSERS[args.target](args.input_file.read()) 287 288 server = GdbServer(panic_info, target=args.target, log_file=args.gdb_log) 289 try: 290 server.run() 291 except KeyboardInterrupt: 292 sys.exit(0) 293 294 295if __name__ == '__main__': 296 main() 297