gdb_panic_server.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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. import argparse
  33. import binascii
  34. import logging
  35. import struct
  36. import sys
  37. from builtins import bytes
  38. from collections import namedtuple
  39. # Used for type annotations only. Silence linter warnings.
  40. from pyparsing import (Combine, Group, Literal, OneOrMore, ParserElement, # noqa: F401 # pylint: disable=unused-import
  41. ParseResults, Word, nums, srange)
  42. try:
  43. import typing # noqa: F401 # pylint: disable=unused-import
  44. except ImportError:
  45. pass
  46. # pyparsing helper
  47. hexnumber = srange('[0-9a-f]')
  48. # List of registers to be passed to GDB, in the order GDB expects.
  49. # The names should match those used in IDF panic handler.
  50. # Registers not present in IDF panic handler output (like X0) will be assumed to be 0.
  51. GDB_REGS_INFO_RISCV_ILP32 = [
  52. 'X0', 'RA', 'SP', 'GP',
  53. 'TP', 'T0', 'T1', 'T2',
  54. 'S0/FP', 'S1', 'A0', 'A1',
  55. 'A2', 'A3', 'A4', 'A5',
  56. 'A6', 'A7', 'S2', 'S3',
  57. 'S4', 'S5', 'S6', 'S7',
  58. 'S8', 'S9', 'S10', 'S11',
  59. 'T3', 'T4', 'T5', 'T6',
  60. 'MEPC'
  61. ]
  62. GDB_REGS_INFO = {
  63. 'esp32c3': GDB_REGS_INFO_RISCV_ILP32,
  64. 'esp32h2': GDB_REGS_INFO_RISCV_ILP32
  65. }
  66. PanicInfo = namedtuple('PanicInfo', 'core_id regs stack_base_addr stack_data')
  67. def build_riscv_panic_output_parser(): # type: () -> typing.Any[typing.Type[ParserElement]]
  68. """Builds a parser for the panic handler output using pyparsing"""
  69. # We don't match the first line, since "Guru Meditation" will not be printed in case of an abort:
  70. # Guru Meditation Error: Core 0 panic'ed (Store access fault). Exception was unhandled.
  71. # Core 0 register dump:
  72. reg_dump_header = Group(Literal('Core') +
  73. Word(nums)('core_id') +
  74. Literal('register dump:'))('reg_dump_header')
  75. # MEPC : 0x4200232c RA : 0x42009694 SP : 0x3fc93a80 GP : 0x3fc8b320
  76. reg_name = Word(srange('[A-Z_0-9/-]'))('name')
  77. hexnumber_with_0x = Combine(Literal('0x') + Word(hexnumber))
  78. reg_value = hexnumber_with_0x('value')
  79. reg_dump_one_reg = Group(reg_name + Literal(':') + reg_value) # not named because there will be OneOrMore
  80. reg_dump_all_regs = Group(OneOrMore(reg_dump_one_reg))('regs')
  81. reg_dump = Group(reg_dump_header + reg_dump_all_regs) # not named because there will be OneOrMore
  82. reg_dumps = Group(OneOrMore(reg_dump))('reg_dumps')
  83. # Stack memory:
  84. # 3fc93a80: 0x00000030 0x00000021 0x3fc8aedc 0x4200232a 0xa5a5a5a5 0xa5a5a5a5 0x3fc8aedc 0x420099b0
  85. stack_line = Group(Word(hexnumber)('base') + Literal(':') +
  86. Group(OneOrMore(hexnumber_with_0x))('data'))
  87. stack_dump = Group(Literal('Stack memory:') +
  88. Group(OneOrMore(stack_line))('lines'))('stack_dump')
  89. # Parser for the complete panic output:
  90. panic_output = reg_dumps + stack_dump
  91. return panic_output
  92. def get_stack_addr_and_data(res): # type: (ParseResults) -> typing.Tuple[int, bytes]
  93. """ Extract base address and bytes from the parsed stack dump """
  94. stack_base_addr = 0 # First reported address in the dump
  95. base_addr = 0 # keeps track of the address for the given line of the dump
  96. bytes_in_line = 0 # bytes of stack parsed on the previous line; used to validate the next base address
  97. stack_data = bytes(b'') # accumulates all the dumped stack data
  98. for line in res.stack_dump.lines:
  99. # update and validate the base address
  100. prev_base_addr = base_addr
  101. base_addr = int(line.base, 16)
  102. if stack_base_addr == 0:
  103. stack_base_addr = base_addr
  104. else:
  105. assert base_addr == prev_base_addr + bytes_in_line
  106. # convert little-endian hex words to byte representation
  107. words = [int(w, 16) for w in line.data]
  108. line_data = bytes(b''.join([struct.pack('<I', w) for w in words]))
  109. bytes_in_line = len(line_data)
  110. # accumulate in the whole stack data
  111. stack_data += line_data
  112. return stack_base_addr, stack_data
  113. def parse_idf_riscv_panic_output(panic_text): # type: (str) -> PanicInfo
  114. """ Decode panic handler output from a file """
  115. panic_output = build_riscv_panic_output_parser()
  116. results = panic_output.searchString(panic_text)
  117. if len(results) != 1:
  118. raise ValueError("Couldn't parse panic handler output")
  119. res = results[0]
  120. if len(res.reg_dumps) > 1:
  121. raise NotImplementedError('Handling of multi-core register dumps not implemented')
  122. # Build a dict of register names/values
  123. rd = res.reg_dumps[0]
  124. core_id = int(rd.reg_dump_header.core_id)
  125. regs = dict()
  126. for reg in rd.regs:
  127. reg_value = int(reg.value, 16)
  128. regs[reg.name] = reg_value
  129. stack_base_addr, stack_data = get_stack_addr_and_data(res)
  130. return PanicInfo(core_id=core_id,
  131. regs=regs,
  132. stack_base_addr=stack_base_addr,
  133. stack_data=stack_data)
  134. PANIC_OUTPUT_PARSERS = {
  135. 'esp32c3': parse_idf_riscv_panic_output,
  136. 'esp32h2': parse_idf_riscv_panic_output
  137. }
  138. class GdbServer(object):
  139. def __init__(self, panic_info, target, log_file=None): # type: (PanicInfo, str, str) -> None
  140. self.panic_info = panic_info
  141. self.in_stream = sys.stdin
  142. self.out_stream = sys.stdout
  143. self.reg_list = GDB_REGS_INFO[target]
  144. self.logger = logging.getLogger('GdbServer')
  145. if log_file:
  146. handler = logging.FileHandler(log_file, 'w+')
  147. self.logger.setLevel(logging.DEBUG)
  148. formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  149. handler.setFormatter(formatter)
  150. self.logger.addHandler(handler)
  151. def run(self): # type: () -> None
  152. """ Process GDB commands from stdin until GDB tells us to quit """
  153. buffer = ''
  154. while True:
  155. buffer += self.in_stream.read(1)
  156. if len(buffer) > 3 and buffer[-3] == '#':
  157. self._handle_command(buffer)
  158. buffer = ''
  159. def _handle_command(self, buffer): # type: (str) -> None
  160. command = buffer[1:-3] # ignore checksums
  161. # Acknowledge the command
  162. self.out_stream.write('+')
  163. self.out_stream.flush()
  164. self.logger.debug('Got command: %s', command)
  165. if command == '?':
  166. # report sigtrap as the stop reason; the exact reason doesn't matter for backtracing
  167. self._respond('T05')
  168. elif command.startswith('Hg') or command.startswith('Hc'):
  169. # Select thread command
  170. self._respond('OK')
  171. elif command == 'qfThreadInfo':
  172. # Get list of threads.
  173. # Only one thread for now, can be extended to show one thread for each core,
  174. # if we dump both cores (e.g. on an interrupt watchdog)
  175. self._respond('m1')
  176. elif command == 'qC':
  177. # That single thread is selected.
  178. self._respond('QC1')
  179. elif command == 'g':
  180. # Registers read
  181. self._respond_regs()
  182. elif command.startswith('m'):
  183. # Memory read
  184. addr, size = [int(v, 16) for v in command[1:].split(',')]
  185. self._respond_mem(addr, size)
  186. elif command.startswith('vKill') or command == 'k':
  187. # Quit
  188. self._respond('OK')
  189. raise SystemExit(0)
  190. else:
  191. # Empty response required for any unknown command
  192. self._respond('')
  193. def _respond(self, data): # type: (str) -> None
  194. # calculate checksum
  195. data_bytes = bytes(data.encode('ascii')) # bytes() for Py2 compatibility
  196. checksum = sum(data_bytes) & 0xff
  197. # format and write the response
  198. res = '${}#{:02x}'.format(data, checksum)
  199. self.logger.debug('Wrote: %s', res)
  200. self.out_stream.write(res)
  201. self.out_stream.flush()
  202. # get the result ('+' or '-')
  203. ret = self.in_stream.read(1)
  204. self.logger.debug('Response: %s', ret)
  205. if ret != '+':
  206. sys.stderr.write("GDB responded with '-' to {}".format(res))
  207. raise SystemExit(1)
  208. def _respond_regs(self): # type: () -> None
  209. response = ''
  210. for reg_name in self.reg_list:
  211. # register values are reported as hexadecimal strings
  212. # in target byte order (i.e. LSB first for RISC-V)
  213. reg_val = self.panic_info.regs.get(reg_name, 0)
  214. reg_bytes = struct.pack('<L', reg_val)
  215. response += binascii.hexlify(reg_bytes).decode('ascii')
  216. self._respond(response)
  217. def _respond_mem(self, start_addr, size): # type: (int, int) -> None
  218. stack_addr_min = self.panic_info.stack_base_addr
  219. stack_data = self.panic_info.stack_data
  220. stack_len = len(self.panic_info.stack_data)
  221. stack_addr_max = stack_addr_min + stack_len
  222. # For any memory address that is not on the stack, pretend the value is 0x00.
  223. # GDB should never ask us for program memory, it will be obtained from the ELF file.
  224. def in_stack(addr): # type: (int) -> typing.Any[bool]
  225. return stack_addr_min <= addr < stack_addr_max
  226. result = ''
  227. for addr in range(start_addr, start_addr + size):
  228. if not in_stack(addr):
  229. result += '00'
  230. else:
  231. result += '{:02x}'.format(stack_data[addr - stack_addr_min])
  232. self._respond(result)
  233. def main(): # type: () -> None
  234. parser = argparse.ArgumentParser()
  235. parser.add_argument('input_file', type=argparse.FileType('r'),
  236. help='File containing the panic handler output')
  237. parser.add_argument('--target', choices=GDB_REGS_INFO.keys(),
  238. help='Chip to use (determines the architecture)')
  239. parser.add_argument('--gdb-log', default=None,
  240. help='If specified, the file for logging GDB server debug information')
  241. args = parser.parse_args()
  242. panic_info = PANIC_OUTPUT_PARSERS[args.target](args.input_file.read())
  243. server = GdbServer(panic_info, target=args.target, log_file=args.gdb_log)
  244. try:
  245. server.run()
  246. except KeyboardInterrupt:
  247. sys.exit(0)
  248. if __name__ == '__main__':
  249. main()