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 def symbol_plus_offset(self, addr): 253 ret = '' 254 prevsize = 0 255 reladdr = self.subtract_load_addr(addr) 256 elf_name = self.elf_for_addr(addr) 257 if elf_name is None: 258 return '' 259 elf = self.get_elf(elf_name) 260 cmd = self.arch_prefix('nm', elf) 261 if not reladdr or not elf or not cmd: 262 return '' 263 ireladdr = int(reladdr, 16) 264 nm = self.my_Popen([cmd, '--numeric-sort', '--print-size', elf]) 265 for line in iter(nm.stdout.readline, ''): 266 try: 267 addr, size, _, name = line.split() 268 except ValueError: 269 # Size is missing 270 try: 271 addr, _, name = line.split() 272 size = '0' 273 except ValueError: 274 # E.g., undefined (external) symbols (line = "U symbol") 275 continue 276 iaddr = int(addr, 16) 277 isize = int(size, 16) 278 if iaddr == ireladdr: 279 ret = name 280 break 281 if iaddr < ireladdr and iaddr + isize >= ireladdr: 282 offs = ireladdr - iaddr 283 ret = name + '+' + str(offs) 284 break 285 if iaddr > ireladdr and prevsize == 0: 286 offs = iaddr + ireladdr 287 ret = prevname + '+' + str(offs) 288 break 289 prevsize = size 290 prevname = name 291 nm.terminate() 292 return ret 293 294 def section_plus_offset(self, addr): 295 ret = '' 296 reladdr = self.subtract_load_addr(addr) 297 elf_name = self.elf_for_addr(addr) 298 if elf_name is None: 299 return '' 300 elf = self.get_elf(elf_name) 301 cmd = self.arch_prefix('objdump', elf) 302 if not reladdr or not elf or not cmd: 303 return '' 304 iaddr = int(reladdr, 16) 305 objdump = self.my_Popen([cmd, '--section-headers', elf]) 306 for line in iter(objdump.stdout.readline, ''): 307 try: 308 idx, name, size, vma, lma, offs, algn = line.split() 309 except ValueError: 310 continue 311 ivma = int(vma, 16) 312 isize = int(size, 16) 313 if ivma == iaddr: 314 ret = name 315 break 316 if ivma < iaddr and ivma + isize >= iaddr: 317 offs = iaddr - ivma 318 ret = name + '+' + str(offs) 319 break 320 objdump.terminate() 321 return ret 322 323 def process_abort(self, line): 324 ret = '' 325 match = re.search(ABORT_ADDR_RE, line) 326 addr = match.group('addr') 327 pre = match.start('addr') 328 post = match.end('addr') 329 sym = self.symbol_plus_offset(addr) 330 sec = self.section_plus_offset(addr) 331 if sym or sec: 332 ret += line[:pre] 333 ret += addr 334 if sym: 335 ret += ' ' + sym 336 if sec: 337 ret += ' ' + sec 338 ret += line[post:] 339 return ret 340 341 # Return all ELF sections with the ALLOC flag 342 def read_sections(self, elf_name): 343 if elf_name is None: 344 return 345 if elf_name in self._sections: 346 return 347 elf = self.get_elf(elf_name) 348 if not elf: 349 return 350 cmd = self.arch_prefix('objdump', elf) 351 if not elf or not cmd: 352 return 353 self._sections[elf_name] = [] 354 objdump = self.my_Popen([cmd, '--section-headers', elf]) 355 for line in iter(objdump.stdout.readline, ''): 356 try: 357 _, name, size, vma, _, _, _ = line.split() 358 except ValueError: 359 if 'ALLOC' in line: 360 self._sections[elf_name].append([name, int(vma, 16), 361 int(size, 16)]) 362 363 def first_vma(self, elf_name): 364 self.read_sections(elf_name) 365 return '0x{:x}'.format(self._sections[elf_name][0][1]) 366 367 def overlaps(self, section, addr, size): 368 sec_addr = section[1] 369 sec_size = section[2] 370 if not size or not sec_size: 371 return False 372 return ((addr <= (sec_addr + sec_size - 1)) and 373 ((addr + size - 1) >= sec_addr)) 374 375 def sections_in_region(self, addr, size, elf_idx): 376 ret = '' 377 addr = self.subtract_load_addr(addr) 378 if not addr: 379 return '' 380 iaddr = int(addr, 16) 381 isize = int(size, 16) 382 elf = self._elfs[int(elf_idx)][0] 383 if elf is None: 384 return '' 385 self.read_sections(elf) 386 if elf not in self._sections: 387 return '' 388 for s in self._sections[elf]: 389 if self.overlaps(s, iaddr, isize): 390 ret += ' ' + s[0] 391 return ret 392 393 def reset(self): 394 self._call_stack_found = False 395 if self._addr2line: 396 self._addr2line.terminate() 397 self._addr2line = None 398 self._addr2line_elf_name = None 399 self._arch = None 400 self._saved_abort_line = '' 401 self._sections = {} # {elf_name: [[name, addr, size], ...], ...} 402 self._regions = [] # [[addr, size, elf_idx, saved line], ...] 403 self._elfs = {0: ["tee.elf", 0]} # {idx: [uuid, load_addr], ...} 404 self._tee_load_addr = '0x0' 405 self._func_graph_found = False 406 self._func_graph_skip_line = True 407 408 def pretty_print_path(self, path): 409 if self._strip_path: 410 return re.sub(re.escape(self._strip_path) + '/*', '', path) 411 return path 412 413 def write(self, line): 414 if self._call_stack_found: 415 match = re.search(STACK_ADDR_RE, line) 416 if match: 417 addr = match.group('addr') 418 pre = match.start('addr') 419 post = match.end('addr') 420 self._out.write(line[:pre]) 421 self._out.write(addr) 422 # The call stack contains return addresses (LR/ELR values). 423 # Heuristic: subtract 2 to obtain the call site of the function 424 # or the location of the exception. This value works for A64, 425 # A32 as well as Thumb. 426 pc = 0 427 lr = int(addr, 16) 428 if lr: 429 pc = lr - 2 430 res = self.resolve('0x{:x}'.format(pc)) 431 res = self.pretty_print_path(res) 432 self._out.write(' ' + res) 433 self._out.write(line[post:]) 434 return 435 else: 436 self.reset() 437 if self._func_graph_found: 438 match = re.search(GRAPH_ADDR_RE, line) 439 match_re = re.search(GRAPH_RE, line) 440 if match: 441 addr = match.group('addr') 442 pre = match.start('addr') 443 post = match.end('addr') 444 self._out.write(line[:pre]) 445 res = self.resolve(addr) 446 res_arr = re.split(' ', res) 447 self._out.write(res_arr[0]) 448 self._out.write(line[post:]) 449 self._func_graph_skip_line = False 450 return 451 elif match_re: 452 self._out.write(line) 453 return 454 elif self._func_graph_skip_line: 455 return 456 else: 457 self.reset() 458 match = re.search(REGION_RE, line) 459 if match: 460 # Region table: save info for later processing once 461 # we know which UUID corresponds to which ELF index 462 addr = match.group('addr') 463 size = match.group('size') 464 elf_idx = match.group('elf_idx') 465 self._regions.append([addr, size, elf_idx, line]) 466 return 467 match = re.search(ELF_LIST_RE, line) 468 if match: 469 # ELF list: save info for later. Region table and ELF list 470 # will be displayed when the call stack is reached 471 i = int(match.group('idx')) 472 self._elfs[i] = [match.group('uuid'), match.group('load_addr'), 473 line] 474 return 475 match = re.search(TA_PANIC_RE, line) 476 if match: 477 code = match.group('code') 478 if code in tee_result_names: 479 line = line.strip() + ' (' + tee_result_names[code] + ')\n' 480 self._out.write(line) 481 return 482 match = re.search(TEE_LOAD_ADDR_RE, line) 483 if match: 484 self._tee_load_addr = match.group('load_addr') 485 match = re.search(CALL_STACK_RE, line) 486 if match: 487 self._call_stack_found = True 488 if self._regions: 489 for r in self._regions: 490 r_addr = r[0] 491 r_size = r[1] 492 elf_idx = r[2] 493 saved_line = r[3] 494 if elf_idx is None: 495 self._out.write(saved_line) 496 else: 497 self._out.write(saved_line.strip() + 498 self.sections_in_region(r_addr, 499 r_size, 500 elf_idx) + 501 '\n') 502 if self._elfs: 503 for k in self._elfs: 504 e = self._elfs[k] 505 if (len(e) >= 3): 506 # TA executable or library 507 self._out.write(e[2].strip()) 508 elf = self.get_elf(e[0]) 509 if elf: 510 rpath = os.path.realpath(elf) 511 path = self.pretty_print_path(rpath) 512 self._out.write(' (' + path + ')') 513 self._out.write('\n') 514 # Here is a good place to resolve the abort address because we 515 # have all the information we need 516 if self._saved_abort_line: 517 self._out.write(self.process_abort(self._saved_abort_line)) 518 match = re.search(FUNC_GRAPH_RE, line) 519 if match: 520 self._func_graph_found = True 521 match = re.search(ABORT_ADDR_RE, line) 522 if match: 523 self.reset() 524 # At this point the arch and TA load address are unknown. 525 # Save the line so We can translate the abort address later. 526 self._saved_abort_line = line 527 self._out.write(line) 528 529 def flush(self): 530 self._out.flush() 531 532 533def main(): 534 args = get_args() 535 if args.dir: 536 # Flatten list in case -d is used several times *and* with multiple 537 # arguments 538 args.dirs = [item for sublist in args.dir for item in sublist] 539 else: 540 args.dirs = [] 541 symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path) 542 543 fd = sys.stdin.fileno() 544 isatty = os.isatty(fd) 545 if isatty: 546 old = termios.tcgetattr(fd) 547 new = termios.tcgetattr(fd) 548 new[3] = new[3] & ~termios.ECHO # lflags 549 try: 550 if isatty: 551 termios.tcsetattr(fd, termios.TCSADRAIN, new) 552 for line in sys.stdin: 553 symbolizer.write(line) 554 finally: 555 symbolizer.flush() 556 if isatty: 557 termios.tcsetattr(fd, termios.TCSADRAIN, old) 558 559 560if __name__ == "__main__": 561 main() 562