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