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]* +(?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 (arm-linux-gnueabihf-, aarch64-linux-gnu-). The resulting command 47is then expected to be found in the user's PATH. 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 174 def arch_prefix(self, cmd, elf): 175 self.set_arch(elf) 176 if self._arch is None: 177 return '' 178 return self._arch + cmd 179 180 def spawn_addr2line(self, elf_name): 181 if elf_name is None: 182 return 183 if self._addr2line_elf_name is elf_name: 184 return 185 if self._addr2line: 186 self._addr2line.terminate 187 self._addr2line = None 188 elf = self.get_elf(elf_name) 189 if not elf: 190 return 191 cmd = self.arch_prefix('addr2line', elf) 192 if not cmd: 193 return 194 self._addr2line = self.my_Popen([cmd, '-f', '-p', '-e', elf]) 195 self._addr2line_elf_name = elf_name 196 197 # If addr falls into a region that maps a TA ELF file, return the load 198 # address of that file. 199 def elf_load_addr(self, addr): 200 if self._regions: 201 for r in self._regions: 202 r_addr = int(r[0], 16) 203 r_size = int(r[1], 16) 204 i_addr = int(addr, 16) 205 if (i_addr >= r_addr and i_addr < (r_addr + r_size)): 206 # Found region 207 elf_idx = r[2] 208 if elf_idx is not None: 209 return self._elfs[int(elf_idx)][1] 210 # In case address is not found in TA ELF file, fallback to tee.elf 211 # especially to symbolize mixed (user-space and kernel) addresses 212 # which is true when syscall ftrace is enabled along with TA 213 # ftrace. 214 return self._tee_load_addr 215 else: 216 # tee.elf 217 return self._tee_load_addr 218 219 def elf_for_addr(self, addr): 220 l_addr = self.elf_load_addr(addr) 221 if l_addr == self._tee_load_addr: 222 return 'tee.elf' 223 for k in self._elfs: 224 e = self._elfs[k] 225 if int(e[1], 16) == int(l_addr, 16): 226 return e[0] 227 return None 228 229 def subtract_load_addr(self, addr): 230 l_addr = self.elf_load_addr(addr) 231 if l_addr is None: 232 return None 233 if int(l_addr, 16) > int(addr, 16): 234 return '' 235 return '0x{:x}'.format(int(addr, 16) - int(l_addr, 16)) 236 237 def resolve(self, addr): 238 reladdr = self.subtract_load_addr(addr) 239 self.spawn_addr2line(self.elf_for_addr(addr)) 240 if not reladdr or not self._addr2line: 241 return '???' 242 if self.elf_for_addr(addr) == 'tee.elf': 243 reladdr = '0x{:x}'.format(int(reladdr, 16) + 244 int(self.first_vma('tee.elf'), 16)) 245 try: 246 print(reladdr, file=self._addr2line.stdin) 247 ret = self._addr2line.stdout.readline().rstrip('\n') 248 except IOError: 249 ret = '!!!' 250 return ret 251 252 # Armv8.5 with Memory Tagging Extension (MTE) 253 def strip_armv85_mte_tag(self, addr): 254 i_addr = int(addr, 16) 255 i_addr &= ~(0xf << 56) 256 return '0x{:x}'.format(i_addr) 257 258 def symbol_plus_offset(self, addr): 259 ret = '' 260 prevsize = 0 261 addr = self.strip_armv85_mte_tag(addr) 262 reladdr = self.subtract_load_addr(addr) 263 elf_name = self.elf_for_addr(addr) 264 if elf_name is None: 265 return '' 266 elf = self.get_elf(elf_name) 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 cmd = self.arch_prefix('objdump', elf) 309 if not reladdr or not elf or not cmd: 310 return '' 311 iaddr = int(reladdr, 16) 312 objdump = self.my_Popen([cmd, '--section-headers', elf]) 313 for line in iter(objdump.stdout.readline, ''): 314 try: 315 idx, name, size, vma, lma, offs, algn = line.split() 316 except ValueError: 317 continue 318 ivma = int(vma, 16) 319 isize = int(size, 16) 320 if ivma == iaddr: 321 ret = name 322 break 323 if ivma < iaddr and ivma + isize >= iaddr: 324 offs = iaddr - ivma 325 ret = name + '+' + str(offs) 326 break 327 objdump.terminate() 328 return ret 329 330 def process_abort(self, line): 331 ret = '' 332 match = re.search(ABORT_ADDR_RE, line) 333 addr = match.group('addr') 334 pre = match.start('addr') 335 post = match.end('addr') 336 sym = self.symbol_plus_offset(addr) 337 sec = self.section_plus_offset(addr) 338 if sym or sec: 339 ret += line[:pre] 340 ret += addr 341 if sym: 342 ret += ' ' + sym 343 if sec: 344 ret += ' ' + sec 345 ret += line[post:] 346 return ret 347 348 # Return all ELF sections with the ALLOC flag 349 def read_sections(self, elf_name): 350 if elf_name is None: 351 return 352 if elf_name in self._sections: 353 return 354 elf = self.get_elf(elf_name) 355 if not elf: 356 return 357 cmd = self.arch_prefix('objdump', elf) 358 if not elf or not cmd: 359 return 360 self._sections[elf_name] = [] 361 objdump = self.my_Popen([cmd, '--section-headers', elf]) 362 for line in iter(objdump.stdout.readline, ''): 363 try: 364 _, name, size, vma, _, _, _ = line.split() 365 except ValueError: 366 if 'ALLOC' in line: 367 self._sections[elf_name].append([name, int(vma, 16), 368 int(size, 16)]) 369 370 def first_vma(self, elf_name): 371 self.read_sections(elf_name) 372 return '0x{:x}'.format(self._sections[elf_name][0][1]) 373 374 def overlaps(self, section, addr, size): 375 sec_addr = section[1] 376 sec_size = section[2] 377 if not size or not sec_size: 378 return False 379 return ((addr <= (sec_addr + sec_size - 1)) and 380 ((addr + size - 1) >= sec_addr)) 381 382 def sections_in_region(self, addr, size, elf_idx): 383 ret = '' 384 addr = self.subtract_load_addr(addr) 385 if not addr: 386 return '' 387 iaddr = int(addr, 16) 388 isize = int(size, 16) 389 elf = self._elfs[int(elf_idx)][0] 390 if elf is None: 391 return '' 392 self.read_sections(elf) 393 if elf not in self._sections: 394 return '' 395 for s in self._sections[elf]: 396 if self.overlaps(s, iaddr, isize): 397 ret += ' ' + s[0] 398 return ret 399 400 def reset(self): 401 self._call_stack_found = False 402 if self._addr2line: 403 self._addr2line.terminate() 404 self._addr2line = None 405 self._addr2line_elf_name = None 406 self._arch = None 407 self._saved_abort_line = '' 408 self._sections = {} # {elf_name: [[name, addr, size], ...], ...} 409 self._regions = [] # [[addr, size, elf_idx, saved line], ...] 410 self._elfs = {0: ["tee.elf", 0]} # {idx: [uuid, load_addr], ...} 411 self._tee_load_addr = '0x0' 412 self._func_graph_found = False 413 self._func_graph_skip_line = True 414 415 def pretty_print_path(self, path): 416 if self._strip_path: 417 return re.sub(re.escape(self._strip_path) + '/*', '', path) 418 return path 419 420 def write(self, line): 421 if self._call_stack_found: 422 match = re.search(STACK_ADDR_RE, line) 423 if match: 424 addr = match.group('addr') 425 pre = match.start('addr') 426 post = match.end('addr') 427 self._out.write(line[:pre]) 428 self._out.write(addr) 429 # The call stack contains return addresses (LR/ELR values). 430 # Heuristic: subtract 2 to obtain the call site of the function 431 # or the location of the exception. This value works for A64, 432 # A32 as well as Thumb. 433 pc = 0 434 lr = int(addr, 16) 435 if lr: 436 pc = lr - 2 437 res = self.resolve('0x{:x}'.format(pc)) 438 res = self.pretty_print_path(res) 439 self._out.write(' ' + res) 440 self._out.write(line[post:]) 441 return 442 else: 443 self.reset() 444 if self._func_graph_found: 445 match = re.search(GRAPH_ADDR_RE, line) 446 match_re = re.search(GRAPH_RE, line) 447 if match: 448 addr = match.group('addr') 449 pre = match.start('addr') 450 post = match.end('addr') 451 self._out.write(line[:pre]) 452 res = self.resolve(addr) 453 res_arr = re.split(' ', res) 454 self._out.write(res_arr[0]) 455 self._out.write(line[post:]) 456 self._func_graph_skip_line = False 457 return 458 elif match_re: 459 self._out.write(line) 460 return 461 elif self._func_graph_skip_line: 462 return 463 else: 464 self.reset() 465 match = re.search(REGION_RE, line) 466 if match: 467 # Region table: save info for later processing once 468 # we know which UUID corresponds to which ELF index 469 addr = match.group('addr') 470 size = match.group('size') 471 elf_idx = match.group('elf_idx') 472 self._regions.append([addr, size, elf_idx, line]) 473 return 474 match = re.search(ELF_LIST_RE, line) 475 if match: 476 # ELF list: save info for later. Region table and ELF list 477 # will be displayed when the call stack is reached 478 i = int(match.group('idx')) 479 self._elfs[i] = [match.group('uuid'), match.group('load_addr'), 480 line] 481 return 482 match = re.search(TA_PANIC_RE, line) 483 if match: 484 code = match.group('code') 485 if code in tee_result_names: 486 line = line.strip() + ' (' + tee_result_names[code] + ')\n' 487 self._out.write(line) 488 return 489 match = re.search(TEE_LOAD_ADDR_RE, line) 490 if match: 491 self._tee_load_addr = match.group('load_addr') 492 match = re.search(CALL_STACK_RE, line) 493 if match: 494 self._call_stack_found = True 495 if self._regions: 496 for r in self._regions: 497 r_addr = r[0] 498 r_size = r[1] 499 elf_idx = r[2] 500 saved_line = r[3] 501 if elf_idx is None: 502 self._out.write(saved_line) 503 else: 504 self._out.write(saved_line.strip() + 505 self.sections_in_region(r_addr, 506 r_size, 507 elf_idx) + 508 '\n') 509 if self._elfs: 510 for k in self._elfs: 511 e = self._elfs[k] 512 if (len(e) >= 3): 513 # TA executable or library 514 self._out.write(e[2].strip()) 515 elf = self.get_elf(e[0]) 516 if elf: 517 rpath = os.path.realpath(elf) 518 path = self.pretty_print_path(rpath) 519 self._out.write(' (' + path + ')') 520 self._out.write('\n') 521 # Here is a good place to resolve the abort address because we 522 # have all the information we need 523 if self._saved_abort_line: 524 self._out.write(self.process_abort(self._saved_abort_line)) 525 match = re.search(FUNC_GRAPH_RE, line) 526 if match: 527 self._func_graph_found = True 528 match = re.search(ABORT_ADDR_RE, line) 529 if match: 530 self.reset() 531 # At this point the arch and TA load address are unknown. 532 # Save the line so We can translate the abort address later. 533 self._saved_abort_line = line 534 self._out.write(line) 535 536 def flush(self): 537 self._out.flush() 538 539 540def main(): 541 args = get_args() 542 if args.dir: 543 # Flatten list in case -d is used several times *and* with multiple 544 # arguments 545 args.dirs = [item for sublist in args.dir for item in sublist] 546 else: 547 args.dirs = [] 548 symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path) 549 550 fd = sys.stdin.fileno() 551 isatty = os.isatty(fd) 552 if isatty: 553 old = termios.tcgetattr(fd) 554 new = termios.tcgetattr(fd) 555 new[3] = new[3] & ~termios.ECHO # lflags 556 try: 557 if isatty: 558 termios.tcsetattr(fd, termios.TCSADRAIN, new) 559 for line in sys.stdin: 560 symbolizer.write(line) 561 finally: 562 symbolizer.flush() 563 if isatty: 564 termios.tcsetattr(fd, termios.TCSADRAIN, old) 565 566 567if __name__ == "__main__": 568 main() 569