xref: /optee_os/scripts/symbolize.py (revision 1e6f2ea0d22f38fa1e96af0659786cfcbdfb5d31)
1#!/usr/bin/env python
2# SPDX-License-Identifier: BSD-2-Clause
3#
4# Copyright (c) 2017, Linaro Limited
5#
6
7
8import argparse
9import glob
10import os
11import re
12import subprocess
13import sys
14
15CALL_STACK_RE = re.compile('Call stack:')
16# This gets the address from lines looking like this:
17# E/TC:0  0x001044a8
18STACK_ADDR_RE = re.compile(r'[UEIDFM]/T[AC]:.*(?P<addr>0x[0-9a-f]+)')
19ABORT_ADDR_RE = re.compile('-abort at address (?P<addr>0x[0-9a-f]+)')
20REGION_RE = re.compile('region [0-9]+: va (?P<addr>0x[0-9a-f]+) '
21                       'pa 0x[0-9a-f]+ size (?P<size>0x[0-9a-f]+)'
22                       '( flags .{6} (\[(?P<elf_idx>[0-9]+)\])?)?')
23ELF_LIST_RE = re.compile(r'\[(?P<idx>[0-9]+)\] (?P<uuid>[0-9a-f\-]+)'
24                         ' @ (?P<load_addr>0x[0-9a-f\-]+)')
25
26epilog = '''
27This scripts reads an OP-TEE abort or panic message from stdin and adds debug
28information to the output, such as '<function> at <file>:<line>' next to each
29address in the call stack. Any message generated by OP-TEE and containing a
30call stack can in principle be processed by this script. This currently
31includes aborts and panics from the TEE core as well as from any TA.
32The paths provided on the command line are used to locate the appropriate ELF
33binary (tee.elf or Trusted Application). The GNU binutils (addr2line, objdump,
34nm) are used to extract the debug info.
35
36OP-TEE abort and panic messages are sent to the secure console. They look like
37the following:
38
39  E/TC:0 User TA data-abort at address 0xffffdecd (alignment fault)
40  ...
41  E/TC:0 Call stack:
42  E/TC:0  0x4000549e
43  E/TC:0  0x40001f4b
44  E/TC:0  0x4000273f
45  E/TC:0  0x40005da7
46
47Inspired by a script of the same name by the Chromium project.
48
49Sample usage:
50
51  $ scripts/symbolize.py -d out/arm-plat-hikey/core -d ../optee_test/out/ta/*
52  <paste whole dump here>
53  ^D
54'''
55
56
57def get_args():
58    parser = argparse.ArgumentParser(
59                formatter_class=argparse.RawDescriptionHelpFormatter,
60                description='Symbolizes OP-TEE abort dumps',
61                epilog=epilog)
62    parser.add_argument('-d', '--dir', action='append', nargs='+',
63        help='Search for ELF file in DIR. tee.elf is needed to decode '
64             'a TEE Core or pseudo-TA abort, while <TA_uuid>.elf is required '
65             'if a user-mode TA has crashed. For convenience, ELF files '
66             'may also be given.')
67    parser.add_argument('-s', '--strip_path', nargs='?',
68        help='Strip STRIP_PATH from file paths (default: current directory, '
69             'use -s with no argument to show full paths)',
70        default=os.getcwd())
71
72    return parser.parse_args()
73
74
75class Symbolizer(object):
76    def __init__(self, out, dirs, strip_path):
77        self._out = out
78        self._dirs = dirs
79        self._strip_path = strip_path
80        self._addr2line = None
81        self.reset()
82
83    def get_elf(self, elf_or_uuid):
84        if not elf_or_uuid.endswith('.elf'):
85            elf_or_uuid += '.elf'
86        for d in self._dirs:
87            if d.endswith(elf_or_uuid) and os.path.isfile(d):
88                return d
89            elf = glob.glob(d + '/' + elf_or_uuid)
90            if elf:
91                return elf[0]
92
93    def set_arch(self):
94        if self._arch:
95            return
96        elf = self.get_elf(self._elfs[0][0])
97        if elf is None:
98            return
99        p = subprocess.Popen(['file', self.get_elf(self._elfs[0][0])],
100                             stdout=subprocess.PIPE)
101        output = p.stdout.readlines()
102        p.terminate()
103        if 'ARM aarch64,' in output[0]:
104            self._arch = 'aarch64-linux-gnu-'
105        elif 'ARM,' in output[0]:
106            self._arch = 'arm-linux-gnueabihf-'
107
108    def arch_prefix(self, cmd):
109        self.set_arch()
110        if self._arch is None:
111            return ''
112        return self._arch + cmd
113
114    def spawn_addr2line(self, elf_name):
115        if elf_name is None:
116            return
117        if self._addr2line_elf_name is elf_name:
118            return
119        if self._addr2line:
120            self._addr2line.terminate
121            self._addr2line = None
122        elf = self.get_elf(elf_name)
123        if not elf:
124            return
125        cmd = self.arch_prefix('addr2line')
126        if not cmd:
127            return
128        self._addr2line = subprocess.Popen([cmd, '-f', '-p', '-e', elf],
129                                           stdin=subprocess.PIPE,
130                                           stdout=subprocess.PIPE)
131        self._addr2line_elf_name = elf_name
132
133    # If addr falls into a region that maps a TA ELF file, return the load
134    # address of that file.
135    def elf_load_addr(self, addr):
136        if self._regions:
137            for r in self._regions:
138                r_addr = int(r[0], 16)
139                r_size = int(r[1], 16)
140                i_addr = int(addr, 16)
141                if (i_addr >= r_addr and i_addr < (r_addr + r_size)):
142                    # Found region
143                    elf_idx = r[2]
144                    if elf_idx is not None:
145                        return self._elfs[int(elf_idx)][1]
146            return None
147        else:
148            # tee.elf
149            return '0x0'
150
151    def elf_for_addr(self, addr):
152        l_addr = self.elf_load_addr(addr)
153        if l_addr is None:
154            return None
155        if l_addr is '0x0':
156            return 'tee.elf'
157        for k in self._elfs:
158            e = self._elfs[k]
159            if int(e[1], 16) == int(l_addr, 16):
160                return e[0]
161        return None
162
163    def subtract_load_addr(self, addr):
164        l_addr = self.elf_load_addr(addr)
165        if l_addr is None:
166            return None
167        if int(l_addr, 16) > int(addr, 16):
168            return ''
169        return '0x{:x}'.format(int(addr, 16) - int(l_addr, 16))
170
171    def resolve(self, addr):
172        reladdr = self.subtract_load_addr(addr)
173        self.spawn_addr2line(self.elf_for_addr(addr))
174        if not reladdr or not self._addr2line:
175            return '???'
176        try:
177            print >> self._addr2line.stdin, reladdr
178            ret = self._addr2line.stdout.readline().rstrip('\n')
179        except IOError:
180            ret = '!!!'
181        return ret
182
183    def symbol_plus_offset(self, addr):
184        ret = ''
185        prevsize = 0
186        reladdr = self.subtract_load_addr(addr)
187        elf_name = self.elf_for_addr(addr)
188        if elf_name is None:
189            return ''
190        elf = self.get_elf(elf_name)
191        cmd = self.arch_prefix('nm')
192        if not reladdr or not elf or not cmd:
193            return ''
194        ireladdr = int(reladdr, 16)
195        nm = subprocess.Popen([cmd, '--numeric-sort', '--print-size', elf],
196                              stdin=subprocess.PIPE,
197                              stdout=subprocess.PIPE)
198        for line in iter(nm.stdout.readline, ''):
199            try:
200                addr, size, _, name = line.split()
201            except:
202                # Size is missing
203                try:
204                    addr, _, name = line.split()
205                    size = '0'
206                except:
207                    # E.g., undefined (external) symbols (line = "U symbol")
208                    continue
209            iaddr = int(addr, 16)
210            isize = int(size, 16)
211            if iaddr == ireladdr:
212                ret = name
213                break
214            if iaddr < ireladdr and iaddr + isize >= ireladdr:
215                offs = ireladdr - iaddr
216                ret = name + '+' + str(offs)
217                break
218            if iaddr > ireladdr and prevsize == 0:
219                offs = iaddr + ireladdr
220                ret = prevname + '+' + str(offs)
221                break
222            prevsize = size
223            prevname = name
224        nm.terminate()
225        return ret
226
227    def section_plus_offset(self, addr):
228        ret = ''
229        reladdr = self.subtract_load_addr(addr)
230        elf_name = self.elf_for_addr(addr)
231        if elf_name is None:
232            return ''
233        elf = self.get_elf(elf_name)
234        cmd = self.arch_prefix('objdump')
235        if not reladdr or not elf or not cmd:
236            return ''
237        iaddr = int(reladdr, 16)
238        objdump = subprocess.Popen([cmd, '--section-headers', elf],
239                                   stdin=subprocess.PIPE,
240                                   stdout=subprocess.PIPE)
241        for line in iter(objdump.stdout.readline, ''):
242            try:
243                idx, name, size, vma, lma, offs, algn = line.split()
244            except:
245                continue
246            ivma = int(vma, 16)
247            isize = int(size, 16)
248            if ivma == iaddr:
249                ret = name
250                break
251            if ivma < iaddr and ivma + isize >= iaddr:
252                offs = iaddr - ivma
253                ret = name + '+' + str(offs)
254                break
255        objdump.terminate()
256        return ret
257
258    def process_abort(self, line):
259        ret = ''
260        match = re.search(ABORT_ADDR_RE, line)
261        addr = match.group('addr')
262        pre = match.start('addr')
263        post = match.end('addr')
264        sym = self.symbol_plus_offset(addr)
265        sec = self.section_plus_offset(addr)
266        if sym or sec:
267            ret += line[:pre]
268            ret += addr
269            if sym:
270                ret += ' ' + sym
271            if sec:
272                ret += ' ' + sec
273            ret += line[post:]
274        return ret
275
276    # Return all ELF sections with the ALLOC flag
277    def read_sections(self, elf_name):
278        if elf_name is None:
279            return
280        if elf_name in self._sections:
281            return
282        elf = self.get_elf(elf_name)
283        cmd = self.arch_prefix('objdump')
284        if not elf or not cmd:
285            return
286        self._sections[elf_name] = []
287        objdump = subprocess.Popen([cmd, '--section-headers', elf],
288                                   stdin=subprocess.PIPE,
289                                   stdout=subprocess.PIPE)
290        for line in iter(objdump.stdout.readline, ''):
291            try:
292                _, name, size, vma, _, _, _ = line.split()
293            except:
294                if 'ALLOC' in line:
295                    self._sections[elf_name].append([name, int(vma, 16),
296                                                     int(size, 16)])
297
298    def overlaps(self, section, addr, size):
299        sec_addr = section[1]
300        sec_size = section[2]
301        if not size or not sec_size:
302            return False
303        return ((addr <= (sec_addr + sec_size - 1)) and
304                ((addr + size - 1) >= sec_addr))
305
306    def sections_in_region(self, addr, size, elf_idx):
307        ret = ''
308        addr = self.subtract_load_addr(addr)
309        if not addr:
310            return ''
311        iaddr = int(addr, 16)
312        isize = int(size, 16)
313        elf = self._elfs[int(elf_idx)][0]
314        if elf is None:
315            return ''
316        self.read_sections(elf)
317        if elf not in self._sections:
318            return ''
319        for s in self._sections[elf]:
320            if self.overlaps(s, iaddr, isize):
321                ret += ' ' + s[0]
322        return ret
323
324    def reset(self):
325        self._call_stack_found = False
326        if self._addr2line:
327            self._addr2line.terminate()
328            self._addr2line = None
329        self._addr2line_elf_name = None
330        self._arch = None
331        self._saved_abort_line = ''
332        self._sections = {}  # {elf_name: [[name, addr, size], ...], ...}
333        self._regions = []   # [[addr, size, elf_idx, saved line], ...]
334        self._elfs = {0: ["tee.elf", 0]}  # {idx: [uuid, load_addr], ...}
335
336
337    def pretty_print_path(self, path):
338        if self._strip_path:
339            return re.sub(re.escape(self._strip_path) + '/*', '', path)
340        return path
341
342
343    def write(self, line):
344            if self._call_stack_found:
345                match = re.search(STACK_ADDR_RE, line)
346                if match:
347                    addr = match.group('addr')
348                    pre = match.start('addr')
349                    post = match.end('addr')
350                    self._out.write(line[:pre])
351                    self._out.write(addr)
352                    res = self.resolve(addr)
353                    res = self.pretty_print_path(res)
354                    self._out.write(' ' + res)
355                    self._out.write(line[post:])
356                    return
357                else:
358                    self.reset()
359            match = re.search(REGION_RE, line)
360            if match:
361                # Region table: save info for later processing once
362                # we know which UUID corresponds to which ELF index
363                addr = match.group('addr')
364                size = match.group('size')
365                elf_idx = match.group('elf_idx')
366                self._regions.append([addr, size, elf_idx, line])
367                return
368            match = re.search(ELF_LIST_RE, line)
369            if match:
370                # ELF list: save info for later. Region table and ELF list
371                # will be displayed when the call stack is reached
372                i = int(match.group('idx'))
373                self._elfs[i] = [match.group('uuid'), match.group('load_addr'),
374                                 line]
375                return
376            match = re.search(CALL_STACK_RE, line)
377            if match:
378                self._call_stack_found = True
379                if self._regions:
380                    for r in self._regions:
381                        r_addr = r[0]
382                        r_size = r[1]
383                        elf_idx = r[2]
384                        saved_line = r[3]
385                        if elf_idx is None:
386                            self._out.write(saved_line)
387                        else:
388                            self._out.write(saved_line.strip() +
389                                            self.sections_in_region(r_addr,
390                                                                    r_size,
391                                                                    elf_idx) +
392                                            '\n')
393                if self._elfs:
394                    for k in self._elfs:
395                        e = self._elfs[k]
396                        if (len(e) >= 3):
397                            # TA executable or library
398                            self._out.write(e[2].strip())
399                            elf = self.get_elf(e[0])
400                            if elf:
401                                rpath = os.path.realpath(elf)
402                                path = self.pretty_print_path(rpath)
403                                self._out.write(' (' + path + ')')
404                            self._out.write('\n')
405                # Here is a good place to resolve the abort address because we
406                # have all the information we need
407                if self._saved_abort_line:
408                    self._out.write(self.process_abort(self._saved_abort_line))
409            match = re.search(ABORT_ADDR_RE, line)
410            if match:
411                self.reset()
412                # At this point the arch and TA load address are unknown.
413                # Save the line so We can translate the abort address later.
414                self._saved_abort_line = line
415            self._out.write(line)
416
417    def flush(self):
418        self._out.flush()
419
420
421def main():
422    args = get_args()
423    if args.dir:
424        # Flatten list in case -d is used several times *and* with multiple
425        # arguments
426        args.dirs = [item for sublist in args.dir for item in sublist]
427    else:
428        args.dirs = []
429    symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path)
430
431    for line in sys.stdin:
432        symbolizer.write(line)
433    symbolizer.flush()
434
435if __name__ == "__main__":
436    main()
437