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