xref: /optee_os/scripts/symbolize.py (revision 817466cb476de705a8e3dabe1ef165fe27a18c2f)
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
15TA_UUID_RE = re.compile(r'Status of TA (?P<uuid>[0-9a-f\-]+)')
16TA_INFO_RE = re.compile('  arch: (?P<arch>\w+)  '
17                        'load address: (?P<load_addr>0x[0-9a-f]+)')
18CALL_STACK_RE = re.compile('Call stack:')
19
20# This gets the address from lines looking like this:
21# E/TC:0  0x001044a8
22STACK_ADDR_RE = re.compile(r'[UEIDFM]/T[AC]:.*(?P<addr>0x[0-9a-f]+)')
23ABORT_ADDR_RE = re.compile('-abort at address (?P<addr>0x[0-9a-f]+)')
24REGION_RE = re.compile('region [0-9]+: va (?P<addr>0x[0-9a-f]+) '
25                       'pa 0x[0-9a-f]+ size (?P<size>0x[0-9a-f]+)')
26
27epilog = '''
28This scripts reads an OP-TEE abort or panic message from stdin and adds debug
29information to the output, such as '<function> at <file>:<line>' next to each
30address in the call stack. Any message generated by OP-TEE and containing a
31call stack can in principle be processed by this script. This currently
32includes aborts and panics from the TEE core as well as from any TA.
33The paths provided on the command line are used to locate the appropriate ELF
34binary (tee.elf or Trusted Application). The GNU binutils (addr2line, objdump,
35nm) are used to extract the debug info.
36
37OP-TEE abort and panic messages are sent to the secure console. They look like
38the following:
39
40  E/TC:0 User TA data-abort at address 0xffffdecd (alignment fault)
41  ...
42  E/TC:0 Call stack:
43  E/TC:0  0x4000549e
44  E/TC:0  0x40001f4b
45  E/TC:0  0x4000273f
46  E/TC:0  0x40005da7
47
48Inspired by a script of the same name by the Chromium project.
49
50Sample usage:
51
52  $ scripts/symbolize.py -d out/arm-plat-hikey/core -d ../optee_test/out/ta/*
53  <paste whole dump here>
54  ^D
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
74class Symbolizer(object):
75    def __init__(self, out, dirs, strip_path):
76        self._out = out
77        self._dirs = dirs
78        self._strip_path = strip_path
79        self._addr2line = None
80        self._bin = 'tee.elf'
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        if self._bin:
97            p = subprocess.Popen([ 'file', self.get_elf(self._bin) ],
98                                 stdout=subprocess.PIPE)
99            output = p.stdout.readlines()
100            p.terminate()
101            if 'ARM aarch64,' in output[0]:
102                self._arch = 'aarch64-linux-gnu-'
103            elif 'ARM,' in output[0]:
104                self._arch = 'arm-linux-gnueabihf-'
105
106    def arch_prefix(self, cmd):
107        self.set_arch()
108        return self._arch + cmd
109
110    def spawn_addr2line(self):
111        if not self._addr2line:
112            elf = self.get_elf(self._bin)
113            if not elf:
114                return
115            cmd = self.arch_prefix('addr2line')
116            if not cmd:
117                return
118            self._addr2line = subprocess.Popen([cmd, '-f', '-p', '-e', elf],
119                                                stdin = subprocess.PIPE,
120                                                stdout = subprocess.PIPE)
121
122    def subtract_load_addr(self, addr):
123        offs = self._load_addr
124        if int(offs, 16) > int(addr, 16):
125            return ''
126        return '0x{:x}'.format(int(addr, 16) - int(offs, 16))
127
128    def resolve(self, addr):
129        reladdr = self.subtract_load_addr(addr)
130        self.spawn_addr2line()
131        if not reladdr or not self._addr2line:
132            return '???'
133        try:
134            print >> self._addr2line.stdin, reladdr
135            ret = self._addr2line.stdout.readline().rstrip('\n')
136        except IOError:
137            ret = '!!!'
138        return ret
139
140    def symbol_plus_offset(self, addr):
141        ret = ''
142        prevsize = 0
143        reladdr = self.subtract_load_addr(addr)
144        elf = self.get_elf(self._bin)
145        cmd = self.arch_prefix('nm')
146        if not reladdr or not elf or not cmd:
147            return ''
148        ireladdr = int(reladdr, 16)
149        nm = subprocess.Popen([cmd, '--numeric-sort', '--print-size', elf],
150                               stdin = subprocess.PIPE,
151                               stdout = subprocess.PIPE)
152        for line in iter(nm.stdout.readline, ''):
153            try:
154                addr, size, _, name = line.split()
155            except:
156                # Size is missing
157                addr, _, name = line.split()
158                size = '0'
159            iaddr = int(addr, 16)
160            isize = int(size, 16)
161            if iaddr == ireladdr:
162                ret = name
163                break
164            if iaddr < ireladdr and iaddr + isize >= ireladdr:
165                offs = ireladdr - iaddr
166                ret = name + '+' + str(offs)
167                break
168            if iaddr > ireladdr and prevsize == 0:
169                offs = iaddr + ireladdr
170                ret = prevname + '+' + str(offs)
171                break
172            prevsize = size
173            prevname = name
174        nm.terminate()
175        return ret
176
177    def section_plus_offset(self, addr):
178        ret = ''
179        reladdr = self.subtract_load_addr(addr)
180        elf = self.get_elf(self._bin)
181        cmd = self.arch_prefix('objdump')
182        if not reladdr or not elf or not cmd:
183            return ''
184        iaddr = int(reladdr, 16)
185        objdump = subprocess.Popen([cmd, '--section-headers', elf],
186                                    stdin = subprocess.PIPE,
187                                    stdout = subprocess.PIPE)
188        for line in iter(objdump.stdout.readline, ''):
189            try:
190                idx, name, size, vma, lma, offs, algn = line.split()
191            except:
192                continue;
193            ivma = int(vma, 16)
194            isize = int(size, 16)
195            if ivma == iaddr:
196                ret = name
197                break
198            if ivma < iaddr and ivma + isize >= iaddr:
199                offs = iaddr - ivma
200                ret = name + '+' + str(offs)
201                break
202        objdump.terminate()
203        return ret
204
205    def process_abort(self, line):
206        ret = ''
207        match = re.search(ABORT_ADDR_RE, line)
208        addr = match.group('addr')
209        pre = match.start('addr')
210        post = match.end('addr')
211        sym = self.symbol_plus_offset(addr)
212        sec = self.section_plus_offset(addr)
213        if sym or sec:
214            ret += line[:pre]
215            ret += addr
216            if sym:
217                ret += ' ' + sym
218            if sec:
219                ret += ' ' + sec
220            ret += line[post:]
221        return ret
222
223    # Return all ELF sections with the ALLOC flag
224    def read_sections(self):
225        if self._sections:
226            return
227        elf = self.get_elf(self._bin)
228        cmd = self.arch_prefix('objdump')
229        if not elf or not cmd:
230            return
231        objdump = subprocess.Popen([cmd, '--section-headers', elf],
232                                    stdin = subprocess.PIPE,
233                                    stdout = subprocess.PIPE)
234        for line in iter(objdump.stdout.readline, ''):
235            try:
236                _, name, size, vma, _, _, _ = line.split()
237            except:
238                if 'ALLOC' in line:
239                    self._sections.append([name, int(vma, 16), int(size, 16)])
240
241    def overlaps(self, section, addr, size):
242        sec_addr = section[1]
243        sec_size = section[2]
244        if not size or not sec_size:
245            return False
246        return (addr <= (sec_addr + sec_size - 1)) and ((addr + size - 1) >= sec_addr)
247
248    def sections_in_region(self, addr, size):
249        ret = ''
250        addr = self.subtract_load_addr(addr)
251        if not addr:
252            return ''
253        iaddr = int(addr, 16)
254        isize = int(size, 16)
255        self.read_sections()
256        for s in self._sections:
257            if self.overlaps(s, iaddr, isize):
258                ret += ' ' + s[0]
259        return ret
260
261    def reset(self):
262        self._call_stack_found = False
263        self._load_addr = '0'
264        if self._addr2line:
265            self._addr2line.terminate()
266            self._addr2line = None
267        self._arch = None
268        self._saved_abort_line = ''
269        self._sections = []
270        self._bin = "tee.elf"
271
272    def write(self, line):
273            if self._call_stack_found:
274                match = re.search(STACK_ADDR_RE, line)
275                if match:
276                    addr = match.group('addr')
277                    pre = match.start('addr')
278                    post = match.end('addr')
279                    self._out.write(line[:pre])
280                    self._out.write(addr)
281                    res = self.resolve(addr)
282                    if self._strip_path:
283                        res = re.sub(re.escape(self._strip_path) + '/*', '',
284                              res)
285                    self._out.write(' ' + res)
286                    self._out.write(line[post:])
287                    return
288                else:
289                    self.reset()
290            match = re.search(REGION_RE, line)
291            if match:
292                addr = match.group('addr')
293                size = match.group('size')
294                self._out.write(line.strip() +
295                                self.sections_in_region(addr, size) + '\n');
296                return
297            match = re.search(CALL_STACK_RE, line)
298            if match:
299                self._call_stack_found = True
300                # Here is a good place to resolve the abort address because we
301                # have all the information we need
302                if self._saved_abort_line:
303                    self._out.write(self.process_abort(self._saved_abort_line))
304            match = re.search(TA_UUID_RE, line)
305            if match:
306                self._bin = match.group('uuid')
307            match = re.search(TA_INFO_RE, line)
308            if match:
309                self._load_addr = match.group('load_addr')
310            match = re.search(ABORT_ADDR_RE, line)
311            if match:
312                self.reset()
313                # At this point the arch and TA load address are unknown.
314                # Save the line so We can translate the abort address later.
315                self._saved_abort_line = line
316            self._out.write(line)
317
318    def flush(self):
319        self._out.flush()
320
321def main():
322    args = get_args()
323    if args.dir:
324        # Flatten list in case -d is used several times *and* with multiple
325        # arguments
326        args.dirs = [item for sublist in args.dir for item in sublist]
327    else:
328        args.dirs = []
329    symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path)
330
331    for line in sys.stdin:
332        symbolizer.write(line)
333    symbolizer.flush()
334
335if __name__ == "__main__":
336    main()
337