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 typing import List , Optional , Union , Dict , Tuple
54import re
65import subprocess
76
@@ -23,50 +22,107 @@ def __init__(
2322 self .toolchain_prefix = toolchain_prefix
2423 self .elf_files = elf_file if isinstance (elf_file , list ) else [elf_file ]
2524 self .rom_elf_file = rom_elf_file
26- self .pc_address_buffer = b''
2725 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 )
30-
31- 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''
35- 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
55- return out
56-
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 ]
26+ if self .rom_elf_file :
27+ self .pc_address_matcher .append (PcAddressMatcher (self .rom_elf_file ))
28+
29+
30+ def decode_addresses (self , line : bytes ) -> List [Tuple [str , List [dict ]]]:
31+ """Decode possible addresses in a line, batching addr2line calls per ELF."""
32+
33+ # Find all hex addresses
34+ addresses = re .findall (ADDRESS_RE , line .decode (errors = 'ignore' ))
35+
36+ # Mapped addresses
37+ mapped : Dict [str , List [dict ]] = {}
38+
39+ # Addresses left to find
40+ remaining = addresses .copy ()
41+
42+ # Check each elf file for matches
43+ for matcher in self .pc_address_matcher :
44+ elf_path = matcher .elf_path
45+ is_rom = elf_path == self .rom_elf_file
46+ elf_addresses = [addr for addr in addresses if matcher .is_executable_address (int (addr , 16 ))]
47+ if not elf_addresses :
48+ continue
49+
50+ # Lookup addresses using addr2line
51+ mapped_addresses = self .lookup_pc_address (elf_addresses , is_rom = is_rom , elf_file = elf_path )
52+
53+ # Store mapped addresses
54+ mapped .update (mapped_addresses )
55+
56+ # Stop searching for addresses that have been found
57+ remaining = [addr for addr in remaining if addr not in mapped_addresses ]
58+
59+ # Return all mapped addresses that were found, in the original order
60+ return [(addr , mapped [addr ]) for addr in addresses if addr in mapped ]
61+
62+
63+ def lookup_pc_address (
64+ self ,
65+ pc_addr : List [str ],
66+ is_rom : bool = False ,
67+ elf_file : str = ''
68+ ) -> Dict [str , List [dict ]]:
69+ """
70+ Decode a list of addresses using addr2line, returning a map from each address string
71+ to a tuple (function_name, path:line).
72+ """
73+ elf_file = elf_file if elf_file else (self .rom_elf_file if is_rom else self .elf_files [0 ]) # type: ignore
74+ cmd = [f'{ self .toolchain_prefix } addr2line' , '-fiaC' , '-e' , elf_file , * pc_addr ]
6175
6276 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' )
77+ batch_output = subprocess .check_output (cmd , cwd = '.' )
6778 except OSError as err :
6879 red_print (f'{ " " .join (cmd )} : { err } ' )
80+ return {}
6981 except subprocess .CalledProcessError as err :
7082 red_print (f'{ " " .join (cmd )} : { err } ' )
7183 red_print ('ELF file is missing or has changed, the build folder was probably modified.' )
72- return None
84+ return {}
85+
86+ decoded_output = batch_output .decode (errors = 'ignore' )
87+
88+ # Step 1: Split into sections where each section starts with an 8-hex-digit address
89+ sections = re .split (r'(?=0x[0-9A-Fa-f]{8}\r?\n)' , decoded_output )
90+
91+ result : Dict [str , List [dict ]] = {}
92+ for section in sections :
93+ section = section .strip ()
94+ if not section :
95+ continue
96+
97+ # Step 2: Split the section by newlines
98+ lines = section .split ('\n ' )
99+
100+ # Step 3: First line is the address
101+ address = lines [0 ].strip ()
102+
103+ # Step 4: Build trace by consuming lines in pairs (function, path:line)
104+ trace : List [dict ] = []
105+ for i in range (1 , len (lines ) - 1 , 2 ):
106+ fn = lines [i ].strip ()
107+ path_line = lines [i + 1 ].strip ()
108+
109+ # Remove any " (discriminator N)" suffix
110+ path_line = re .sub (r' \(discriminator \d+\)$' , '' , path_line )
111+
112+ # Split on the last colon before digits to separate path and line number
113+ parts = re .split (r':(?=\d+|\?$)' , path_line , maxsplit = 1 )
114+ if len (parts ) == 2 :
115+ path , line_str = parts
116+ line_num = int (line_str ) if line_str != '?' else '?'
117+ else :
118+ path = parts [0 ]
119+ line_num = 0
120+
121+ if path == '??' and is_rom :
122+ path = 'ROM'
123+
124+ trace .append ({'fn' : fn , 'path' : path , 'line' : line_num })
125+
126+ result [address ] = trace
127+
128+ return result
0 commit comments