add analyze function for exe

This commit is contained in:
VALLONGOL 2025-05-23 15:34:47 +02:00
parent 4aed5cee70
commit b6db1c8623
3 changed files with 754 additions and 671 deletions

View File

@ -76,6 +76,60 @@ class GDBSession:
if self.dumper_options:
logger.info(f"Dumper options provided: {self.dumper_options}")
def get_gdb_version(self, timeout: int = DEFAULT_GDB_OPERATION_TIMEOUT) -> Optional[str]:
"""
Retrieves the GDB version string.
Args:
timeout: Timeout for the GDB command.
Returns:
The GDB version string (typically the first line of 'gdb --version'),
or None if an error occurs or version cannot be parsed.
"""
if not self.child or not self.child.isalive():
# Questo metodo potrebbe essere chiamato anche prima che una sessione completa sia "startata"
# per l'analisi dei simboli, quindi potremmo dover avviare GDB solo per questo.
# Per ora, assumiamo che sia chiamato su una sessione già avviata,
# o che il chiamante gestisca l'avvio/chiusura di una sessione temporanea.
# In alternativa, potrebbe essere un metodo statico o una funzione helper
# che lancia 'gdb --version' come processo separato.
# Per coerenza con gli altri metodi, lo lasciamo come metodo d'istanza.
# Se la sessione non è 'start()'ata (cioè non c'è un eseguibile caricato),
# GDB potrebbe comunque rispondere a 'show version'.
logger.warning("GDB session not fully active, attempting 'show version'.")
# Se child non esiste, non possiamo fare nulla qui.
# Il chiamante (es. ProfileManagerWindow per l'analisi) dovrà gestire
# l'avvio di una sessione GDB se necessario.
# Questa implementazione assume che self.child esista.
if not self.child:
logger.error("No GDB child process available to get version.")
return None
# Usiamo 'show version' che funziona all'interno di una sessione GDB attiva
# 'gdb --version' è per l'uso da riga di comando esterna.
command = "show version"
logger.info(f"Requesting GDB version with command: '{command}'")
try:
output = self.send_cmd(command, expect_prompt=True, timeout=timeout)
# L'output di 'show version' è multiriga. La prima riga è di solito quella che vogliamo.
# Esempio:
# GNU gdb (GDB) 16.2
# Copyright (C) 2024 Free Software Foundation, Inc.
# ...
if output:
first_line = output.splitlines()[0].strip()
logger.info(f"GDB version string: {first_line}")
return first_line
logger.warning("No output received for 'show version' command.")
return None
except (ConnectionError, TimeoutError) as e:
logger.error(f"Error getting GDB version: {e}", exc_info=True)
return None
except Exception as e_parse:
logger.error(f"Error parsing 'show version' output: {e_parse}", exc_info=True)
return None
def start(self, timeout: int = DEFAULT_GDB_OPERATION_TIMEOUT) -> None:
command = f'"{self.gdb_path}" --nx --quiet "{self.executable_path}"'
logger.info(f"Spawning GDB process: {command} with startup timeout: {timeout}s")
@ -140,6 +194,8 @@ class GDBSession:
def list_functions(self, regex_filter: Optional[str] = None, timeout: int = DEFAULT_GDB_OPERATION_TIMEOUT) -> List[str]:
"""
Lists functions known to GDB, optionally filtered by a regex.
(Implementazione precedente di list_functions è già abbastanza buona, la riporto qui per completezza
assicurandoci che sia allineata con le necessità)
Args:
regex_filter: Optional regex to filter function names.
@ -159,109 +215,83 @@ class GDBSession:
logger.info(f"Requesting GDB function list with command: '{command}'")
functions: List[str] = []
try:
# Assicurarsi che la paginazione sia disattivata è gestito in start()
output = self.send_cmd(command, expect_prompt=True, timeout=timeout)
# L'output di 'info functions' può essere complesso.
# Esempio:
# File my_source.cpp:
# 123: void MyClass::myMethod(int);
# 456: int anotherFunction();
# Non-debugging symbols:
# 0x00401000 _start
# 0x004010a0 __do_global_dtors_aux
#
# Cerchiamo di estrarre nomi di funzioni che sembrano validi identificatori C/C++
# Questo regex cerca identificatori che possono includere :: e <template parametri>,
# spesso terminanti con ( o a volte solo il nome se è un simbolo non-debugging.
# È un'euristica e potrebbe aver bisogno di affinamenti.
# Prioritizziamo le linee che iniziano con numeri di riga o nomi di file.
potential_function_lines = []
current_file_context = None
# Flag per indicare se siamo nella sezione "Non-debugging symbols"
in_non_debugging_symbols_section = False
for line in output.splitlines():
line_strip = line.strip()
if not line_strip:
in_non_debugging_symbols_section = False # Una riga vuota potrebbe resettare la sezione
continue
# Cattura il contesto del file, se presente
file_match = re.match(r"File\s+(.+):", line_strip)
if file_match:
current_file_context = file_match.group(1).strip()
logger.debug(f"Function parsing context: File '{current_file_context}'")
continue # Passa alla riga successiva
# Ignora le sezioni "Non-debugging symbols" per ora, a meno che non si vogliano includere
if line_strip.startswith("Non-debugging symbols:") or \
(line_strip.startswith("0x") and " " in line_strip and not line_strip.endswith(";") and not line_strip.endswith(")")): # Heuristica per simboli non-debugging
logger.debug(f"Skipping non-debugging symbol line: {line_strip}")
current_file_context = None # Resetta contesto file se entriamo in questa sezione
if line_strip.startswith("All defined functions"): # Ignora questa intestazione comune
continue
if line_strip.startswith("File "): # Resetta contesto non-debug se incontriamo un nuovo file
in_non_debugging_symbols_section = False
file_match = re.match(r"File\s+(.+):", line_strip)
if file_match:
current_file_context = file_match.group(1).strip()
logger.debug(f"Function parsing context: File '{current_file_context}'")
continue
# Regex per estrarre il nome della funzione da righe tipo "123: void MyNamespace::MyClass<Template>::Method(int);"
# o "ReturnType funcName(params)"
# Questo cerca qualcosa che assomigli a un nome di funzione prima di una parentesi aperta
# o prima di un punto e virgola se non ci sono parametri visibili.
# Il nome può contenere '::', '<', '>', '_', alfanumerici.
# ^\s*(?:\w+\s+)?([a-zA-Z_][\w:<>()]*?(?:::[a-zA-Z_][\w:<>()]*?)*)\s*\(.*?\);?$
# ^\s*(?:[^\s]+\s+)*([a-zA-Z_][\w:<>()~*\s&-]*?(?:::[a-zA-Z_][\w:<>()~*\s&-]*?)*)\s*\(
# Il seguente è un tentativo più semplice e robusto
# Cerca un identificatore valido seguito da una parentesi aperta '(',
# opzionalmente preceduto da tipo di ritorno e numero di riga.
# Pattern per identificatore C++: ([a-zA-Z_]\w*(::[a-zA-Z_]\w*)*(<[^>]*>)?(?:\s*const)?)
# Regex per matchare: opzionale(numero_riga: tipo_ritorno) NOME_FUNZIONE (parametri) ;
# Difficile fare un regex perfetto. Proviamo un approccio più semplice per ora:
# cerca un nome valido seguito da '('.
if line_strip.startswith("Non-debugging symbols:"):
in_non_debugging_symbols_section = True
logger.debug("Entering Non-debugging symbols section.")
continue
# Tentativo 1: Estrarre da linee con numero di riga e tipo
# es: "123: void MyClass::myMethod(int);"
m = re.match(r"^\s*\d+:\s+(?:[\w\s:*&<>~]+\s+)?([a-zA-Z_][\w:<>\s~*&-]*?(?:::[a-zA-Z_][\w:<>\s~*&-]*?)*)\s*\(", line_strip)
if m:
func_name = m.group(1).strip()
# Rimuovi eventuali spazi extra o qualificatori 'const' alla fine se catturati per errore
func_name = re.sub(r'\s+const\s*$', '', func_name)
if func_name not in functions: # Evita duplicati se GDB è verboso
# Se siamo nella sezione non-debugging, i simboli sono spesso solo indirizzo e nome
if in_non_debugging_symbols_section:
# Esempio: 0x00401000 _start
m_non_debug = re.match(r"^\s*0x[0-9a-fA-F]+\s+([a-zA-Z_][\w:<>\.~]*)", line_strip)
if m_non_debug:
func_name = m_non_debug.group(1)
if func_name not in functions:
functions.append(func_name)
logger.debug(f"Found non-debugging symbol/function: {func_name}")
continue # Processa la prossima riga
# Pattern per simboli di debug (più strutturati)
# Tentativo 1: "numero_riga: [tipo_ritorno] nome_funzione(parametri);"
m_debug_line = re.match(r"^\s*\d+:\s+(?:[\w\s:*&<>~\[\]]+\s+)?([a-zA-Z_][\w:<>\s~*&\-\[\]]*?(?:::[a-zA-Z_][\w:<>\s~*&\-\[\]]*?)*)\s*\(", line_strip)
if m_debug_line:
func_name = m_debug_line.group(1).strip()
func_name = re.sub(r'\s+const\s*$', '', func_name).strip() # Rimuovi 'const' alla fine e spazi
if func_name and func_name not in functions:
functions.append(func_name)
logger.debug(f"Found function (type 1): {func_name}")
logger.debug(f"Found function (debug, type 1): {func_name}")
continue
# Tentativo 2: Linee che iniziano direttamente con il nome della funzione o tipo di ritorno
# es: "int main()" o "MyClass::MyClass()"
# ([a-zA-Z_][\w:<>\s~*&-]*?(?:::[a-zA-Z_][\w:<>\s~*&-]*?)*)\s*\(
m2 = re.match(r"^\s*(?:[\w\s:*&<>~]+\s+)?([a-zA-Z_][\w:<>\s~*&-]*?(?:::[a-zA-Z_][\w:<>\s~*&-]*?)*)\s*\(", line_strip)
if m2:
func_name = m2.group(1).strip()
func_name = re.sub(r'\s+const\s*$', '', func_name)
if func_name and func_name not in functions: # Assicurati che non sia vuoto
functions.append(func_name)
logger.debug(f"Found function (type 2): {func_name}")
# Tentativo 2: "[tipo_ritorno] nome_funzione(parametri)" (senza numero riga)
m_debug_no_line = re.match(r"^\s*(?:[\w\s:*&<>~\[\]]+\s+)?([a-zA-Z_][\w:<>\s~*&\-\[\]]*?(?:::[a-zA-Z_][\w:<>\s~*&\-\[\]]*?)*)\s*\(", line_strip)
if m_debug_no_line:
func_name = m_debug_no_line.group(1).strip()
func_name = re.sub(r'\s+const\s*$', '', func_name).strip()
if func_name and func_name not in functions:
# Evita di aggiungere tipi o parole chiave come funzioni
if not (func_name in ["void", "int", "char", "short", "long", "float", "double", "bool",
"class", "struct", "enum", "union", "typename", "template"] or func_name.endswith("operator")):
functions.append(func_name)
logger.debug(f"Found function (debug, type 2): {func_name}")
continue
# A volte 'info functions' lista solo il nome, specialmente per simboli senza info complete
# Es. "_ZN12MyNamespace8MyClassILi1EE11anotherFuncEv" (mangled)
# O simboli semplici come "main" su una riga a sé se non c'è file context
# Questo è più rischioso perché potrebbe catturare non-funzioni.
# Lo attiviamo solo se non c'è un contesto di file attivo (per evitare di prendere nomi di file come funzioni)
# e se la linea sembra un identificatore C++ valido.
if not current_file_context and re.match(r"^[a-zA-Z_][\w:<>\.~]*$", line_strip) and '(' not in line_strip and ';' not in line_strip:
# Evita di aggiungere cose che sono chiaramente tipi o altro
if "::" in line_strip or line_strip.startswith("_Z") or (not any(c.islower() for c in line_strip) and len(line_strip) > 4): # Heuristica per nomi mangled o solo maiuscole (costanti?)
if line_strip not in functions:
functions.append(line_strip)
logger.debug(f"Found potential symbol (type 3): {line_strip}")
if not functions and output: # Se non abbiamo trovato nulla con regex ma c'era output
logger.warning(f"Could not parse function names reliably from 'info functions' output. Output was:\n{output}")
elif functions:
if functions:
logger.info(f"Successfully parsed {len(functions)} function names.")
functions.sort() # Ordina per una visualizzazione migliore
functions.sort()
elif output: # C'era output ma non abbiamo parsato nulla
logger.warning(f"Could not parse any function names from 'info functions' output, though output was received. First 200 chars of output:\n{output[:200]}")
except (ConnectionError, TimeoutError) as e:
logger.error(f"Error listing functions from GDB: {e}", exc_info=True)
return [] # Ritorna lista vuota in caso di errore di comunicazione
return []
except Exception as e_parse:
logger.error(f"Error parsing 'info functions' output: {e_parse}", exc_info=True)
return [] # Ritorna lista vuota in caso di errore di parsing
return []
return functions

View File

@ -65,21 +65,22 @@ class ActionEditorWindow(tk.Toplevel):
def __init__(self, parent: tk.Widget,
action_data: Optional[Dict[str, Any]] = None,
is_new: bool = True,
target_executable_path: Optional[str] = None, # NUOVO PARAMETRO
app_settings: Optional[AppSettings] = None): # NUOVO PARAMETRO
target_executable_path: Optional[str] = None,
app_settings: Optional[AppSettings] = None,
symbol_analysis_data: Optional[Dict[str, Any]] = None): # NUOVO PARAMETRO
super().__init__(parent)
self.parent_window = parent
self.is_new_action = is_new
self.result: Optional[Dict[str, Any]] = None
# --- NUOVI ATTRIBUTI ---
self.target_executable_path = target_executable_path
self.app_settings = app_settings # Necessario per gdb_path, timeouts
# --- FINE NUOVI ATTRIBUTI ---
self.app_settings = app_settings
self.symbol_analysis_data = symbol_analysis_data # NUOVO ATTRIBUTO
# ... (resto dell'init come prima) ...
title = "Add New Action" if self.is_new_action else "Edit Action"
self.title(title)
self.geometry("650x580") # Aumentato leggermente per il nuovo bottone
self.geometry("650x580") # Potrebbe servire aggiustare
self.resizable(False, False)
self.transient(parent)
@ -256,80 +257,74 @@ class ActionEditorWindow(tk.Toplevel):
# --- NUOVA FUNZIONE ---
def _browse_functions(self) -> None:
if not self.target_executable_path or not os.path.isfile(self.target_executable_path):
messagebox.showerror("Error", "Target executable for the profile is not set or invalid. Cannot browse functions.", parent=self)
return
if not self.app_settings:
messagebox.showerror("Error", "Application settings not available. Cannot determine GDB path.", parent=self)
return
# --- LOGICA AGGIORNATA ---
functions_to_show: List[str] = []
source_of_functions = "live GDB query" # Default
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
if not gdb_exe_path or not os.path.isfile(gdb_exe_path):
messagebox.showerror("GDB Error", f"GDB executable not found or not configured: {gdb_exe_path}", parent=self)
return
# Mostra un messaggio di attesa
self.config(cursor="watch")
self.update_idletasks()
temp_gdb_session: Optional[GDBSession] = None
functions_list: List[str] = []
error_occurred = False
try:
logger.info(f"Creating temporary GDBSession for function listing. Target: {self.target_executable_path}")
# Creiamo una sessione GDBSession senza script dumper e senza opzioni dumper,
# serve solo per 'info functions'.
temp_gdb_session = GDBSession(
gdb_path=gdb_exe_path,
executable_path=self.target_executable_path,
gdb_script_full_path=None, # Non serve lo script dumper per info functions
dumper_options={}
)
# Usiamo timeout brevi per l'avvio e il comando
startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30) // 2 # Più breve
command_timeout = self.app_settings.get_setting("timeouts", "gdb_command", 30) // 2 # Più breve
logger.debug("Starting temporary GDB session...")
temp_gdb_session.start(timeout=startup_timeout) # Avvia GDB
if not temp_gdb_session.symbols_found:
messagebox.showwarning("No Debug Symbols",
"GDB reported no debugging symbols found in the executable. "
"The function list might be empty or incomplete.", parent=self)
# Continuiamo comunque, GDB potrebbe listare alcuni simboli non-debugging.
logger.debug("Listing functions...")
functions_list = temp_gdb_session.list_functions(timeout=command_timeout)
except FileNotFoundError as fnf_e:
logger.error(f"Error during function browsing (FileNotFound): {fnf_e}", exc_info=True)
messagebox.showerror("Error", f"File not found during GDB interaction: {fnf_e}", parent=self)
error_occurred = True
except (ConnectionError, TimeoutError) as session_e:
logger.error(f"Session error during function browsing: {session_e}", exc_info=True)
messagebox.showerror("GDB Error", f"Could not communicate with GDB to list functions: {session_e}", parent=self)
error_occurred = True
except Exception as e:
logger.error(f"Unexpected error during function browsing: {e}", exc_info=True)
messagebox.showerror("Error", f"An unexpected error occurred: {e}", parent=self)
error_occurred = True
finally:
if temp_gdb_session and temp_gdb_session.is_alive():
logger.debug("Quitting temporary GDB session...")
quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit", 10) // 2
try:
temp_gdb_session.quit(timeout=quit_timeout)
except Exception as e_quit:
logger.error(f"Error quitting temporary GDB session: {e_quit}")
self.config(cursor="") # Ripristina cursore
if not error_occurred:
if functions_list:
dialog = FunctionSelectorDialog(self, functions_list)
selected_function = dialog.result
if selected_function:
self.breakpoint_var.set(selected_function)
# 1. Prova a usare i dati di analisi dei simboli pre-calcolati, se validi
if self.symbol_analysis_data and isinstance(self.symbol_analysis_data, dict):
# Verifica che i dati di analisi siano per l'eseguibile corrente
# (confronto semplice del path, idealmente si usa checksum/timestamp in futuro)
analyzed_exe = self.symbol_analysis_data.get("analyzed_executable_path")
if analyzed_exe and os.path.normpath(analyzed_exe) == os.path.normpath(str(self.target_executable_path)): # Confronta path normalizzati
cached_functions = self.symbol_analysis_data.get("symbols", {}).get("functions", [])
if isinstance(cached_functions, list) and cached_functions: # Se abbiamo funzioni cachate
functions_to_show = cached_functions
source_of_functions = "cached analysis"
logger.info(f"Using {len(functions_to_show)} cached functions for browsing.")
else:
messagebox.showinfo("No Functions", "No functions found or GDB did not return any.", parent=self)
logger.info("Cached symbol analysis is for a different executable or path mismatch. Will perform live query if target is valid.")
if analyzed_exe: logger.debug(f"Analyzed exe: '{analyzed_exe}', Current target: '{self.target_executable_path}'")
# 2. Se non ci sono funzioni cachate valide, esegui una query live a GDB
if not functions_to_show:
if not self.target_executable_path or not os.path.isfile(self.target_executable_path):
messagebox.showerror("Error", "Target executable for the profile is not set or invalid. Cannot browse functions.", parent=self)
return
if not self.app_settings:
messagebox.showerror("Error", "Application settings not available. Cannot determine GDB path.", parent=self)
return
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
if not gdb_exe_path or not os.path.isfile(gdb_exe_path):
messagebox.showerror("GDB Error", f"GDB executable not found or not configured: {gdb_exe_path}", parent=self)
return
self.config(cursor="watch")
self.update_idletasks()
temp_gdb_session: Optional[GDBSession] = None
live_query_error = False
try:
logger.info(f"Performing live GDB query for functions. Target: {self.target_executable_path}")
temp_gdb_session = GDBSession(gdb_exe_path, self.target_executable_path, None, {})
startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30) // 2
command_timeout = self.app_settings.get_setting("timeouts", "gdb_command", 30) // 2
temp_gdb_session.start(timeout=startup_timeout)
if not temp_gdb_session.symbols_found:
messagebox.showwarning("No Debug Symbols",
"GDB reported no debugging symbols in the executable. "
"The function list (live query) might be empty or incomplete.", parent=self)
functions_to_show = temp_gdb_session.list_functions(timeout=command_timeout)
except Exception as e:
logger.error(f"Error during live function query: {e}", exc_info=True)
messagebox.showerror("GDB Query Error", f"Could not retrieve functions from GDB: {e}", parent=self)
live_query_error = True
finally:
if temp_gdb_session and temp_gdb_session.is_alive():
quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit", 10) // 2
try: temp_gdb_session.quit(timeout=quit_timeout)
except Exception: pass # Ignora errori nel quit della sessione temporanea
self.config(cursor="")
if live_query_error: return # Non mostrare la dialog se c'è stato un errore
# 3. Mostra la dialog con le funzioni (cachate o da query live)
if functions_to_show:
logger.info(f"Displaying FunctionSelectorDialog with {len(functions_to_show)} functions from '{source_of_functions}'.")
dialog = FunctionSelectorDialog(self, functions_to_show, title=f"Select Function (from {source_of_functions})")
selected_function = dialog.result
if selected_function:
self.breakpoint_var.set(selected_function)
else:
messagebox.showinfo("No Functions", f"No functions found (source: {source_of_functions}). Ensure target is compiled with debug symbols.", parent=self)

File diff suppressed because it is too large Load Diff