xref: /optee_os/scripts/symbolize.py (revision 309991262b5d996439962aabedd341dcd206abff)
1#!/usr/bin/env python
2#
3# Copyright (c) 2017, Linaro Limited
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8#
9# 1. Redistributions of source code must retain the above copyright notice,
10# this list of conditions and the following disclaimer.
11#
12# 2. Redistributions in binary form must reproduce the above copyright notice,
13# this list of conditions and the following disclaimer in the documentation
14# and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26# POSSIBILITY OF SUCH DAMAGE.
27#
28
29
30import argparse
31import glob
32import os
33import re
34import subprocess
35import sys
36
37TA_UUID_RE = re.compile(r'Status of TA (?P<uuid>[0-9a-f\-]+)')
38TA_INFO_RE = re.compile(':  arch: (?P<arch>\w+)  '
39                        'load address: (?P<load_addr>0x[0-9a-f]+)')
40CALL_STACK_RE = re.compile('Call stack:')
41STACK_ADDR_RE = re.compile(r':  (?P<addr>0x[0-9a-f]+)')
42ABORT_ADDR_RE = re.compile('-abort at address (?P<addr>0x[0-9a-f]+)')
43REGION_RE = re.compile('region [0-9]+: va (?P<addr>0x[0-9a-f]+) '
44                       'pa 0x[0-9a-f]+ size (?P<size>0x[0-9a-f]+)')
45
46epilog = '''
47This scripts reads an OP-TEE abort message from stdin and adds debug
48information ('function at file:line') next to each address in the call stack.
49It uses the paths provided on the command line to locate the appropriate ELF
50binary (tee.elf or Trusted Application) and runs arm-linux-gnueabihf-addr2line
51or aarch64-linux-gnu-addr2line to process the addresses.
52
53OP-TEE abort messages are sent to the secure console. They look like the
54following:
55
56  ERROR:   TEE-CORE: User TA data-abort at address 0xffffdecd (alignment fault)
57  ...
58  ERROR:   TEE-CORE: Call stack:
59  ERROR:   TEE-CORE:  0x4000549e
60  ERROR:   TEE-CORE:  0x40001f4b
61  ERROR:   TEE-CORE:  0x4000273f
62  ERROR:   TEE-CORE:  0x40005da7
63
64Inspired by a script of the same name by the Chromium project.
65
66Sample usage:
67
68  $ scripts/symbolize.py -d out/arm-plat-hikey/core -d ../optee_test/out/ta/*
69  <paste whole dump here>
70  ^D
71'''
72
73def get_args():
74    parser = argparse.ArgumentParser(
75                formatter_class=argparse.RawDescriptionHelpFormatter,
76                description='Symbolizes OP-TEE abort dumps',
77                epilog=epilog)
78    parser.add_argument('-d', '--dir', action='append', nargs='+',
79        help='Search for ELF file in DIR. tee.elf is needed to decode '
80             'a TEE Core or pseudo-TA abort, while <TA_uuid>.elf is required '
81             'if a user-mode TA has crashed. For convenience, ELF files '
82             'may also be given.')
83    parser.add_argument('-s', '--strip_path',
84        help='Strip STRIP_PATH from file paths')
85
86    return parser.parse_args()
87
88class Symbolizer(object):
89    def __init__(self, out, dirs, strip_path):
90        self._out = out
91        self._dirs = dirs
92        self._strip_path = strip_path
93        self._addr2line = None
94        self._bin = 'tee.elf'
95        self.reset()
96
97    def get_elf(self, elf_or_uuid):
98        if not elf_or_uuid.endswith('.elf'):
99            elf_or_uuid += '.elf'
100        for d in self._dirs:
101            if d.endswith(elf_or_uuid) and os.path.isfile(d):
102                return d
103            elf = glob.glob(d + '/' + elf_or_uuid)
104            if elf:
105                return elf[0]
106
107    def set_arch(self):
108        if self._arch:
109            return
110        if self._bin:
111            p = subprocess.Popen([ 'file', self.get_elf(self._bin) ],
112                                 stdout=subprocess.PIPE)
113            output = p.stdout.readlines()
114            p.terminate()
115            if 'ARM aarch64,' in output[0]:
116                self._arch = 'aarch64-linux-gnu-'
117            elif 'ARM,' in output[0]:
118                self._arch = 'arm-linux-gnueabihf-'
119
120    def arch_prefix(self, cmd):
121        self.set_arch()
122        return self._arch + cmd
123
124    def spawn_addr2line(self):
125        if not self._addr2line:
126            elf = self.get_elf(self._bin)
127            if not elf:
128                return
129            cmd = self.arch_prefix('addr2line')
130            if not cmd:
131                return
132            self._addr2line = subprocess.Popen([cmd, '-f', '-p', '-e', elf],
133                                                stdin = subprocess.PIPE,
134                                                stdout = subprocess.PIPE)
135
136    def subtract_load_addr(self, addr):
137        offs = self._load_addr
138        if int(offs, 16) > int(addr, 16):
139            return ''
140        return '0x{:x}'.format(int(addr, 16) - int(offs, 16))
141
142    def resolve(self, addr):
143        reladdr = self.subtract_load_addr(addr)
144        self.spawn_addr2line()
145        if not reladdr or not self._addr2line:
146            return '???'
147        try:
148            print >> self._addr2line.stdin, reladdr
149            ret = self._addr2line.stdout.readline().rstrip('\n')
150        except IOError:
151            ret = '!!!'
152        return ret
153
154    def symbol_plus_offset(self, addr):
155        ret = ''
156        prevsize = 0
157        reladdr = self.subtract_load_addr(addr)
158        elf = self.get_elf(self._bin)
159        cmd = self.arch_prefix('nm')
160        if not reladdr or not elf or not cmd:
161            return ''
162        ireladdr = int(reladdr, 16)
163        nm = subprocess.Popen([cmd, '--numeric-sort', '--print-size', elf],
164                               stdin = subprocess.PIPE,
165                               stdout = subprocess.PIPE)
166        for line in iter(nm.stdout.readline, ''):
167            try:
168                addr, size, _, name = line.split()
169            except:
170                # Size is missing
171                addr, _, name = line.split()
172                size = '0'
173            iaddr = int(addr, 16)
174            isize = int(size, 16)
175            if iaddr == ireladdr:
176                ret = name
177                break
178            if iaddr < ireladdr and iaddr + isize >= ireladdr:
179                offs = ireladdr - iaddr
180                ret = name + '+' + str(offs)
181                break
182            if iaddr > ireladdr and prevsize == 0:
183                offs = iaddr + ireladdr
184                ret = prevname + '+' + str(offs)
185                break
186            prevsize = size
187            prevname = name
188        nm.terminate()
189        return ret
190
191    def section_plus_offset(self, addr):
192        ret = ''
193        reladdr = self.subtract_load_addr(addr)
194        elf = self.get_elf(self._bin)
195        cmd = self.arch_prefix('objdump')
196        if not reladdr or not elf or not cmd:
197            return ''
198        iaddr = int(reladdr, 16)
199        objdump = subprocess.Popen([cmd, '--section-headers', elf],
200                                    stdin = subprocess.PIPE,
201                                    stdout = subprocess.PIPE)
202        for line in iter(objdump.stdout.readline, ''):
203            try:
204                idx, name, size, vma, lma, offs, algn = line.split()
205            except:
206                continue;
207            ivma = int(vma, 16)
208            isize = int(size, 16)
209            if ivma == iaddr:
210                ret = name
211                break
212            if ivma < iaddr and ivma + isize >= iaddr:
213                offs = iaddr - ivma
214                ret = name + '+' + str(offs)
215                break
216        objdump.terminate()
217        return ret
218
219    def process_abort(self, line):
220        ret = ''
221        match = re.search(ABORT_ADDR_RE, line)
222        addr = match.group('addr')
223        pre = match.start('addr')
224        post = match.end('addr')
225        sym = self.symbol_plus_offset(addr)
226        sec = self.section_plus_offset(addr)
227        if sym or sec:
228            ret += line[:pre]
229            ret += addr
230            if sym:
231                ret += ' ' + sym
232            if sec:
233                ret += ' ' + sec
234            ret += line[post:]
235        return ret
236
237    # Return all ELF sections with the ALLOC flag
238    def read_sections(self):
239        if self._sections:
240            return
241        elf = self.get_elf(self._bin)
242        cmd = self.arch_prefix('objdump')
243        if not elf or not cmd:
244            return
245        objdump = subprocess.Popen([cmd, '--section-headers', elf],
246                                    stdin = subprocess.PIPE,
247                                    stdout = subprocess.PIPE)
248        for line in iter(objdump.stdout.readline, ''):
249            try:
250                _, name, size, vma, _, _, _ = line.split()
251            except:
252                if 'ALLOC' in line:
253                    self._sections.append([name, int(vma, 16), int(size, 16)])
254
255    def overlaps(self, section, addr, size):
256        sec_addr = section[1]
257        sec_size = section[2]
258        if not size or not sec_size:
259            return False
260        return (addr <= (sec_addr + sec_size - 1)) and ((addr + size - 1) >= sec_addr)
261
262    def sections_in_region(self, addr, size):
263        ret = ''
264        addr = self.subtract_load_addr(addr)
265        if not addr:
266            return ''
267        iaddr = int(addr, 16)
268        isize = int(size, 16)
269        self.read_sections()
270        for s in self._sections:
271            if self.overlaps(s, iaddr, isize):
272                ret += ' ' + s[0]
273        return ret
274
275    def reset(self):
276        self._call_stack_found = False
277        self._load_addr = '0'
278        if self._addr2line:
279            self._addr2line.terminate()
280            self._addr2line = None
281        self._arch = None
282        self._saved_abort_line = ''
283        self._sections = []
284
285    def write(self, line):
286            if self._call_stack_found:
287                match = re.search(STACK_ADDR_RE, line)
288                if match:
289                    addr = match.group('addr')
290                    pre = match.start('addr')
291                    post = match.end('addr')
292                    self._out.write(line[:pre])
293                    self._out.write(addr)
294                    res = self.resolve(addr)
295                    if self._strip_path:
296                        res = re.sub(re.escape(self._strip_path) + '/*', '',
297                              res)
298                    self._out.write(' ' + res)
299                    self._out.write(line[post:])
300                    return
301                else:
302                    self.reset()
303            match = re.search(REGION_RE, line)
304            if match:
305                addr = match.group('addr')
306                size = match.group('size')
307                self._out.write(line.strip() +
308                                self.sections_in_region(addr, size) + '\n');
309                return
310            match = re.search(CALL_STACK_RE, line)
311            if match:
312                self._call_stack_found = True
313                # Here is a good place to resolve the abort address because we
314                # have all the information we need
315                if self._saved_abort_line:
316                    self._out.write(self.process_abort(self._saved_abort_line))
317            match = re.search(TA_UUID_RE, line)
318            if match:
319                self._bin = match.group('uuid')
320            match = re.search(TA_INFO_RE, line)
321            if match:
322                self._load_addr = match.group('load_addr')
323            match = re.search(ABORT_ADDR_RE, line)
324            if match:
325                # At this point the arch and TA load address are unknown.
326                # Save the line so We can translate the abort address later.
327                self._saved_abort_line = line
328            self._out.write(line)
329
330    def flush(self):
331        self._out.flush()
332
333def main():
334    args = get_args()
335    if args.dir:
336        # Flatten list in case -d is used several times *and* with multiple
337        # arguments
338        args.dirs = [item for sublist in args.dir for item in sublist]
339    else:
340        args.dirs = []
341    symbolizer = Symbolizer(sys.stdout, args.dirs, args.strip_path)
342
343    for line in sys.stdin:
344        symbolizer.write(line)
345    symbolizer.flush()
346
347if __name__ == "__main__":
348    main()
349