xref: /optee_os/scripts/symbolize.py (revision 12fc37711783247b0d05fdc271ef007f4930767b)
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