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