Perchè i simboli sono importanti?

Nel corso degli anni passati al Supporto Tecnico mi sono (purtroppo) accorto che un argomento (ed uno strumento) molto importante, quasi fondamentale per il debugging è in realtà quasi sconosciuto a molti sviluppatori che lo considerano qualcosa di accessorio ed a volte addirittura una scocciatura: sto parlando dei simboli (ed anche se sono un appassionato, in questo caso Robert Langdon non c’entra niente smile_nerd). Per questo motivo voglio partire subito con un’affermazione “forte”: il debugging con simboli sbagliati può essere molto peggio del debugging senza simboli!

Che cosa sono i simboli?

Sostanzialmente si può pensare ai simboli come a piccoli, particolari database, file che contengono informazioni circa sulle linee di codice sorgente, i tipi di dati utilizzati, le variabili, metodi e tutto quello che serve per fornire “nomi” per tutte le linee di codice dell’applicazione. Solitamente hanno estensione .pdb o .dbg, e devono fare match (devono corrispondere) con l’eseguibile tramite un timestamp interno creato al momento della compilazione; per questo motivo è molto importante generare i simboli ad ogni compilazione, anche per le build di release. Se mai avrete un problema con una vostra applicazione (o anche con una di terze parti) e dovrete farne il debugging, se non avete a disposizione i matching symbols (simboli ottenuti dalla stessa compilazione e build che avete in produzione) potreste trovarvi nei guai… Ricompilare nuovamente l’applicazione per ottenere i file dei simboli (anche senza modificare il codice sorgente) difficilmente vi aiuterà perchè in ogni caso il timestamp generato sarà differente, non farà match e Windbg o altri debugger vi segnaleranno la mancanza di simboli appropriati.

Advanced Compiler Options

Di default, in Visual Studio 2008 una compilazione con il flag di debug usa l’opzione “Full” per la generazione dei simboli, mentre passando alla compilazione con il flag di release viene usata la l’opzione “pdb-only”. Questo non si applica ai progetti web (ASP.NET) dal momento che dalla versione 2.0 di default non viene più creata la dll contenente il codice dell’applicazione; questo a meno che non si usi il template “Web Application Project” che sostanzialmente ripropone il modello di compilazione di ASP.NET 1.1 con la dll nella cartella /bin.

Come si usano i simboli?

Aprendo un dump con Windbg, l’esecuzione di ogni comando (ad esempio per visualizzare il callstack di un thread) fa si che venga cercato un simboli appropriati che faccia match con il codice eseguibile in esecuzione nel processo, per restituire un output il più dettagliato possibile: ma possiamo dire a Windbg dove andare a cercare i simboli? Verrà usato il percorso (che può contenere più cartelle/URL) specificato nella finestra di dialogo “Symbol Search Path” disponibile dal menu File (o premendo la combinazione di tasti CTRL+S).

Symbol Search Path

Qui è possibile specificare i server dei simboli dai quali Windbg può scaricare i simboli, ed è possibile usare più server contemporaneamente: verranno acceduti nello stesso ordine specificato in questa finestra di dialogo, uno dopo l’altro fino a quando il file corretto viene trovato (si spera smile_regular). Microsoft ha un server pubblico accessibile direttamente da Internet nel quale è possibile trovare i simboli per i nostri prodotti: l’indirizzo è http://msdl.microsoft.com/download/symbols.

Mi capita spesso di analizzare file di dump anche dal portatile senza la connessione ad Internet o accesso alla nostra rete interna, quindi per me è importante poter portare a termine il lavoro offline ed in ogni caso credo non sia molto conveniente sprecare tempo in attesa che il debugger scarichi gli stessi simboli ogni volta… Per questo motivo è possibile create un proprio symbol store locale (conosciuto anche come downstream store), una cartella nella quale salvare i simboli scaricati dai vari server e nella quale Windbg può cercare i simboli prima di accedere a risorse di rete (normalmente più lente e magari anche non disponibili).

Il symbol path è una stringa composta di percorsi multipli a cartelle e/o URL, separati dal simbolo dell’asterisco (“*”) o punto è virgola (“;”). Per ogni cartella inclusa nel symbol path il debugger cercherà 3 sottocartelle specifiche: ad esempio, supponendo di avere la cartella c:\mystore nel nostro percorso, e stiamo cercando il file dei simboli per una dll, il debugger cercherà prima la cartella c:\mystore\symbols\dll, poi in c:\mystore\dll ed infine in c:\mystore. Se non è possibile trovare il file ricercato allora cercherà nella cartella corrente e nella cartella corrente con il nome della dll “appeso” alla fine.

Ecco un esempio di symbol path:

SRV*c:\symbols*\\internalshare\symbols*http://msdl.microsoft.com/download/symbols

Questo a dire la verità è il symbol path (anche se un po’ semplificato) che uso normalmente sulle mie macchine: come potete vedere ho una cache locale in c:\symbols. Questa è la cartella nella quale il mio Windbg cerca come primo passo il file dei simboli che gli serve; se non lo trova, allora lo direziono verso un server interno nel quale teniamo tutti i nostri simboli privati, e nell’eventualità che per un problema il file non sia nemmeno li, cerco nel server pubblico su Internet. Se si includono due asterischi di seguito dove normalmente di inserirebbe un downstream store, verrà creata ed usata una cartella sym nella cartella d’installazione del debugger; questa cartella può essere modificata usando il comando !homedir. Se non si specifica un downstream store e non si usano doppi asterischi, allora i simboli verranno caricati direttamente dal server dei simboli e non ne verrà creata una copia locale. Se si accede ai simboli tramite HTTP (o HTTPS) o se il symbol store usa file compressi, allora il downstream store  verrà sempre usato; se non ne viene specificato uno allora si userà la cartella sym nella cartella d’installazione del debugger.

Può succedere che il debugger trovi un simbolo il cui nome sia quello atteso ma che non faccia match con l’eseguibile, e lo carichi comunque; in questo caso è importante capire se un simbolo fa match o meno (vedremo più avanti come).

Un’altra opzione è usare la variabile di sistema _NT_SYMBOL_PATH (la sintassi da usare è quella appena descritta per il symbol path) e viene utilizzata da debugger come Windbg o anche adplus quando si cattura un dump. Lo stesso principio si applica a Visual Studio (http://support.microsoft.com/kb/311503/en-us):

Debugging Options (Visual Studio)

Come verificare se sto usando i simboli corretti?

Guardando un callstack a volte è immediatamente chiaro se c’è un problema con i simboli perchè Windbg dice qualcosa simile a questo:

 ChildEBP RetAddr  
0012f6dc 7c59a2d1 NTDLL!NtDelayExecution(void)+0xb 
0012f6fc 7c59a29c KERNEL32!SleepEx(unsigned long dwMilliseconds = 0xfa, int bAlertable = 0)+0x32 
 *** ERROR: Symbol file could not be found. Defaulted to export symbols for aspnet_wp.exe -   
0012f708 00442f5f KERNEL32!Sleep(unsigned long dwMilliseconds = 0x444220)+0xb 
WARNING: Stack unwind information not available. Following frames may be wrong. 
0012ff60 00444220 aspnet_wp+0x2f5f 
0012ffc0 7c5989a5 aspnet_wp!PMGetStartTimeStamp+0x676 
0012fff0 00000000 KERNEL32!BaseProcessStart(<function> * lpStartAddress = 0x004440dd)+0x3d

Sfortunatamente non sempre si è così fortunati e potreste ritrovarvi a chiedervi se lo stack che state guardando è valido o se ci siano delle imprecisioni che vi possono portare su una strada completamente sbagliata. Per prima cosa è possibile usare il comando lml per sapere quali simboli sono stati effettivamente caricati:

 0:000>; lml
start    end        module name
01000000 01004000   w3wp       (pdb symbols)          C:\debuggers_public\sym\w3wp.pdb\3E8000E61\w3wp.pdb
5a360000 5a36c000   w3dt       (pdb symbols)          C:\debuggers_public\sym\w3dt.pdb\3E8017722\w3dt.pdb
5a390000 5a3e5000   w3core     (pdb symbols)          C:\debuggers_public\sym\w3core.pdb\3E8010742\w3core.pdb
77e40000 77f34000   kernel32   (pdb symbols)          C:\debuggers_public\sym\kernel32.pdb\3E8016FF2\kernel32.pdb
77f40000 77ffa000   ntdll      (pdb symbols)          C:\debuggers_public\sym\ntdll.pdb\3E800DDD2\ntdll.pdb

Con il comand lme possiamo vedere i simboli problematici, ad esempio:

 ntdll M (pdb symbols) .sympath SRV\ntdll.pdb\36515FB5D04345E491F672FA2E2878C02\ntdll.pdb

La “M” evidenziata sta per “mismatch” ed indica un problema con quel modulo in particolare; possono essere usati altri simboli, con significati differenti: vi consiglio di cercare l’argomento “Symbol Status Abbreviations” nell’help di Windbg per maggiori dettagli.

In alternativa è possibile utilizzare il comando !sym noisy ed il comando .reload per ricaricare i simboli in modalità “verbosa” e fare in modo che Windbg scriva un output dettagliato del caricamento (o degli eventuali problemi incontraci) per ogni file dei simboli. Sempre nell’help di Windbg vi consiglio di leggere anche l’argomento “Symbols files and paths - overview”.

Trucco: se avete i simboli corretti ma Windbg non li carica…

A volte (non ho ben capito esattamente perchè) capita che Windbg non riesca a caricare il file dei simboli corretto anche se è già presente nella nostra cache locale: in questo caso provate a cancellare la cartella del simbolo che vi da problemi e forzatene nuovamente il download con il comando .reload /f <module_name> . Solitamente questo è sufficiente per risolvere il problema.

Debuggare senza simboli?

Non è impossibile, ma è sicuramente più complicato e richiede molto più tempo; la differenza principale sta nel fatto che il debugger non sarà in grado di visualizzare i nomi dei metodi, delle variabili ecc…, in generale lo stack sarà meno leggibile. Per darvi un esempio veloce, vi riporto una parte dello stack di un thread (lo stesso thread preso dallo stesso dump), è parte di una semplice applicazione d’esempio con un bottone che quando cliccato scrive a video la data e ora correnti.

senza simboli:

 5 Id: 10d4.1204 Suspend: 1 Teb: 7ffd7000 Unfrozen 
ldEBP RetAddr  
NING: Frame IP not in any known module. Following frames may be wrong. 
9f524 71a6b7f8 0x7c90eb94 
9fa0c 03490657 0x71a6b7f8 
WARNING: Unable to verify checksum for System.dll 
ERROR: Module load completed but symbols could not be loaded for System.dll 
9fa40 7a603543 CLRStub[StubLinkStub]@3490657(<Win32 error 318>) 
a8240 032908ff System!System.Net.Sockets.Socket.Accept(<HRESULT 0x80004001>)+0xc7 
ERROR: Module load completed but symbols could not be loaded for WebDev.WebHost.dll 
9fab0 7940a67a WebDev_WebHost!Microsoft.VisualStudio.WebHost.Server.OnStart(<HRESULT 0x80004001>)+0x27 
WARNING: Unable to verify checksum for mscorlib.dll 
ERROR: Module load completed but symbols could not be loaded for mscorlib.dll 
bd1b4 7937d2bd mscorlib!System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(<HRESULT 0x80004001>)+0x1a 
bd1b4 7940a7d8 mscorlib!System.Threading.ExecutionContext.Run(<HRESULT 0x80004001>)+0x81 
9fae0 7940a75c mscorlib!System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(<HRESULT 0x80004001>)+0x44 
32010 79e79dd3 mscorlib!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(<HRESULT 0x80004001>)+0x60 
9fb04 79e79d57 0x79e79dd3 
9fb84 79f71cba 0x79e79d57 
9fba4 79f71c64 0x79f71cba 
9fc08 79f71cf3 0x79f71c64 
9fc3c 7a0b0896 0x79f71cf3 
9fc9c 79f7ba4f 0x7a0b0896 
9fcb0 79f7b9eb 0x79f7ba4f 
9fd44 79f7b90c 0x79f7b9eb 
9fd80 79ef9887 0x79f7b90c 
9fda8 79ef985e 0x79ef9887 
9fdc0 7a0a32da 0x79ef985e 
9fe28 79ef938f 0x7a0a32da 
9fe94 79f7be67 0x79ef938f 
9ffb4 7c80b683 0x79f7be67 
9ffec 00000000 0x7c80b683

con i simboli corretti:

 5 Id: 10d4.1204 Suspend: 1 Teb: 7ffd7000 Unfrozen 
ldEBP RetAddr  
9f4e4 7c90e9c0 ntdll!KiFastSystemCallRet 
9f4e8 71a54033 ntdll!ZwWaitForSingleObject+0xc 
9f524 71a6b7f8 mswsock!SockWaitForSingleObject+0x1a0 
9f9bc 71ac0e2e mswsock!WSPAccept+0x21f 
9f9f0 71ac103f ws2_32!WSAAccept+0x85 
9fa0c 03490657 ws2_32!accept+0x17 
WARNING: Unable to verify checksum for System.ni.dll 
9fa40 7a603543 CLRStub[StubLinkStub]@3490657(<Win32 error 318>) 
a8240 032908ff System_ni!System.Net.Sockets.Socket.Accept(<HRESULT 0x80004001>)+0xc7 
9fab0 7940a67a WebDev_WebHost!Microsoft.VisualStudio.WebHost.Server.OnStart(<HRESULT 0x80004001>)+0x27 
WARNING: Unable to verify checksum for mscorlib.ni.dll 
bd1b4 7937d2bd mscorlib_ni!System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(<HRESULT 0x80004001>)+0x1a 
bd1b4 7940a7d8 mscorlib_ni!System.Threading.ExecutionContext.Run(<HRESULT 0x80004001>)+0x81 
9fae0 7940a75c mscorlib_ni!System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(<HRESULT 0x80004001>)+0x44 
32010 79e79dd3 mscorlib_ni!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(<HRESULT 0x80004001>)+0x60 
9fb04 79e79d57 mscorwks!CallDescrWorker+0x33 
9fb84 79f71cba mscorwks!CallDescrWorkerWithHandler+0xa3 
9fba4 79f71c64 mscorwks!DispatchCallBody+0x1e 
9fc08 79f71cf3 mscorwks!DispatchCallDebuggerWrapper+0x3d 
9fc3c 7a0b0896 mscorwks!DispatchCallNoEH+0x51 
9fc9c 79f7ba4f mscorwks!QueueUserWorkItemManagedCallback+0x6c 
9fcb0 79f7b9eb mscorwks!Thread::DoADCallBack+0x32a 
9fd44 79f7b90c mscorwks!Thread::ShouldChangeAbortToUnload+0xe3 
9fd80 79ef9887 mscorwks!Thread::ShouldChangeAbortToUnload+0x30a 
9fda8 79ef985e mscorwks!Thread::ShouldChangeAbortToUnload+0x33e 
9fdc0 7a0a32da mscorwks!ManagedThreadBase::ThreadPool+0x13 
9fe28 79ef938f mscorwks!ManagedPerAppDomainTPCount::DispatchWorkItem+0xdb 
9fe3c 79ef926b mscorwks!ThreadpoolMgr::ExecuteWorkRequest+0xaf 
9fe94 79f7be67 mscorwks!ThreadpoolMgr::WorkerThreadStart+0x223 
9ffb4 7c80b683 mscorwks!Thread::intermediateThreadProc+0x49 
9ffec 00000000 kernel32!BaseThreadStart+0x37

Credo la differenza sia piuttosto ovvia.

Ho avuto i simboli dal mio cliente: ora che faccio?

La cosa più semplice è usare symstore.exe che trovate nella cartella d’installazione dei Debugging Tools, un comando simile a questo:

symstore add /f c:\temp\SymbolTest\Bin\*.pdb /s c:\symbols /t “Symbol Test”

Diamo un’occhiata alla sintassi:

  • “add” credo si commenti da solo smile_wink
  • “/f” indica quale file si vuole aggiungere al proprio store locale: si possono usare caratteri jolly, è quindi possibile aggiungere più file con un solo comando
  • “/s” è il percorso al nostro symbol store locale (ma potrebbe anche essere il symbol server aziendale)
  • “/t” è una descrizione (richiesta) per il file dei simboli

Symstore crea una struttura simile a quella che vedete qui sotto:

SymStore

Da notare la cartella “000Admin” che contiene un file per ogni transazione (ogni operazione “add” o “delete”), oltre al file server.txt che contiene una lista di tutte le transazioni attualmente sul server, mentre history.txt contiene una lista di tutte le transazioni eseguite sulla macchina. Per maggiori informazioni vi rimando all’argomento “Using SymStore” nell’help di Windbg.

Conclusione

Anche se è possibile debuggare senza simboli (questo è specialmente vero per il codice .NET) ricordatevi che la vostra vita potrebbe essere molto più semplice se voi (ed i vostri clienti) avrete buona cura dei vostri file dei simboli e li genererete ogni volta che l’applicazione verrà ricompilata, anche in modalità di release.

Ho volutamente semplificato l’argomento, ho semplicemente voluto condividere quello che ho imparato nel mio lavoro di tutti i giorni e darvi alcune dritte per poter cominciare; vi consiglio comunque di approfondire la questione leggendo l’argomento “Symbols” nell’help di Windbg o fate una ricerca in Internet, non è difficile provare buoni post/articoli al riguardo smile_nerd

 

Carlo Cardella

Senior Support Engineer

EMEA IIS and Web Developer Support