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
55import re
66import subprocess
77
1111# regex matches an potential address
1212ADDRESS_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
1526class 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