#!/usr/bin/env python3
|
# SPDX-License-Identifier: BSD-2-Clause
|
#
|
# Copyright (c) 2017, Linaro Limited
|
#
|
|
|
import argparse
|
import errno
|
import glob
|
import os
|
import re
|
import subprocess
|
import sys
|
import termios
|
|
CALL_STACK_RE = re.compile('Call stack:')
|
TEE_LOAD_ADDR_RE = re.compile(r'TEE load address @ (?P<load_addr>0x[0-9a-f]+)')
|
# This gets the address from lines looking like this:
|
# E/TC:0 0x001044a8
|
STACK_ADDR_RE = re.compile(
|
r'[UEIDFM]/(TC|LD):(\?*|[0-9]*) [0-9]* +(?P<addr>0x[0-9a-f]+)')
|
ABORT_ADDR_RE = re.compile(r'-abort at address (?P<addr>0x[0-9a-f]+)')
|
REGION_RE = re.compile(r'region +[0-9]+: va (?P<addr>0x[0-9a-f]+) '
|
r'pa 0x[0-9a-f]+ size (?P<size>0x[0-9a-f]+)'
|
r'( flags .{4} (\[(?P<elf_idx>[0-9]+)\])?)?')
|
ELF_LIST_RE = re.compile(r'\[(?P<idx>[0-9]+)\] (?P<uuid>[0-9a-f\-]+)'
|
r' @ (?P<load_addr>0x[0-9a-f\-]+)')
|
FUNC_GRAPH_RE = re.compile(r'Function graph')
|
GRAPH_ADDR_RE = re.compile(r'(?P<addr>0x[0-9a-f]+)')
|
GRAPH_RE = re.compile(r'}')
|
|
epilog = '''
|
This scripts reads an OP-TEE abort or panic message from stdin and adds debug
|
information to the output, such as '<function> at <file>:<line>' next to each
|
address in the call stack. Any message generated by OP-TEE and containing a
|
call stack can in principle be processed by this script. This currently
|
includes aborts and panics from the TEE core as well as from any TA.
|
The paths provided on the command line are used to locate the appropriate ELF
|
binary (tee.elf or Trusted Application). The GNU binutils (addr2line, objdump,
|
nm) are used to extract the debug info. If the CROSS_COMPILE environment
|
variable is set, it is used as a prefix to the binutils tools. That is, the
|
script will invoke $(CROSS_COMPILE)addr2line etc. If it is not set however,
|
the prefix will be determined automatically for each ELF file based on its
|
architecture (arm-linux-gnueabihf-, aarch64-linux-gnu-). The resulting command
|
is then expected to be found in the user's PATH.
|
|
OP-TEE abort and panic messages are sent to the secure console. They look like
|
the following:
|
|
E/TC:0 User TA data-abort at address 0xffffdecd (alignment fault)
|
...
|
E/TC:0 Call stack:
|
E/TC:0 0x4000549e
|
E/TC:0 0x40001f4b
|
E/TC:0 0x4000273f
|
E/TC:0 0x40005da7
|
|
Inspired by a script of the same name by the Chromium project.
|
|
Sample usage:
|
|
$ scripts/symbolize.py -d out/arm-plat-hikey/core -d ../optee_test/out/ta/*
|
<paste whole dump here>
|
^D
|
|
Also, this script reads function graph generated for OP-TEE user TA from
|
/tmp/ftrace-<ta_uuid>.out file and resolves function addresses to corresponding
|
symbols.
|
|
Sample usage:
|
|
$ cat /tmp/ftrace-<ta_uuid>.out | scripts/symbolize.py -d <ta_uuid>.elf
|
<paste function graph here>
|
^D
|
'''
|
|
|
def get_args():
|
parser = argparse.ArgumentParser(
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
description='Symbolizes OP-TEE abort dumps or function graphs',
|
epilog=epilog)
|
parser.add_argument('-d', '--dir', action='append', nargs='+',
|
help='Search for ELF file in DIR. tee.elf is needed '
|
'to decode a TEE Core or pseudo-TA abort, while '
|
'<TA_uuid>.elf is required if a user-mode TA has '
|
'crashed. For convenience, ELF files may also be '
|
'given.')
|
parser.add_argument('-s', '--strip_path', nargs='?',
|
help='Strip STRIP_PATH from file paths (default: '
|
'current directory, use -s with no argument to show '
|
'full paths)', default=os.getcwd())
|
|
return parser.parse_args()
|
|
|
class Symbolizer(object):
|
def __init__(self, out, dirs, strip_path):
|
self._out = out
|
self._dirs = dirs
|
self._strip_path = strip_path
|
self._addr2line = None
|
self.reset()
|
|
def my_Popen(self, cmd):
|
try:
|
return subprocess.Popen(cmd, stdin=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
universal_newlines=True,
|
bufsize=1)
|
except OSError as e:
|
if e.errno == errno.ENOENT:
|
print("*** Error:{}: command not found".format(cmd[0]),
|
file=sys.stderr)
|
sys.exit(1)
|
|
def get_elf(self, elf_or_uuid):
|
if not elf_or_uuid.endswith('.elf'):
|
elf_or_uuid += '.elf'
|
for d in self._dirs:
|
if d.endswith(elf_or_uuid) and os.path.isfile(d):
|
return d
|
elf = glob.glob(d + '/' + elf_or_uuid)
|
if elf:
|
return elf[0]
|
|
def set_arch(self, elf):
|
self._arch = os.getenv('CROSS_COMPILE')
|
if self._arch:
|
return
|
p = subprocess.Popen(['file', '-L', elf], stdout=subprocess.PIPE)
|
output = p.stdout.readlines()
|
p.terminate()
|
if b'ARM aarch64,' in output[0]:
|
self._arch = 'aarch64-linux-gnu-'
|
elif b'ARM,' in output[0]:
|
self._arch = 'arm-linux-gnueabihf-'
|
|
def arch_prefix(self, cmd, elf):
|
self.set_arch(elf)
|
if self._arch is None:
|
return ''
|
return self._arch + cmd
|
|
def spawn_addr2line(self, elf_name):
|
if elf_name is None:
|
return
|
if self._addr2line_elf_name is elf_name:
|
return
|
if self._addr2line:
|
self._addr2line.terminate
|
self._addr2line = None
|
elf = self.get_elf(elf_name)
|
if not elf:
|
return
|
cmd = self.arch_prefix('addr2line', elf)
|
if not cmd:
|
return
|
self._addr2line = self.my_Popen([cmd, '-f', '-p', '-e', elf])
|
self._addr2line_elf_name = elf_name
|
|
# If addr falls into a region that maps a TA ELF file, return the load
|
# address of that file.
|
def elf_load_addr(self, addr):
|
if self._regions:
|
for r in self._regions:
|
r_addr = int(r[0], 16)
|
r_size = int(r[1], 16)
|
i_addr = int(addr, 16)
|
if (i_addr >= r_addr and i_addr < (r_addr + r_size)):
|
# Found region
|
elf_idx = r[2]
|
if elf_idx is not None:
|
return self._elfs[int(elf_idx)][1]
|
# In case address is not found in TA ELF file, fallback to tee.elf
|
# especially to symbolize mixed (user-space and kernel) addresses
|
# which is true when syscall ftrace is enabled along with TA
|
# ftrace.
|
return self._tee_load_addr
|
else:
|
# tee.elf
|
return self._tee_load_addr
|
|
def elf_for_addr(self, addr):
|
l_addr = self.elf_load_addr(addr)
|
if l_addr == self._tee_load_addr:
|
return 'tee.elf'
|
for k in self._elfs:
|
e = self._elfs[k]
|
if int(e[1], 16) == int(l_addr, 16):
|
return e[0]
|
return None
|
|
def subtract_load_addr(self, addr):
|
l_addr = self.elf_load_addr(addr)
|
if l_addr is None:
|
return None
|
if int(l_addr, 16) > int(addr, 16):
|
return ''
|
return '0x{:x}'.format(int(addr, 16) - int(l_addr, 16))
|
|
def resolve(self, addr):
|
reladdr = self.subtract_load_addr(addr)
|
self.spawn_addr2line(self.elf_for_addr(addr))
|
if not reladdr or not self._addr2line:
|
return '???'
|
if self.elf_for_addr(addr) == 'tee.elf':
|
reladdr = '0x{:x}'.format(int(reladdr, 16) +
|
int(self.first_vma('tee.elf'), 16))
|
try:
|
print(reladdr, file=self._addr2line.stdin)
|
ret = self._addr2line.stdout.readline().rstrip('\n')
|
except IOError:
|
ret = '!!!'
|
return ret
|
|
def symbol_plus_offset(self, addr):
|
ret = ''
|
prevsize = 0
|
reladdr = self.subtract_load_addr(addr)
|
elf_name = self.elf_for_addr(addr)
|
if elf_name is None:
|
return ''
|
elf = self.get_elf(elf_name)
|
cmd = self.arch_prefix('nm', elf)
|
if not reladdr or not elf or not cmd:
|
return ''
|
ireladdr = int(reladdr, 16)
|
nm = self.my_Popen([cmd, '--numeric-sort', '--print-size', elf])
|
for line in iter(nm.stdout.readline, ''):
|
try:
|
addr, size, _, name = line.split()
|
except ValueError:
|
# Size is missing
|
try:
|
addr, _, name = line.split()
|
size = '0'
|
except ValueError:
|
# E.g., undefined (external) symbols (line = "U symbol")
|
continue
|
iaddr = int(addr, 16)
|
isize = int(size, 16)
|
if iaddr == ireladdr:
|
ret = name
|
break
|
if iaddr < ireladdr and iaddr + isize >= ireladdr:
|
offs = ireladdr - iaddr
|
ret = name + '+' + str(offs)
|
break
|
if iaddr > ireladdr and prevsize == 0:
|
offs = iaddr + ireladdr
|
ret = prevname + '+' + str(offs)
|
break
|
prevsize = size
|
prevname = name
|
nm.terminate()
|
return ret
|
|
def section_plus_offset(self, addr):
|
ret = ''
|
reladdr = self.subtract_load_addr(addr)
|
elf_name = self.elf_for_addr(addr)
|
if elf_name is None:
|
return ''
|
elf = self.get_elf(elf_name)
|
cmd = self.arch_prefix('objdump', elf)
|
if not reladdr or not elf or not cmd:
|
return ''
|
iaddr = int(reladdr, 16)
|
objdump = self.my_Popen([cmd, '--section-headers', elf])
|
for line in iter(objdump.stdout.readline, ''):
|
try:
|
idx, name, size, vma, lma, offs, algn = line.split()
|
except ValueError:
|
continue
|
ivma = int(vma, 16)
|
isize = int(size, 16)
|
if ivma == iaddr:
|
ret = name
|
break
|
if ivma < iaddr and ivma + isize >= iaddr:
|
offs = iaddr - ivma
|
ret = name + '+' + str(offs)
|
break
|
objdump.terminate()
|
return ret
|
|
def process_abort(self, line):
|
ret = ''
|
match = re.search(ABORT_ADDR_RE, line)
|
addr = match.group('addr')
|
pre = match.start('addr')
|
post = match.end('addr')
|
sym = self.symbol_plus_offset(addr)
|
sec = self.section_plus_offset(addr)
|
if sym or sec:
|
ret += line[:pre]
|
ret += addr
|
if sym:
|
ret += ' ' + sym
|
if sec:
|
ret += ' ' + sec
|
ret += line[post:]
|
return ret
|
|
# Return all ELF sections with the ALLOC flag
|
def read_sections(self, elf_name):
|
if elf_name is None:
|
return
|
if elf_name in self._sections:
|
return
|
elf = self.get_elf(elf_name)
|
if not elf:
|
return
|
cmd = self.arch_prefix('objdump', elf)
|
if not elf or not cmd:
|
return
|
self._sections[elf_name] = []
|
objdump = self.my_Popen([cmd, '--section-headers', elf])
|
for line in iter(objdump.stdout.readline, ''):
|
try:
|
_, name, size, vma, _, _, _ = line.split()
|
except ValueError:
|
if 'ALLOC' in line:
|
self._sections[elf_name].append([name, int(vma, 16),
|
int(size, 16)])
|
|
def first_vma(self, elf_name):
|
self.read_sections(elf_name)
|
return '0x{:x}'.format(self._sections[elf_name][0][1])
|
|
def overlaps(self, section, addr, size):
|
sec_addr = section[1]
|
sec_size = section[2]
|
if not size or not sec_size:
|
return False
|
return ((addr <= (sec_addr + sec_size - 1)) and
|
((addr + size - 1) >= sec_addr))
|
|
def sections_in_region(self, addr, size, elf_idx):
|
ret = ''
|
addr = self.subtract_load_addr(addr)
|
if not addr:
|
return ''
|
iaddr = int(addr, 16)
|
isize = int(size, 16)
|
elf = self._elfs[int(elf_idx)][0]
|
if elf is None:
|
return ''
|
self.read_sections(elf)
|
if elf not in self._sections:
|
return ''
|
for s in self._sections[elf]:
|
if self.overlaps(s, iaddr, isize):
|
ret += ' ' + s[0]
|
return ret
|
|
def reset(self):
|
self._call_stack_found = False
|
if self._addr2line:
|
self._addr2line.terminate()
|
self._addr2line = None
|
self._addr2line_elf_name = None
|
self._arch = None
|
self._saved_abort_line = ''
|
self._sections = {} # {elf_name: [[name, addr, size], ...], ...}
|
self._regions = [] # [[addr, size, elf_idx, saved line], ...]
|
self._elfs = {0: ["tee.elf", 0]} # {idx: [uuid, load_addr], ...}
|
self._tee_load_addr = '0x0'
|
self._func_graph_found = False
|
self._func_graph_skip_line = True
|
|
def pretty_print_path(self, path):
|
if self._strip_path:
|
return re.sub(re.escape(self._strip_path) + '/*', '', path)
|
return path
|
|
def write(self, line):
|
if self._call_stack_found:
|
match = re.search(STACK_ADDR_RE, line)
|
if match:
|
addr = match.group('addr')
|
pre = match.start('addr')
|
post = match.end('addr')
|
self._out.write(line[:pre])
|
self._out.write(addr)
|
# The call stack contains return addresses (LR/ELR values).
|
# Heuristic: subtract 2 to obtain the call site of the function
|
# or the location of the exception. This value works for A64,
|
# A32 as well as Thumb.
|
pc = 0
|
lr = int(addr, 16)
|
if lr:
|
pc = lr - 2
|
res = self.resolve('0x{:x}'.format(pc))
|
res = self.pretty_print_path(res)
|
self._out.write(' ' + res)
|
self._out.write(line[post:])
|
return
|
else:
|
self.reset()
|
if self._func_graph_found:
|
match = re.search(GRAPH_ADDR_RE, line)
|
match_re = re.search(GRAPH_RE, line)
|
if match:
|
addr = match.group('addr')
|
pre = match.start('addr')
|
post = match.end('addr')
|
self._out.write(line[:pre])
|
res = self.resolve(addr)
|
res_arr = re.split(' ', res)
|
self._out.write(res_arr[0])
|
self._out.write(line[post:])
|
self._func_graph_skip_line = False
|
return
|
elif match_re:
|
self._out.write(line)
|
return
|
elif self._func_graph_skip_line:
|
return
|
else:
|
self.reset()
|
match = re.search(REGION_RE, line)
|
if match:
|
# Region table: save info for later processing once
|
# we know which UUID corresponds to which ELF index
|
addr = match.group('addr')
|
size = match.group('size')
|
elf_idx = match.group('elf_idx')
|
self._regions.append([addr, size, elf_idx, line])
|
return
|
match = re.search(ELF_LIST_RE, line)
|
if match:
|
# ELF list: save info for later. Region table and ELF list
|
# will be displayed when the call stack is reached
|
i = int(match.group('idx'))
|
self._elfs[i] = [match.group('uuid'), match.group('load_addr'),
|
line]
|
return
|
match = re.search(TEE_LOAD_ADDR_RE, line)
|
if match:
|
self._tee_load_addr = match.group('load_addr')
|
match = re.search(CALL_STACK_RE, line)
|
if match:
|
self._call_stack_found = True
|
if self._regions:
|
for r in self._regions:
|
r_addr = r[0]
|
r_size = r[1]
|
elf_idx = r[2]
|
saved_line = r[3]
|
if elf_idx is None:
|
self._out.write(saved_line)
|
else:
|
self._out.write(saved_line.strip() +
|
self.sections_in_region(r_addr,
|
r_size,
|
elf_idx) +
|
'\n')
|
if self._elfs:
|
for k in self._elfs:
|
e = self._elfs[k]
|
if (len(e) >= 3):
|
# TA executable or library
|
self._out.write(e[2].strip())
|
elf = self.get_elf(e[0])
|
if elf:
|
rpath = os.path.realpath(elf)
|
path = self.pretty_print_path(rpath)
|
self._out.write(' (' + path + ')')
|
self._out.write('\n')
|
# Here is a good place to resolve the abort address because we
|
# have all the information we need
|
if self._saved_abort_line:
|
self._out.write(self.process_abort(self._saved_abort_line))
|
match = re.search(FUNC_GRAPH_RE, line)
|
if match:
|
self._func_graph_found = True
|
match = re.search(ABORT_ADDR_RE, line)
|
if match:
|
self.reset()
|
# At this point the arch and TA load address are unknown.
|
# Save the line so We can translate the abort address later.
|
self._saved_abort_line = line
|
self._out.write(line)
|
|
def flush(self):
|
self._out.flush()
|
|
|
def main():
|
args = get_args()
|
if args.dir:
|
# Flatten list in case -d is used several times *and* with multiple
|
# arguments
|
args.dirs = [item for sublist in args.dir for item in sublist]
|
else:
|
args.dirs = []
|
symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path)
|
|
fd = sys.stdin.fileno()
|
isatty = os.isatty(fd)
|
if isatty:
|
old = termios.tcgetattr(fd)
|
new = termios.tcgetattr(fd)
|
new[3] = new[3] & ~termios.ECHO # lflags
|
try:
|
if isatty:
|
termios.tcsetattr(fd, termios.TCSADRAIN, new)
|
for line in sys.stdin:
|
symbolizer.write(line)
|
finally:
|
symbolizer.flush()
|
if isatty:
|
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
|
if __name__ == "__main__":
|
main()
|