Assembly? Ma il linguaggio di iOS/Mac OS non è l’Objective-C? Si, certo, ma a volte conviene sfruttare qualcosa che va ancora più a basso livello per risolvere i nostri problemi, in questi casi parliamo dell’Assembly. In questi anni d’esperienza ho incontrato moltissimi sviluppatori, credo di averne conosciuti più di 200 (sviluppatori che vantano applicazioni come Delibar, Delicious Library, BBEdit, ecc…), ma pochissimi di loro (forse li riesco a contare sulle dita di una mano) mi hanno detto di aver sfruttato appieno le potenzialità di GDB (GNU Project Debugger). Ecco perchè ho deciso di scrivere questo piccolo quickstart, che non sarà altro che un punto di partenza nel caso in cui siate interessati ad approfondire la questione, ma veniamo al dunque.
Quando occorre scendere ad un livello più basso?
Ci sono volte in cui capita di scrivere metodi (magari noiosi) e poi ci si tritrova bloccati per un problema di memoria che non riusciamo a risolvere. Spesso si tratta di errori banali, altre volte di errori un po’ più complessi, altre volte ancora si è talmente sicuri del proprio codice che si arriva a pensare che sia Apple ad aver commesso un errore (mai commettere questo sbaglio :P).
In ognuno di questi casi, saper sfruttare GDB non è un male, anzi, ci si mette un po’ di tempo a prendere confidenza, ma una volta appreso il funzionamento, non si riesce più a farne a meno.
Per questa guida sfrutteremo un piccolo esempio pratico, di cui trovate il sorgente disponibile per il download a fondo articolo. Si tratta di un banalissimo programma per iPhone che mostra in una tabella gli URL di 10 technotes presenti sul sito Apple (che consiglio di leggere, male certamente non farà :D), prelevati da un semplicissimo array. Al tap su di una cella (row), verrà inviato un messaggio all’applicazione di aprire Safari Mobile aprendo l’URL associato. Insomma, un programmino davvero banale e di facile comprensione.
Vediamo il codice del nostro programma di esempio
Iniziamo a vedere il codice del nostro esempio partendo dal file di intestazione (.h) del nostro RootViewController:

Nel file di implementazione (.m) ho quindi istanziato l’array che contiene gli URL che andremo a mostrare e ad aprire direttamente in Safari:

(I più attenti molto probabilmente si saranno già accorti dell’errore, ma andiamo avanti ;))
Mentre i metodi che si occuperanno di avviare Safari sono definiti nel seguente modo:

Alcuni di voi si chiederanno perchè ho fatto tutta questa strada per aprire un semplice URL in Safari? La risposta è complessa, cercherò comunque di non complicare troppo le cose cercando si spegare in modo chiaro: in pratica ho bisogno di diverse iterazioni affinchè l’errore si verifichi nel simulatore. Infatti, il simulatore, dispone di tantissima memoria (quella libera nei nostri sistemi) evidentemente molto più rispetto a quella disponibile normalmente nei nostri iPhone.
Ok, il codice del nostro piccolo esempio è tutto qui. Facciamo partire la nostra applicazione, e noteremo che questa funzionerà benissimo… apparentemente!

Se premiamo su una qualsiasi riga della tabella, si aprirà Safari senza nessun problema. A questo punto non mi resta che provare a fare lo scroll e siamo a posto: metto il dito, trascino la mia tabella e magicamente… CRASH!
A primo impatto, penso che non sia possibile, il codice è giusto! Riavvio l’applicazione e riprovo! Risultato: stesso comportamento, quando scrollo l’applicazione questa va in crash, ma stavolta è avvenuto in un punto diverso rispetto al precedente.. brutto segnale. Mi rimbocco le maniche e mi preparo a cercare di capire dove sta il problema.

Come potete vedere dall’immagine qui sopra, il nostro caro GDB non da nessun errore se non un (fastidiosissimo) EXC_BAD_ACCESS.
Siccome io so che l’array è popolato, e lo capiamo dal fatto che i miei URL sono correttamente stampati nella tabella, provo a cercare di capire cosa non sta effettivamente funzionando e piazzo un bel breakpoint durante il runtime (subito dopo aver fatto partire l’applicazione), nella linea dove prima l’applicazione è andata in crash, quindi riprovo ad eseguire uno scroll. Faccio uno “StepIn” così da entrare nel metodo:
-(NSString *) getFileNameForIndex:(NSInteger)index
Una volta al suo interno ricevo di nuovo un segnale di errore, ma questa volta si tratta di un SIGABRT:
A questo punto decido quindi di stampare il codice Assembly generato ed ecco il risultato:

Nelle architetture ARM, le funzioni vengono gestite nel seguente modo:
- primi 4 parametri passati via registro (gli altri vanno nello stack e diventa più complessa la gestione) con i seguenti nomi: $r0, $r1, $r2, $r3
- l’indirizzo di ritorno viene passato dal registro: $lr
- il risultato viene passato nel registro: $eax
NOTA: ci sono delle eccezioni, piuttosto rare, ma ci sono 😉 ma tutto sommato, come linea guida, questa è più che sufficiente per i nostri scopi.
Sono curioso di capire cosa ritorna la mia funziona, per logica stampando con il comando ‘po $eax’ dovrei ottenere un oggetto stringa, invece ottengo il seguente messaggio:
Can’t print the description of a NIL object.
Che è in linea con quanto XCode mi segnala, ovvero un errore di memoria. Continuo a curiosare ed a questo punto ho una soluzione, spezzettare la funzione:
-(NSString *) getFileNameForIndex:(NSInteger)index
in modo tale da avere più pezzetti da controllare, via sicuramente più semplice, ma prima, per scaramanzia, stampo il contenuto della variabile dataArray, giusto per essere sicuri che sia tutto ok:
Come potete vedere.. non è tutto ok… dataArray dovrebbe essere un array, mentre in realtà, in questo caso, è un NSBundle con retainCount uguale a 4:
A questo punto è chiaro che il problema è proprio con la variabile dataArray, aggiungo un breakpoint quando creo il dataArray e faccio ripartire l’applicazione.
Mi fermo subito dopo l’init dell’array e so già che, visto il funzionamento iniziale della app, il suo contenuto fino a questo punto è corretto, ma deve esserci qualcosa che rilascia l’array, decido di stampare lo stack di autorelease:

Noto che al top dello stack (cioè appena uscirò dalla funzione, ‘viewDidLoad’ sarà rilasciato) c’è un NSArrayI, per curiosità stampo il suo contenuto:

ED ECCO IL NOSTRO ARRAY! E l’errore è veramente banale: ‘NSArray arrayWithObjects:’ crea un array autorelease che verrà rilasciato appena il pool sarà “drainato”, cioè all’uscita della funzione.
L’applicazione funziona correttamente subito dopo il lancio, perchè i registri non vengono puliti immediatamente e nel simulatore, con memoria di 2, 4, 6, 8 o più giga, non è sempre necessaria questa procedura, ecco perchè spesso ci sono problemi che sorgono solo sui device e non nei simulatori. Ecco quindi che il problema si verifica solo in seconda istanza.
Sostituisco il metodo con [[NSArray alloc] init…] e tutto funziona a meraviglia!
Se volete approfondire l’argomento, vi consiglio di leggere la nota tecnica TN2239 sul sito della Apple, mentre se avete già qualche base di GDB, allora una bella Reference Card per GDB sicuramente non guasta. 😉
Eccovi il sorgente del progetto di esempio utilizzato nella nostra guida: AssemblyTest










6 Responses to “GDB (GNU Project Debugger): Debug del linguaggio Assembly”
19 Aprile 2011
cikpisPotevi anche mettere un retain alla fine cioè:
dataArray = [[NSArray arrayWithObject:Object 1…..Object n-1, nil] retain];
19 Aprile 2011
gianBellissimo articolo,
anche io cerco di utilizzare al massimo il GDB anche se a volte è decisamente ostico…
Posso chiederti una cosa riguardo l’AutoreleasePool?! Nell’articolo scrivi che la funzione drain verrà richiamata appena uscirai dalla funzione “viewDidLoad”. E’ sistematico questo comporamento?!
Sapevo che la funzione drain veniva chiamata in modo arbitrario dal SO in base ad eventi casuali (come l’interazione dell’utente, etc. etc.).
Grazie mille e complimenti ancora per l’articolo..
G.
19 Aprile 2011
ignaziocgrande articolo! potessi leggerne ogni giorno di articoli così!! complimenti…devo smetterla di fare debug a forza di printf! 🙂
19 Aprile 2011
XfightOttimo articolo !
21 Aprile 2011
Junior B.é buona norma pensarlo… diciamo che non é esattamente così, il comportamento dell’autorelease pool iniziare é arbitrario, ecco perché l’applicazione funziona in prima istanza, dopo il lancio e non crasha subito… bisognerebbe anche tenere in considerazione i registri, ma vabbeh… diciamo che é buona norma pensare che dopo l’uscita di un loop o la fine di una funzione, l’autorelease pool rilascia tutti gli oggetti che devono essere rilasciati. Non esiste un modo di sapere quando, ma pensarla così aiuta molto, anche perché poi si giocherebbe con il fuoco 😉
21 Aprile 2011
Junior B.anche, ma non sono un gran fan di questa dicitura ed il motivo é che il codice diventa meno leggibile… se ho un [[NSArray alloc… so che poi dovrà per forza esserci un release da qualche parte, ma se scrivo un ‘retain’ dopo un ‘arrayWith…’, questo potrebbe anche scapparmi… é questione di gusto, ma preferisco tenere una certa ‘grammatica’, anche perché lavorando in team semplifica molto la vita quando si cercano i bug 😀