Skip to content

Commit 29d0f91

Browse files
committed
feat: Batch requests and extract fn/paths from addr2line
1 parent 8856af7 commit 29d0f91

File tree

1 file changed

+190
-36
lines changed

1 file changed

+190
-36
lines changed
Lines changed: 190 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+
func: str
23+
path: str
24+
line: str
1425

1526
class PcAddressDecoder:
1627
"""
@@ -23,50 +34,193 @@ 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+
48+
# Translate any addresses found in the line to their source locations
49+
decoded = self.translate_addresses(line.decode(errors='ignore'))
50+
if not decoded:
51+
return ''
52+
53+
# Synthesize the output of addr2line --pretty-print, while preserving improvements from translate_addresses
54+
# which relies on the non pretty-print output of addr2line.
55+
56+
# `decoded` contains [(0x40376121, [(func, path, line), ...]), ...]
57+
# Which gets converted to:
58+
# 0x40376121: func at path:line
59+
60+
def format_trace_entry(location: PcAddressLocation):
61+
if location.path == 'ROM':
62+
return f'{location.func} in ROM'
63+
64+
return f'{location.func} at {location.path}' + (f':{location.line}' if location.line else '')
65+
3566
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
67+
# For each address and its corresponding trace
68+
for addr, trace in decoded:
69+
# Append address
70+
out += f'{addr}: '
71+
if not trace:
72+
out += '(unknown)\n'
73+
continue
74+
75+
# Append first trace entry
76+
out += f'{format_trace_entry(trace[0])}\n'
77+
78+
# Any subsequent entries indicate inlined functions
79+
for entry in trace[1:]:
80+
out += f' (inlined by) {format_trace_entry(entry)}\n'
81+
5582
return out
5683

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

62150
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')
151+
batch_output = subprocess.check_output(cmd, cwd='.')
67152
except OSError as err:
68153
red_print(f'{" ".join(cmd)}: {err}')
154+
return {}
69155
except subprocess.CalledProcessError as err:
70156
red_print(f'{" ".join(cmd)}: {err}')
71157
red_print('ELF file is missing or has changed, the build folder was probably modified.')
72-
return None
158+
return {}
159+
160+
decoded_output = batch_output.decode(errors='ignore')
161+
162+
return PcAddressDecoder.parse_addr2line_output(decoded_output, is_rom=is_rom)
163+
164+
@staticmethod
165+
def parse_addr2line_output(
166+
output: str,
167+
is_rom: bool = False,
168+
) -> Dict[str, List[PcAddressLocation]]:
169+
"""
170+
Parse the output of addr2line.
171+
:param output: The output of addr2line as a string.
172+
:param is_rom: If True, replace '??' paths with 'ROM' as paths are not available from ROM ELF files.
173+
:return: Map from each address to a list of its source locations (with multiple indicating an inlined function).
174+
"""
175+
176+
# == addr2line output example ==
177+
# 0xabcd1234 # Aad # First input address
178+
# foo() # A0f # Function
179+
# foo.c:123 # A0p # Source location
180+
# 0x1234abcd # Bad # Second input address
181+
# inlined() # B0f # Inlined function
182+
# bar.c:456 # B0p # Source location
183+
# bar() # B1f # Function which inlined inlined()
184+
# bar.c:789 # B1p # Source location
185+
# ... # ... # ... more entries
186+
187+
# Step 1: Split into sections representing each address and its trace (A**, B**)
188+
sections = re.split(ADDR2LINE_ADDRESS_LOOKAHEAD_RE, output)
189+
190+
result: Dict[str, List[PcAddressLocation]] = {}
191+
for section in sections:
192+
section = section.strip() # Remove trailing newline
193+
if not section:
194+
continue
195+
196+
# Step 2: Split the section by newlines (Aad, A0f, A0p)
197+
lines = section.split('\n')
198+
199+
# Step 3: First line is the address (Aad)
200+
address = lines[0].strip()
201+
202+
# Step 4: Build trace by consuming lines in pairs (A0f + A0p)
203+
# Multiple entries indicate inlined functions (B0f + B0p, B1f + B1p, etc.)
204+
trace: List[PcAddressLocation] = []
205+
valid = False
206+
for func, path_line in zip(map(str.strip, lines[1::2]), map(str.strip, lines[2::2])):
207+
path_match = ADDR2LINE_FILE_LINE_RE.match(path_line)
208+
path = path_match.group('file') if path_match else path_line
209+
line = path_match.group('line') if path_match else ''
210+
211+
# If any entry's function or path are present the trace is valid
212+
# Otherwise if none of the entries are valid, we skip this address
213+
valid = valid or func != '??' or path != '??'
214+
215+
# ROM ELF files do not provide paths, so we replace '??' with 'ROM'
216+
if path == '??' and is_rom:
217+
path = 'ROM'
218+
219+
# Add the trace entry
220+
trace.append(PcAddressLocation(func, path, line))
221+
222+
# Step 5: Store the address and its trace in result (if valid and contains entries), go to next section
223+
if valid and trace:
224+
result[address] = trace
225+
226+
return result

0 commit comments

Comments
 (0)