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