1#!/usr/bin/env python3 2# SPDX-License-Identifier: BSD-2-Clause 3# 4# Copyright (c) 2017, Linaro Limited 5# 6 7 8import argparse 9import errno 10import glob 11import os 12import re 13import subprocess 14import sys 15import termios 16 17CALL_STACK_RE = re.compile('Call stack:') 18TEE_LOAD_ADDR_RE = re.compile(r'TEE load address @ (?P<load_addr>0x[0-9a-f]+)') 19# This gets the address from lines looking like this: 20# E/TC:0 0x001044a8 21STACK_ADDR_RE = re.compile( 22 r'[UEIDFM]/(TC|LD):([0-9]+ )?(\?*|[0-9]*) [0-9]* +(?P<addr>0x[0-9a-f]+)') 23ABORT_ADDR_RE = re.compile(r'-abort at address (?P<addr>0x[0-9a-f]+)') 24TA_PANIC_RE = re.compile(r'TA panicked with code (?P<code>0x[0-9a-f]+)') 25REGION_RE = re.compile(r'region +[0-9]+: va (?P<addr>0x[0-9a-f]+) ' 26 r'pa 0x[0-9a-f]+ size (?P<size>0x[0-9a-f]+)' 27 r'( flags .{4} (\[(?P<elf_idx>[0-9]+)\])?)?') 28ELF_LIST_RE = re.compile(r'\[(?P<idx>[0-9]+)\] (?P<uuid>[0-9a-f\-]+)' 29 r' @ (?P<load_addr>0x[0-9a-f\-]+)') 30FUNC_GRAPH_RE = re.compile(r'Function graph') 31GRAPH_ADDR_RE = re.compile(r'(?P<addr>0x[0-9a-f]+)') 32GRAPH_RE = re.compile(r'}') 33 34epilog = ''' 35This scripts reads an OP-TEE abort or panic message from stdin and adds debug 36information to the output, such as '<function> at <file>:<line>' next to each 37address in the call stack. Any message generated by OP-TEE and containing a 38call stack can in principle be processed by this script. This currently 39includes aborts and panics from the TEE core as well as from any TA. 40The paths provided on the command line are used to locate the appropriate ELF 41binary (tee.elf or Trusted Application). The GNU binutils (addr2line, objdump, 42nm) are used to extract the debug info. If the CROSS_COMPILE environment 43variable is set, it is used as a prefix to the binutils tools. That is, the 44script will invoke $(CROSS_COMPILE)addr2line etc. If it is not set however, 45the prefix will be determined automatically for each ELF file based on its 46architecture. The resulting command is then expected to be found in the user's 47PATH. 48 49OP-TEE abort and panic messages are sent to the secure console. They look like 50the following: 51 52 E/TC:0 User TA data-abort at address 0xffffdecd (alignment fault) 53 ... 54 E/TC:0 Call stack: 55 E/TC:0 0x4000549e 56 E/TC:0 0x40001f4b 57 E/TC:0 0x4000273f 58 E/TC:0 0x40005da7 59 60Inspired by a script of the same name by the Chromium project. 61 62Sample usage: 63 64 $ scripts/symbolize.py -d out/arm-plat-hikey/core -d ../optee_test/out/ta/* 65 <paste whole dump here> 66 ^D 67 68Also, this script reads function graph generated for OP-TEE user TA from 69/tmp/ftrace-<ta_uuid>.out file and resolves function addresses to corresponding 70symbols. 71 72Sample usage: 73 74 $ cat /tmp/ftrace-<ta_uuid>.out | scripts/symbolize.py -d <ta_uuid>.elf 75 <paste function graph here> 76 ^D 77''' 78 79tee_result_names = { 80 '0xf0100001': 'TEE_ERROR_CORRUPT_OBJECT', 81 '0xf0100002': 'TEE_ERROR_CORRUPT_OBJECT_2', 82 '0xf0100003': 'TEE_ERROR_STORAGE_NOT_AVAILABLE', 83 '0xf0100004': 'TEE_ERROR_STORAGE_NOT_AVAILABLE_2', 84 '0xf0100006': 'TEE_ERROR_CIPHERTEXT_INVALID ', 85 '0xffff0000': 'TEE_ERROR_GENERIC', 86 '0xffff0001': 'TEE_ERROR_ACCESS_DENIED', 87 '0xffff0002': 'TEE_ERROR_CANCEL', 88 '0xffff0003': 'TEE_ERROR_ACCESS_CONFLICT', 89 '0xffff0004': 'TEE_ERROR_EXCESS_DATA', 90 '0xffff0005': 'TEE_ERROR_BAD_FORMAT', 91 '0xffff0006': 'TEE_ERROR_BAD_PARAMETERS', 92 '0xffff0007': 'TEE_ERROR_BAD_STATE', 93 '0xffff0008': 'TEE_ERROR_ITEM_NOT_FOUND', 94 '0xffff0009': 'TEE_ERROR_NOT_IMPLEMENTED', 95 '0xffff000a': 'TEE_ERROR_NOT_SUPPORTED', 96 '0xffff000b': 'TEE_ERROR_NO_DATA', 97 '0xffff000c': 'TEE_ERROR_OUT_OF_MEMORY', 98 '0xffff000d': 'TEE_ERROR_BUSY', 99 '0xffff000e': 'TEE_ERROR_COMMUNICATION', 100 '0xffff000f': 'TEE_ERROR_SECURITY', 101 '0xffff0010': 'TEE_ERROR_SHORT_BUFFER', 102 '0xffff0011': 'TEE_ERROR_EXTERNAL_CANCEL', 103 '0xffff300f': 'TEE_ERROR_OVERFLOW', 104 '0xffff3024': 'TEE_ERROR_TARGET_DEAD', 105 '0xffff3041': 'TEE_ERROR_STORAGE_NO_SPACE', 106 '0xffff3071': 'TEE_ERROR_MAC_INVALID', 107 '0xffff3072': 'TEE_ERROR_SIGNATURE_INVALID', 108 '0xffff5000': 'TEE_ERROR_TIME_NOT_SET', 109 '0xffff5001': 'TEE_ERROR_TIME_NEEDS_RESET', 110 } 111 112 113def get_args(): 114 parser = argparse.ArgumentParser( 115 formatter_class=argparse.RawDescriptionHelpFormatter, 116 description='Symbolizes OP-TEE abort dumps or function graphs', 117 epilog=epilog) 118 parser.add_argument('-d', '--dir', action='append', nargs='+', 119 help='Search for ELF file in DIR. tee.elf is needed ' 120 'to decode a TEE Core or pseudo-TA abort, while ' 121 '<TA_uuid>.elf is required if a user-mode TA has ' 122 'crashed. For convenience, ELF files may also be ' 123 'given.') 124 parser.add_argument('-s', '--strip_path', nargs='?', 125 help='Strip STRIP_PATH from file paths (default: ' 126 'current directory, use -s with no argument to show ' 127 'full paths)', default=os.getcwd()) 128 129 return parser.parse_args() 130 131 132class Symbolizer(object): 133 def __init__(self, out, dirs, strip_path): 134 self._out = out 135 self._dirs = dirs 136 self._strip_path = strip_path 137 self._addr2line = None 138 self.reset() 139 140 def my_Popen(self, cmd): 141 try: 142 return subprocess.Popen(cmd, stdin=subprocess.PIPE, 143 stdout=subprocess.PIPE, 144 universal_newlines=True, 145 bufsize=1) 146 except OSError as e: 147 if e.errno == errno.ENOENT: 148 print("*** Error:{}: command not found".format(cmd[0]), 149 file=sys.stderr) 150 sys.exit(1) 151 152 def get_elf(self, elf_or_uuid): 153 if not elf_or_uuid.endswith('.elf'): 154 elf_or_uuid += '.elf' 155 for d in self._dirs: 156 if d.endswith(elf_or_uuid) and os.path.isfile(d): 157 return d 158 elf = glob.glob(d + '/' + elf_or_uuid) 159 if elf: 160 return elf[0] 161 162 def set_arch(self, elf): 163 self._arch = os.getenv('CROSS_COMPILE') 164 if self._arch: 165 return 166 p = subprocess.Popen(['file', '-L', elf], stdout=subprocess.PIPE) 167 output = p.stdout.readlines() 168 p.terminate() 169 if b'ARM aarch64,' in output[0]: 170 self._arch = 'aarch64-linux-gnu-' 171 elif b'ARM,' in output[0]: 172 self._arch = 'arm-linux-gnueabihf-' 173 elif b'RISC-V,' in output[0]: 174 if b'32-bit' in output[0]: 175 self._arch = 'riscv32-unknown-linux-gnu-' 176 elif b'64-bit' in output[0]: 177 self._arch = 'riscv64-unknown-linux-gnu-' 178 179 def arch_prefix(self, cmd, elf): 180 self.set_arch(elf) 181 if self._arch is None: 182 return '' 183 return self._arch + cmd 184 185 def spawn_addr2line(self, elf_name): 186 if elf_name is None: 187 return 188 if self._addr2line_elf_name is elf_name: 189 return 190 if self._addr2line: 191 self._addr2line.terminate 192 self._addr2line = None 193 elf = self.get_elf(elf_name) 194 if not elf: 195 return 196 cmd = self.arch_prefix('addr2line', elf) 197 if not cmd: 198 return 199 self._addr2line = self.my_Popen([cmd, '-f', '-p', '-e', elf]) 200 self._addr2line_elf_name = elf_name 201 202 # If addr falls into a region that maps a TA ELF file, return the load 203 # address of that file. 204 def elf_load_addr(self, addr): 205 if self._regions: 206 for r in self._regions: 207 r_addr = int(r[0], 16) 208 r_size = int(r[1], 16) 209 i_addr = int(addr, 16) 210 if (i_addr >= r_addr and i_addr < (r_addr + r_size)): 211 # Found region 212 elf_idx = r[2] 213 if elf_idx is not None: 214 return self._elfs[int(elf_idx)][1] 215 # In case address is not found in TA ELF file, fallback to tee.elf 216 # especially to symbolize mixed (user-space and kernel) addresses 217 # which is true when syscall ftrace is enabled along with TA 218 # ftrace. 219 return self._tee_load_addr 220 else: 221 # tee.elf 222 return self._tee_load_addr 223 224 def elf_for_addr(self, addr): 225 l_addr = self.elf_load_addr(addr) 226 if l_addr == self._tee_load_addr: 227 return 'tee.elf' 228 for k in self._elfs: 229 e = self._elfs[k] 230 if int(e[1], 16) == int(l_addr, 16): 231 return e[0] 232 return None 233 234 def subtract_load_addr(self, addr): 235 l_addr = self.elf_load_addr(addr) 236 if l_addr is None: 237 return None 238 if int(l_addr, 16) > int(addr, 16): 239 return '' 240 return '0x{:x}'.format(int(addr, 16) - int(l_addr, 16)) 241 242 def resolve(self, addr): 243 reladdr = self.subtract_load_addr(addr) 244 self.spawn_addr2line(self.elf_for_addr(addr)) 245 if not reladdr or not self._addr2line: 246 return '???' 247 if self.elf_for_addr(addr) == 'tee.elf': 248 reladdr = '0x{:x}'.format(int(reladdr, 16) + 249 int(self.first_vma('tee.elf'), 16)) 250 try: 251 print(reladdr, file=self._addr2line.stdin) 252 ret = self._addr2line.stdout.readline().rstrip('\n') 253 except IOError: 254 ret = '!!!' 255 return ret 256 257 def symbol_plus_offset(self, addr): 258 ret = '' 259 prevsize = 0 260 reladdr = self.subtract_load_addr(addr) 261 elf_name = self.elf_for_addr(addr) 262 if elf_name is None: 263 return '' 264 elf = self.get_elf(elf_name) 265 if elf is None: 266 return '' 267 cmd = self.arch_prefix('nm', elf) 268 if not reladdr or not elf or not cmd: 269 return '' 270 ireladdr = int(reladdr, 16) 271 nm = self.my_Popen([cmd, '--numeric-sort', '--print-size', elf]) 272 for line in iter(nm.stdout.readline, ''): 273 try: 274 addr, size, _, name = line.split() 275 except ValueError: 276 # Size is missing 277 try: 278 addr, _, name = line.split() 279 size = '0' 280 except ValueError: 281 # E.g., undefined (external) symbols (line = "U symbol") 282 continue 283 iaddr = int(addr, 16) 284 isize = int(size, 16) 285 if iaddr == ireladdr: 286 ret = name 287 break 288 if iaddr < ireladdr and iaddr + isize >= ireladdr: 289 offs = ireladdr - iaddr 290 ret = name + '+' + str(offs) 291 break 292 if iaddr > ireladdr and prevsize == 0: 293 offs = iaddr + ireladdr 294 ret = prevname + '+' + str(offs) 295 break 296 prevsize = size 297 prevname = name 298 nm.terminate() 299 return ret 300 301 def section_plus_offset(self, addr): 302 ret = '' 303 reladdr = self.subtract_load_addr(addr) 304 elf_name = self.elf_for_addr(addr) 305 if elf_name is None: 306 return '' 307 elf = self.get_elf(elf_name) 308 if elf is None: 309 return '' 310 cmd = self.arch_prefix('objdump', elf) 311 if not reladdr or not elf or not cmd: 312 return '' 313 iaddr = int(reladdr, 16) 314 objdump = self.my_Popen([cmd, '--section-headers', elf]) 315 for line in iter(objdump.stdout.readline, ''): 316 try: 317 idx, name, size, vma, lma, offs, algn = line.split() 318 except ValueError: 319 continue 320 ivma = int(vma, 16) 321 isize = int(size, 16) 322 if ivma == iaddr: 323 ret = name 324 break 325 if ivma < iaddr and ivma + isize >= iaddr: 326 offs = iaddr - ivma 327 ret = name + '+' + str(offs) 328 break 329 objdump.terminate() 330 return ret 331 332 def process_abort(self, line): 333 ret = '' 334 match = re.search(ABORT_ADDR_RE, line) 335 addr = match.group('addr') 336 pre = match.start('addr') 337 post = match.end('addr') 338 sym = self.symbol_plus_offset(addr) 339 sec = self.section_plus_offset(addr) 340 if sym or sec: 341 ret += line[:pre] 342 ret += addr 343 if sym: 344 ret += ' ' + sym 345 if sec: 346 ret += ' ' + sec 347 ret += line[post:] 348 return ret 349 350 # Return all ELF sections with the ALLOC flag 351 def read_sections(self, elf_name): 352 if elf_name is None: 353 return 354 if elf_name in self._sections: 355 return 356 elf = self.get_elf(elf_name) 357 if not elf: 358 return 359 cmd = self.arch_prefix('objdump', elf) 360 if not elf or not cmd: 361 return 362 self._sections[elf_name] = [] 363 objdump = self.my_Popen([cmd, '--section-headers', elf]) 364 for line in iter(objdump.stdout.readline, ''): 365 try: 366 _, name, size, vma, _, _, _ = line.split() 367 except ValueError: 368 if 'ALLOC' in line: 369 self._sections[elf_name].append([name, int(vma, 16), 370 int(size, 16)]) 371 372 def first_vma(self, elf_name): 373 self.read_sections(elf_name) 374 return '0x{:x}'.format(self._sections[elf_name][0][1]) 375 376 def overlaps(self, section, addr, size): 377 sec_addr = section[1] 378 sec_size = section[2] 379 if not size or not sec_size: 380 return False 381 return ((addr <= (sec_addr + sec_size - 1)) and 382 ((addr + size - 1) >= sec_addr)) 383 384 def sections_in_region(self, addr, size, elf_idx): 385 ret = '' 386 addr = self.subtract_load_addr(addr) 387 if not addr: 388 return '' 389 iaddr = int(addr, 16) 390 isize = int(size, 16) 391 elf = self._elfs[int(elf_idx)][0] 392 if elf is None: 393 return '' 394 self.read_sections(elf) 395 if elf not in self._sections: 396 return '' 397 for s in self._sections[elf]: 398 if self.overlaps(s, iaddr, isize): 399 ret += ' ' + s[0] 400 return ret 401 402 def reset(self): 403 self._call_stack_found = False 404 if self._addr2line: 405 self._addr2line.terminate() 406 self._addr2line = None 407 self._addr2line_elf_name = None 408 self._arch = None 409 self._saved_abort_line = '' 410 self._sections = {} # {elf_name: [[name, addr, size], ...], ...} 411 self._regions = [] # [[addr, size, elf_idx, saved line], ...] 412 self._elfs = {0: ["tee.elf", 0]} # {idx: [uuid, load_addr], ...} 413 self._tee_load_addr = '0x0' 414 self._func_graph_found = False 415 self._func_graph_skip_line = True 416 417 def pretty_print_path(self, path): 418 if self._strip_path: 419 return re.sub(re.escape(self._strip_path) + '/*', '', path) 420 return path 421 422 def write(self, line): 423 if self._call_stack_found: 424 match = re.search(STACK_ADDR_RE, line) 425 if match: 426 addr = match.group('addr') 427 pre = match.start('addr') 428 post = match.end('addr') 429 self._out.write(line[:pre]) 430 self._out.write(addr) 431 # The call stack contains return addresses (LR/ELR values). 432 # Heuristic: subtract 2 to obtain the call site of the function 433 # or the location of the exception. This value works for A64, 434 # A32 as well as Thumb. 435 pc = 0 436 lr = int(addr, 16) 437 if lr: 438 pc = lr - 2 439 res = self.resolve('0x{:x}'.format(pc)) 440 res = self.pretty_print_path(res) 441 self._out.write(' ' + res) 442 self._out.write(line[post:]) 443 return 444 else: 445 self.reset() 446 if self._func_graph_found: 447 match = re.search(GRAPH_ADDR_RE, line) 448 match_re = re.search(GRAPH_RE, line) 449 if match: 450 addr = match.group('addr') 451 pre = match.start('addr') 452 post = match.end('addr') 453 self._out.write(line[:pre]) 454 res = self.resolve(addr) 455 res_arr = re.split(' ', res) 456 self._out.write(res_arr[0]) 457 self._out.write(line[post:]) 458 self._func_graph_skip_line = False 459 return 460 elif match_re: 461 self._out.write(line) 462 return 463 elif self._func_graph_skip_line: 464 return 465 else: 466 self.reset() 467 match = re.search(REGION_RE, line) 468 if match: 469 # Region table: save info for later processing once 470 # we know which UUID corresponds to which ELF index 471 addr = match.group('addr') 472 size = match.group('size') 473 elf_idx = match.group('elf_idx') 474 self._regions.append([addr, size, elf_idx, line]) 475 return 476 match = re.search(ELF_LIST_RE, line) 477 if match: 478 # ELF list: save info for later. Region table and ELF list 479 # will be displayed when the call stack is reached 480 i = int(match.group('idx')) 481 self._elfs[i] = [match.group('uuid'), match.group('load_addr'), 482 line] 483 return 484 match = re.search(TA_PANIC_RE, line) 485 if match: 486 code = match.group('code') 487 if code in tee_result_names: 488 line = line.strip() + ' (' + tee_result_names[code] + ')\n' 489 self._out.write(line) 490 return 491 match = re.search(TEE_LOAD_ADDR_RE, line) 492 if match: 493 self._tee_load_addr = match.group('load_addr') 494 match = re.search(CALL_STACK_RE, line) 495 if match: 496 self._call_stack_found = True 497 if self._regions: 498 for r in self._regions: 499 r_addr = r[0] 500 r_size = r[1] 501 elf_idx = r[2] 502 saved_line = r[3] 503 if elf_idx is None: 504 self._out.write(saved_line) 505 else: 506 self._out.write(saved_line.strip() + 507 self.sections_in_region(r_addr, 508 r_size, 509 elf_idx) + 510 '\n') 511 if self._elfs: 512 for k in self._elfs: 513 e = self._elfs[k] 514 if (len(e) >= 3): 515 # TA executable or library 516 self._out.write(e[2].strip()) 517 elf = self.get_elf(e[0]) 518 if elf: 519 rpath = os.path.realpath(elf) 520 path = self.pretty_print_path(rpath) 521 self._out.write(' (' + path + ')') 522 self._out.write('\n') 523 # Here is a good place to resolve the abort address because we 524 # have all the information we need 525 if self._saved_abort_line: 526 self._out.write(self.process_abort(self._saved_abort_line)) 527 match = re.search(FUNC_GRAPH_RE, line) 528 if match: 529 self._func_graph_found = True 530 match = re.search(ABORT_ADDR_RE, line) 531 if match: 532 self.reset() 533 # At this point the arch and TA load address are unknown. 534 # Save the line so We can translate the abort address later. 535 self._saved_abort_line = line 536 self._out.write(line) 537 538 def flush(self): 539 self._out.flush() 540 541 542def main(): 543 args = get_args() 544 if args.dir: 545 # Flatten list in case -d is used several times *and* with multiple 546 # arguments 547 args.dirs = [item for sublist in args.dir for item in sublist] 548 else: 549 args.dirs = [] 550 symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path) 551 552 fd = sys.stdin.fileno() 553 isatty = os.isatty(fd) 554 if isatty: 555 old = termios.tcgetattr(fd) 556 new = termios.tcgetattr(fd) 557 new[3] = new[3] & ~termios.ECHO # lflags 558 try: 559 if isatty: 560 termios.tcsetattr(fd, termios.TCSADRAIN, new) 561 for line in sys.stdin: 562 symbolizer.write(line) 563 finally: 564 symbolizer.flush() 565 if isatty: 566 termios.tcsetattr(fd, termios.TCSADRAIN, old) 567 568 569if __name__ == "__main__": 570 main() 571