Claudio Brotto

Notazione curiosa nella C-Runtime Library

Sono capitato più o meno casualmente di fronte alla funzione check_managed_app  della libreria run-time C, deputata a verificare se l'immagine eseguibile caricata sia o meno un'applicazione managed valida.

La funzione è definita come:

static int __cdecl check_managed_app(void) {...}

e il suo valore di ritorno viene memorizzato in una variabile locale a mainCRTStartup. Tale variabile (managedapp) è utilizzata nel prosequio della funzione in un paio di occasioni, tramite codice come quello che segue:

if (!managedapp) {...}

Il codice "incriminato" è il seguente (file crt0.c, riga 399):

return !! pNTHeader32->
          DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].
          VirtualAddress;

Questo snippet di codice verifica che il campo VirtualAddress della struttura _IMAGE_DATA_DIRECTORY a cui si riferisce pNTHeader32 ->DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR] sia diverso da zero. Se il valore è diverso da zero, l'applicazione è managed.

A parte il metodo utilizzato per la verifica, quello che mi ha colpito è la notazione "!!".

Perchè utilizzare un doppio not ? Dal momento che uno degli assiomi dell'algebra booleana recita (più o meno): "not (not (a)) = a"  ?

La spiegazione che mi dò è la seguente.

In C è considerato true qualsiasi valore numerico diverso da zero.

Ecco perchè, per inciso, è perfettamente lecito, e piuttosto frequente, trovare codice come:

if (! pPuntatoreAQualcosa) {
  
// Non provo nemmeno a dereferenziare un puntatore nullo !  
  // ...
else {
  
// ... ammesso, e non concesso, che un puntatore non nullo sia effettivamente valido ! 
}

Ad un primo esame, mi sembrava più naturale scrivere semplicemente:

return pNTHeader32 ->
       DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].
       VirtualAddress;

eliminando la doppia negazione.

Ma andando ad esaminare il codice macchina generato dal compilatore, la differenza risulta evidente.

[Disclaimer.: Ho dovuto rispolverare le mie già scarse conoscenze di assembler, spero di non aver preso qualche abbaglio !]

Senza la doppia negazione, l'output del compilatore sarebbe stato (ho fatto qualche test con una struttura fatta a mano, più semplice ma funzionalmente analoga):

mov eax,dword ptr [pNTHeader32] ; carica nel registro eax l'indirizzo di memoria a cui 
                                
; punta pNTHeader32
mov eax,dword ptr [eax+0D0h]    ; carica 
in eax il valore contenuto nel campo richiesto, 
                                ; calcolato come offset rispetto all
'indirizzo base della struttura

Poichè nella convenzione di chiamata utilizzata (cdecl) il valore di ritorno di una funzione è passato al chiamante nel registro EAX, tutto chiò che serve è copiare in tale registro il valore del campo richiesto (pNTHeader32 ->DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress ).

E'ovvio che, se il campo contiene il valore 123, tale sarà il valore di ritorno della funzione. Nelle istruzioni decisionali, il valore è considerato  true, e la cosa funziona.

Con la doppia negazione, invece, il compilatore genera il seguente codice macchina:

mov         eax,dword ptr [pNTHeader32]
xor         ecx,ecx 
cmp         dword ptr [eax+0D0h],0 
setne       cl
   
mov         eax,ecx

In pratica, il valore del campo interessato viene confrontato con la costante 0, e il registro EAX (per via indiretta) viene settato a 1 se il confronto fallisce, cioè se pNTHeader32 ->DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress è diverso da zero .

Il codice:

return (pNTHeader32 ->
       DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].
       VirtualAddress != 0);

genera, infatti, lo stesso codice x86.

Il contro della soluzione scelta credo siala maggiore occupazione di spazio da parte del codice e la leggera pessimizzazione. Ma si parla di pochi byte e di nanosecondi in una funzione che viene eseguita una volta sola, all'avvio dell'applicazione !!!

Il pro, a mio parere, è una maggiore "debuggabilità" della funzione, che deriva dall'aver forzato i valori true ad 1, cosa non espressamente richiesta dal linguaggio C.

Se volete la mia opinione, fatta salva la scelta di forzare i valori true a 1, sarebbe stato più leggibile un codice che avesse effettuato il confronto esplicito con zero (come quello scritto sopra), dato anche che l'assembler generato risulta perfettamente identico.

O forse è semplicemente una convenzione sintattica che non conoscevo per niente, e mi risulta meno naturale da leggere, oltre ad avermi fatto passare mezza mattinata ad indagarne il funzionamento !

powered by IMHO 1.2

Posted: mar 19 2005, 12.55 by devlizard | with 3 comment(s)
Filed under:

Comments

devlizard said:

Interessanti i commenti a questo post di Omer van Kloeten:
http://weblogs.asp.net/okloeten/archive/2004/04/14/113009.aspx
# marzo 20, 2005 8.31

TrackBack said:

# marzo 20, 2005 4.59

Claudio Brotto said:

Riporto qui il link che Adrian Florea ha segnalato come commento al mio post di ieri , che riguardava

# gennaio 27, 2007 11.29