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