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+ fn : str
23+ path : str
24+ line : str
1425
1526class 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