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