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