gdb_panic_server.py 11 KB

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