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 itertools import groupby
4+ from typing import List , Optional , Union , Dict
55import re
66import subprocess
77
@@ -23,50 +23,105 @@ 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 ) -> Dict [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+ # Prepare result (to preserve overall order)
38+ result : Dict [str , Optional [List [dict ]]] = {addr : None for addr in addresses }
39+
40+ # Check each elf file for matches
41+ for matcher in self .pc_address_matcher :
42+ elf_path = matcher .elf_path
43+ elf_addresses = [addr for addr in addresses if matcher .is_executable_address (int (addr , 16 ))]
44+ if not elf_addresses :
45+ continue
46+
47+ # Lookup addresses using addr2line
48+ mapped_addresses = self .lookup_pc_address (elf_addresses , is_rom = (elf_path == self .rom_elf_file ), elf_file = elf_path )
49+
50+ # Update result with the mapped addresses
51+ result .update (mapped_addresses )
52+
53+ # Stop searching for addresses that have been found
54+ addresses = [addr for addr in addresses if addr not in mapped_addresses .keys ()]
55+
56+ # Filter out addresses that could not be resolved
57+ result = {key : value for key , value in result .items () if value is not None }
58+
59+ return result
60+
61+
62+ def lookup_pc_address (
63+ self ,
64+ pc_addr : List [str ],
65+ is_rom : bool = False ,
66+ elf_file : str = ''
67+ ) -> Dict [str , List [dict ]]:
68+ """
69+ Decode a list of addresses using addr2line, returning a map from each address string
70+ to a tuple (function_name, path:line).
71+ """
72+ elf_file = elf_file if elf_file else (self .rom_elf_file if is_rom else self .elf_files [0 ]) # type: ignore
73+ cmd = [f'{ self .toolchain_prefix } addr2line' , '-fiaC' , '-e' , elf_file , * pc_addr ]
6174
6275 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' )
76+ batch_output = subprocess .check_output (cmd , cwd = '.' )
6777 except OSError as err :
6878 red_print (f'{ " " .join (cmd )} : { err } ' )
79+ return {}
6980 except subprocess .CalledProcessError as err :
7081 red_print (f'{ " " .join (cmd )} : { err } ' )
7182 red_print ('ELF file is missing or has changed, the build folder was probably modified.' )
72- return None
83+ return {}
84+
85+ decoded_output = batch_output .decode (errors = 'ignore' )
86+
87+ # Step 1: Split into sections where each section starts with an 8-hex-digit address
88+ sections = re .split (r'(?=0x[0-9A-Fa-f]{8}\r?\n)' , decoded_output )
89+
90+ result : Dict [str , List [dict ]] = {}
91+ for section in sections :
92+ section = section .strip ()
93+ if not section :
94+ continue
95+
96+ # Step 2: Split the section by newlines
97+ lines = section .split ('\n ' )
98+
99+ # Step 3: First line is the address
100+ address = lines [0 ].strip ()
101+
102+ # Step 4: Build trace by consuming lines in pairs (function, path:line)
103+ trace : List [dict ] = []
104+ for i in range (1 , len (lines ) - 1 , 2 ):
105+ fn = lines [i ].strip ()
106+ path_line = lines [i + 1 ].strip ()
107+
108+ # Remove any " (discriminator N)" suffix
109+ path_line = re .sub (r' \(discriminator \d+\)$' , '' , path_line )
110+
111+ # Split on the last colon before digits to separate path and line number
112+ parts = re .split (r':(?=\d+|\?$)' , path_line , maxsplit = 1 )
113+ if len (parts ) == 2 :
114+ path , line_str = parts
115+ line_num = int (line_str ) if line_str != '?' else '?'
116+ else :
117+ path = parts [0 ]
118+ line_num = 0
119+
120+ if path == '??' and is_rom :
121+ path = 'ROM'
122+
123+ trace .append ({'fn' : fn , 'path' : path , 'line' : line_num })
124+
125+ result [address ] = trace
126+
127+ return result
0 commit comments