xref: /optee_os/scripts/symbolize.py (revision 4bca302aecf5fcb0c2654134f0790a7f7a42c122)
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                addr, _, name = line.split()
204                size = '0'
205            iaddr = int(addr, 16)
206            isize = int(size, 16)
207            if iaddr == ireladdr:
208                ret = name
209                break
210            if iaddr < ireladdr and iaddr + isize >= ireladdr:
211                offs = ireladdr - iaddr
212                ret = name + '+' + str(offs)
213                break
214            if iaddr > ireladdr and prevsize == 0:
215                offs = iaddr + ireladdr
216                ret = prevname + '+' + str(offs)
217                break
218            prevsize = size
219            prevname = name
220        nm.terminate()
221        return ret
222
223    def section_plus_offset(self, addr):
224        ret = ''
225        reladdr = self.subtract_load_addr(addr)
226        elf_name = self.elf_for_addr(addr)
227        if elf_name is None:
228            return ''
229        elf = self.get_elf(elf_name)
230        cmd = self.arch_prefix('objdump')
231        if not reladdr or not elf or not cmd:
232            return ''
233        iaddr = int(reladdr, 16)
234        objdump = subprocess.Popen([cmd, '--section-headers', elf],
235                                   stdin=subprocess.PIPE,
236                                   stdout=subprocess.PIPE)
237        for line in iter(objdump.stdout.readline, ''):
238            try:
239                idx, name, size, vma, lma, offs, algn = line.split()
240            except:
241                continue
242            ivma = int(vma, 16)
243            isize = int(size, 16)
244            if ivma == iaddr:
245                ret = name
246                break
247            if ivma < iaddr and ivma + isize >= iaddr:
248                offs = iaddr - ivma
249                ret = name + '+' + str(offs)
250                break
251        objdump.terminate()
252        return ret
253
254    def process_abort(self, line):
255        ret = ''
256        match = re.search(ABORT_ADDR_RE, line)
257        addr = match.group('addr')
258        pre = match.start('addr')
259        post = match.end('addr')
260        sym = self.symbol_plus_offset(addr)
261        sec = self.section_plus_offset(addr)
262        if sym or sec:
263            ret += line[:pre]
264            ret += addr
265            if sym:
266                ret += ' ' + sym
267            if sec:
268                ret += ' ' + sec
269            ret += line[post:]
270        return ret
271
272    # Return all ELF sections with the ALLOC flag
273    def read_sections(self, elf_name):
274        if elf_name is None:
275            return
276        if elf_name in self._sections:
277            return
278        elf = self.get_elf(elf_name)
279        cmd = self.arch_prefix('objdump')
280        if not elf or not cmd:
281            return
282        self._sections[elf_name] = []
283        objdump = subprocess.Popen([cmd, '--section-headers', elf],
284                                   stdin=subprocess.PIPE,
285                                   stdout=subprocess.PIPE)
286        for line in iter(objdump.stdout.readline, ''):
287            try:
288                _, name, size, vma, _, _, _ = line.split()
289            except:
290                if 'ALLOC' in line:
291                    self._sections[elf_name].append([name, int(vma, 16),
292                                                     int(size, 16)])
293
294    def overlaps(self, section, addr, size):
295        sec_addr = section[1]
296        sec_size = section[2]
297        if not size or not sec_size:
298            return False
299        return ((addr <= (sec_addr + sec_size - 1)) and
300                ((addr + size - 1) >= sec_addr))
301
302    def sections_in_region(self, addr, size, elf_idx):
303        ret = ''
304        addr = self.subtract_load_addr(addr)
305        if not addr:
306            return ''
307        iaddr = int(addr, 16)
308        isize = int(size, 16)
309        elf = self._elfs[int(elf_idx)][0]
310        if elf is None:
311            return ''
312        self.read_sections(elf)
313        if elf not in self._sections:
314            return ''
315        for s in self._sections[elf]:
316            if self.overlaps(s, iaddr, isize):
317                ret += ' ' + s[0]
318        return ret
319
320    def reset(self):
321        self._call_stack_found = False
322        if self._addr2line:
323            self._addr2line.terminate()
324            self._addr2line = None
325        self._addr2line_elf_name = None
326        self._arch = None
327        self._saved_abort_line = ''
328        self._sections = {}  # {elf_name: [[name, addr, size], ...], ...}
329        self._regions = []   # [[addr, size, elf_idx, saved line], ...]
330        self._elfs = {0: ["tee.elf", 0]}  # {idx: [uuid, load_addr], ...}
331
332    def write(self, line):
333            if self._call_stack_found:
334                match = re.search(STACK_ADDR_RE, line)
335                if match:
336                    addr = match.group('addr')
337                    pre = match.start('addr')
338                    post = match.end('addr')
339                    self._out.write(line[:pre])
340                    self._out.write(addr)
341                    res = self.resolve(addr)
342                    if self._strip_path:
343                        res = re.sub(re.escape(self._strip_path) + '/*', '',
344                                     res)
345                    self._out.write(' ' + res)
346                    self._out.write(line[post:])
347                    return
348                else:
349                    self.reset()
350            match = re.search(REGION_RE, line)
351            if match:
352                # Region table: save info for later processing once
353                # we know which UUID corresponds to which ELF index
354                addr = match.group('addr')
355                size = match.group('size')
356                elf_idx = match.group('elf_idx')
357                self._regions.append([addr, size, elf_idx, line])
358                return
359            match = re.search(ELF_LIST_RE, line)
360            if match:
361                # ELF list: save info for later. Region table and ELF list
362                # will be displayed when the call stack is reached
363                i = int(match.group('idx'))
364                self._elfs[i] = [match.group('uuid'), match.group('load_addr'),
365                                 line]
366                return
367            match = re.search(CALL_STACK_RE, line)
368            if match:
369                self._call_stack_found = True
370                if self._regions:
371                    for r in self._regions:
372                        r_addr = r[0]
373                        r_size = r[1]
374                        elf_idx = r[2]
375                        saved_line = r[3]
376                        if elf_idx is None:
377                            self._out.write(saved_line)
378                        else:
379                            self._out.write(saved_line.strip() +
380                                            self.sections_in_region(r_addr,
381                                                                    r_size,
382                                                                    elf_idx) +
383                                            '\n')
384                if self._elfs:
385                    for k in self._elfs:
386                        e = self._elfs[k]
387                        if (len(e) >= 3):
388                            self._out.write(e[2])
389                # Here is a good place to resolve the abort address because we
390                # have all the information we need
391                if self._saved_abort_line:
392                    self._out.write(self.process_abort(self._saved_abort_line))
393            match = re.search(ABORT_ADDR_RE, line)
394            if match:
395                self.reset()
396                # At this point the arch and TA load address are unknown.
397                # Save the line so We can translate the abort address later.
398                self._saved_abort_line = line
399            self._out.write(line)
400
401    def flush(self):
402        self._out.flush()
403
404
405def main():
406    args = get_args()
407    if args.dir:
408        # Flatten list in case -d is used several times *and* with multiple
409        # arguments
410        args.dirs = [item for sublist in args.dir for item in sublist]
411    else:
412        args.dirs = []
413    symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path)
414
415    for line in sys.stdin:
416        symbolizer.write(line)
417    symbolizer.flush()
418
419if __name__ == "__main__":
420    main()
421