Skip to content

Commit 1a86f37

Browse files
committed
feat: Batch requests and extract fn/paths from addr2line
1 parent 0a8c83d commit 1a86f37

File tree

1 file changed

+176
-36
lines changed

1 file changed

+176
-36
lines changed
Lines changed: 176 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Apache-2.0
3-
4-
from typing import List, Optional, Union
3+
from dataclasses import dataclass
4+
from typing import List, Optional, Union, Dict, Tuple
55
import re
66
import subprocess
77

@@ -11,6 +11,17 @@
1111
# regex matches an potential address
1212
ADDRESS_RE = re.compile(r'0x[0-9a-f]{8}', re.IGNORECASE)
1313

14+
# regex to split address sections in addr2line output (lookahead to preserve address when splitting)
15+
ADDR2LINE_ADDRESS_LOOKAHEAD_RE = re.compile(r'(?=0x[0-9a-f]{8}\r?\n)')
16+
# regex matches filename and line number in addr2line output (and ignores discriminators)
17+
ADDR2LINE_FILE_LINE_RE = re.compile(r'(?P<file>.*):(?P<line>\d+|\?)(?: \(discriminator \d+\))?$')
18+
19+
# Decoded PC address trace
20+
@dataclass
21+
class PcAddressLocation:
22+
fn: str
23+
path: str
24+
line: str
1425

1526
class PcAddressDecoder:
1627
"""
@@ -23,50 +34,179 @@ def __init__(
2334
self.toolchain_prefix = toolchain_prefix
2435
self.elf_files = elf_file if isinstance(elf_file, list) else [elf_file]
2536
self.rom_elf_file = rom_elf_file
26-
self.pc_address_buffer = b''
2737
self.pc_address_matcher = [PcAddressMatcher(file) for file in self.elf_files]
28-
if rom_elf_file is not None:
29-
self.rom_pc_address_matcher = PcAddressMatcher(rom_elf_file)
38+
if self.rom_elf_file:
39+
self.pc_address_matcher.append(PcAddressMatcher(self.rom_elf_file))
3040

3141
def decode_address(self, line: bytes) -> str:
32-
"""Decoded possible addresses in line"""
33-
line = self.pc_address_buffer + line
34-
self.pc_address_buffer = b''
42+
"""
43+
Find executable addresses in a line and translate them to source locations using addr2line.
44+
**Deprecated**: Method preserved for esp-idf-monitor < 1.7 compatibility - use `translate_addresses` instead.
45+
:return: A string containing human-readable addr2line output for the addresses found in the line.
46+
"""
47+
# Translate any addresses found in the line to their source locations
48+
decoded = self.translate_addresses(line.decode(errors='ignore'))
49+
if not decoded:
50+
return ''
51+
52+
# Synthesize the output of addr2line --pretty-print, while preserving improvements from translate_addresses
53+
# which relies on the non pretty-print output of addr2line.
54+
def format_trace_entry(tr: PcAddressLocation):
55+
if tr.path == 'ROM':
56+
return f'{tr.fn} in ROM'
57+
58+
return f'{tr.fn} at {tr.path}' + (f':{tr.line}' if tr.line else '')
59+
3560
out = ''
36-
for match in re.finditer(ADDRESS_RE, line.decode(errors='ignore')):
37-
num = match.group()
38-
address_int = int(num, 16)
39-
translation = None
40-
41-
# Try looking for the address in the app ELF files
42-
for matcher in self.pc_address_matcher:
43-
if matcher.is_executable_address(address_int):
44-
translation = self.lookup_pc_address(num, elf_file=matcher.elf_path)
45-
if translation is not None:
46-
break
47-
# Not found in app ELF file, check ROM ELF file (if it is available)
48-
if translation is None and self.rom_elf_file is not None and \
49-
self.rom_pc_address_matcher.is_executable_address(address_int):
50-
translation = self.lookup_pc_address(num, is_rom=True, elf_file=self.rom_elf_file)
51-
52-
# Translation found either in the app or ROM ELF file
53-
if translation is not None:
54-
out += translation
61+
# For each address and its corresponding trace
62+
for addr, trace in decoded:
63+
# Append address
64+
out += f'{addr}: '
65+
if not trace:
66+
out += '(unknown)\n'
67+
continue
68+
69+
# Append first trace entry
70+
out += f'{format_trace_entry(trace[0])}\n'
71+
72+
# Any subsequent entries indicate inlined functions
73+
for entry in trace[1:]:
74+
out += f' (inlined by) {format_trace_entry(entry)}\n'
75+
5576
return out
5677

57-
def lookup_pc_address(self, pc_addr: str, is_rom: bool = False, elf_file: str = '') -> Optional[str]:
58-
"""Decode address using addr2line tool"""
59-
elf_file: str = elf_file if elf_file else self.rom_elf_file if is_rom else self.elf_files[0] # type: ignore
60-
cmd = [f'{self.toolchain_prefix}addr2line', '-pfiaC', '-e', elf_file, pc_addr]
78+
def translate_addresses(self, line: str) -> List[Tuple[str, List[PcAddressLocation]]]:
79+
"""
80+
Find executable addresses in a line and translate them to source locations using addr2line.
81+
:param line: The line to decode, as a string.
82+
:return: List of addresses and their source locations (with multiple locations indicating an inlined function).
83+
"""
84+
85+
# === Example input line ===
86+
# Backtrace: 0x40376121:0x3fcb5590 0x40384ef9:0x3fcb55b0 0x4202c8c9:0x3fcb55d0
87+
# Each pair represents a program counter (PC) address and a stack pointer (SP) address.
88+
# We parse them all and filter out those that are not considered executable by one of the configured ELF files.
89+
90+
# Find all hex addresses (0x40376121, 0x3fcb5590, etc.)
91+
addresses = re.findall(ADDRESS_RE, line)
92+
if not addresses:
93+
return []
94+
95+
# Addresses left to find (initially a copy of addresses: 0x40376121, 0x3fcb5590, etc.)
96+
remaining = addresses.copy()
97+
98+
# Mapped addresses (0x40376121 => [(fn, path, line), ...])
99+
mapped: Dict[str, List[PcAddressLocation]] = {}
100+
101+
# Iterate through available ELF files
102+
for matcher in self.pc_address_matcher:
103+
elf_path = matcher.elf_path
104+
is_rom = elf_path == self.rom_elf_file
105+
106+
# Find any remaining addresses that are executable in this ELF file
107+
elf_addresses = [addr for addr in remaining if matcher.is_executable_address(int(addr, 16))]
108+
if not elf_addresses:
109+
continue
110+
111+
# Translate addresses using addr2line
112+
elf_mapped = self.perform_addr2line(addresses=elf_addresses, elf_file=elf_path, is_rom=is_rom)
113+
114+
# Update shared mapped addresses
115+
mapped.update(elf_mapped)
116+
117+
# Stop searching for addresses that have been found (even if they may exist in other ELF files)
118+
remaining = [addr for addr in remaining if addr not in elf_mapped]
119+
120+
# If there are no remaining addresses, we can stop looking through ELF files
121+
if not remaining:
122+
break
123+
124+
# All discovered and translated addresses are now in `mapped`, but they are ordered based on the ELF files.
125+
# Recreate the original order of `addresses`, allowing also for multiple instances of the same address.
126+
# [(0x40376121, [(fn, path, line), ...]), ...]
127+
return [(addr, mapped[addr]) for addr in addresses if addr in mapped]
128+
129+
def perform_addr2line(
130+
self,
131+
addresses: List[str],
132+
elf_file: str,
133+
is_rom: bool = False,
134+
) -> Dict[str, List[PcAddressLocation]]:
135+
"""
136+
Translate a list of executable addresses to source locations using addr2line.
137+
:param addresses: List of addresses to translate.
138+
:param elf_file: The ELF file eto use for translating.
139+
:param is_rom: If True, replace '??' paths with 'ROM' as paths are not available from ROM ELF files.
140+
:return: Map from each address to a list of its source locations (with multiple indicating an inlined function).
141+
"""
142+
cmd = [f'{self.toolchain_prefix}addr2line', '-fiaC', '-e', elf_file, *addresses]
61143

62144
try:
63-
translation = subprocess.check_output(cmd, cwd='.')
64-
if b'?? ??:0' not in translation:
65-
decoded = translation.decode()
66-
return decoded if not is_rom else decoded.replace('at ??:?', 'in ROM')
145+
batch_output = subprocess.check_output(cmd, cwd='.')
67146
except OSError as err:
68147
red_print(f'{" ".join(cmd)}: {err}')
148+
return {}
69149
except subprocess.CalledProcessError as err:
70150
red_print(f'{" ".join(cmd)}: {err}')
71151
red_print('ELF file is missing or has changed, the build folder was probably modified.')
72-
return None
152+
return {}
153+
154+
decoded_output = batch_output.decode(errors='ignore')
155+
156+
return PcAddressDecoder.parse_addr2line_output(decoded_output, is_rom=is_rom)
157+
158+
@staticmethod
159+
def parse_addr2line_output(
160+
output: str,
161+
is_rom: bool = False,
162+
) -> Dict[str, List[PcAddressLocation]]:
163+
"""
164+
Parse the output of addr2line.
165+
:param output: The output of addr2line as a string.
166+
:param is_rom: If True, replace '??' paths with 'ROM' as paths are not available from ROM ELF files.
167+
:return: Map from each address to a list of its source locations (with multiple indicating an inlined function).
168+
"""
169+
170+
# == addr2line output example ==
171+
# 0xabcd1234 # Aad # First input address
172+
# foo() # A0f # Function
173+
# foo.c:123 # A0p # Source location
174+
# 0x1234abcd # Bad # Second input address
175+
# inlined() # B0f # Inlined function
176+
# bar.c:456 # B0p # Source location
177+
# bar() # B1f # Function which inlined inlined()
178+
# bar.c:789 # B1p # Source location
179+
# ... # ... # ... more entries
180+
181+
# Step 1: Split into sections representing each address and its trace (A**, B**)
182+
sections = re.split(ADDR2LINE_ADDRESS_LOOKAHEAD_RE, output)
183+
184+
result: Dict[str, List[PcAddressLocation]] = {}
185+
for section in sections:
186+
section = section.strip() # Remove trailing newline
187+
if not section:
188+
continue
189+
190+
# Step 2: Split the section by newlines (Aad, A0f, A0p)
191+
lines = section.split('\n')
192+
193+
# Step 3: First line is the address (Aad)
194+
address = lines[0].strip()
195+
196+
# Step 4: Build trace by consuming lines in pairs (A0f + A0p)
197+
# Multiple entries indicate inlined functions (B0f + B0p, B1f + B1p, etc.)
198+
trace: List[PcAddressLocation] = []
199+
for fn, path_line in zip(map(str.strip, lines[1::2]), map(str.strip, lines[2::2])):
200+
path_match = ADDR2LINE_FILE_LINE_RE.match(path_line)
201+
path = path_match.group('file') if path_match else path_line
202+
line = path_match.group('line') if path_match else ''
203+
if path == '??' and is_rom:
204+
path = 'ROM'
205+
206+
# Add the trace entry
207+
trace.append(PcAddressLocation(fn, path, line))
208+
209+
# Step 5: Store the address and its trace in result, go to next section
210+
result[address] = trace
211+
212+
return result

0 commit comments

Comments
 (0)