xref: /aosp_15_r20/external/pigweed/pw_symbolizer/py/pw_symbolizer/symbolizer.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Utilities for address symbolization."""
15
16import abc
17from typing import Iterable
18from dataclasses import dataclass
19
20
21@dataclass(frozen=True)
22class Symbol:
23    """Symbols produced by a symbolizer."""
24
25    address: int
26    name: str = ''
27    file: str = ''
28    line: int = 0
29
30    def to_string(self, max_filename_len: int = 0) -> str:
31        if not self.name:
32            name = f'0x{self.address:08X}'
33        else:
34            name = self.name
35
36        return f'{name} ({self.file_and_line(max_filename_len)})'
37
38    def file_and_line(self, max_filename_len: int = 0) -> str:
39        """Returns a file/line number string, with question marks if unknown."""
40
41        if not self.file:
42            return '??:?'
43
44        if max_filename_len and len(self.file) > max_filename_len:
45            return f'[...]{self.file[-max_filename_len:]}:{self.line}'
46
47        return f'{self.file}:{self.line}'
48
49    def __str__(self):
50        return self.to_string()
51
52
53class Symbolizer(abc.ABC):
54    """An interface for symbolizing addresses."""
55
56    @abc.abstractmethod
57    def symbolize(self, address: int) -> Symbol:
58        """Symbolizes an address using a loaded binary or symbol database."""
59
60    def dump_stack_trace(
61        self, addresses, most_recent_first: bool = True
62    ) -> str:
63        """Symbolizes and dumps a list of addresses as a stack trace.
64
65        most_recent_first controls the hint provided at the top of the stack
66        trace. If call stack depth increases with each element in the input
67        list, most_recent_first should be false.
68        """
69        order: str = 'first' if most_recent_first else 'last'
70
71        stack_trace: list[str] = []
72        stack_trace.append(f'Stack Trace (most recent call {order}):')
73
74        max_width = len(str(len(addresses)))
75        for i, address in enumerate(addresses):
76            depth = i + 1
77            symbol = self.symbolize(address)
78
79            if symbol.name:
80                sym_desc = f'{symbol.name} (0x{symbol.address:08X})'
81            else:
82                sym_desc = f'(0x{symbol.address:08X})'
83
84            stack_trace.append(f'  {depth:>{max_width}}: at {sym_desc}')
85            stack_trace.append(f'      in {symbol.file_and_line()}')
86
87        return '\n'.join(stack_trace)
88
89
90class FakeSymbolizer(Symbolizer):
91    """A fake symbolizer that only knows a fixed set of symbols."""
92
93    def __init__(self, known_symbols: Iterable[Symbol] | None = None):
94        if known_symbols is not None:
95            self._db = {sym.address: sym for sym in known_symbols}
96        else:
97            self._db = {}
98
99    def symbolize(self, address: int) -> Symbol:
100        """Symbolizes a fixed symbol database."""
101        if address in self._db:
102            return self._db[address]
103
104        return Symbol(address)
105