Divertirsi con i puntatori a funzioni in C

Questo post nasce da quattro chiacchiere scambiate con un buon amico. Per qualche motivo la discussione deve essere scivolata sui valori restituiti dalle funzioni e sulle stravaganze del codice scritto in C.

Qualcosa mi è rimasto in mente, una domanda che aveva una risposta istintiva ma che non avrei saputo completamente. Come risultato mi son trovato a scrivere un pò di codice.

La domanda: Una funzione di tipo void può restituire un valore?

La risposta è chiaramente sì.

Tutto il codice seguente non è “buon codice” dato che dà per scontata l’architettura x86 e quindi puntatori ed interi a 32 bit. I risultati possono essere simili con qualche modifica al codice per l’architetture x64 ma anche tremendamente diversi usando, ad esempio, l’architettura SPARC V9.

Iniziamo con il codice di una funzione estremamente banale:

void mock_function() {
   printf("Hi there!\n");
   return 10;
}

Questa funzione viene accettata da un compilatore, ma genererà sicuramente un warning, ad esempio in gcc il warning sarà del tipo warning: ‘return’ with a value, in function returning void

Supponiamo di avere una variabile int i. La seguente riga di codice non incontrerà il favore del compilatore generando, anzichè un warning, un errore e impedendo la compilazione.

i = mock_function();

Il che è ragionevole, ma poco interessante. Per fortuna, grazie ai puntatori, il problema è risolto molto velocemente.

void *(*function_pointer)();
function_pointer = mock_function;

A questo punto il compilatore accetta il seguente codice, probabilmente con due warning: assegnazione ad un intero di un puntatore senza cast e assegnazione tra puntatori non compatibili.

i = function_pointer();

Vediamo un programma d’esempio:

#include "stdio.h"
void mock_function();
int main (int argc, char *argv[]) {
   int i;
   void *(*function_pointer)();
   function_pointer = mock_function;
   i = function_pointer();
   printf("%d\n",i);
   return 0;
}
void mock_function() {
   printf("Hi there!\n");
   return 10;
}

A parte alcuni warning, il programma viene compilato senza errori in un eseguibile.

Supponendo di averlo compilato su una macchina basata su architettura x86 sono ragionevolmente convinto che il programma stamperà a schermo il risultato 10.

Naturalmente c’è il trucco. Il seguente frammento di codice nella funzione non serve assolutamente a restituire il valore 10.

return 10;

Scrivendo return 9; la funzione continuerebbe a restituire 10. Anche scrivendo return;

Perchè?

Tutto dipende dalla convenzione di chiamata. Quando viene richiamata una funzione deve esistere un modo ben definito per passare alla funzione stessa gli argomenti e per ricevere dalla funzione il valore che essa ritorna.

Esistono diverse convenzioni di chiamata ma nell’architettura x86 generalmente il valore restituito da una funzione si trova in un registro, per la precisione il registro EAX.

Si tratta certamente di una semplificazione esagerata ma per il codice di esempio è sufficiente.

La funzione mock_function() è di tipo void, quindi non restituisce nulla. i = mock_function(); non funziona perchè il compilatore ragionevolmente rifiuta di generare un binario in cui il contenuto del registro EAX venga spostato nella variabile i per poi essere utilizzato.

Ma dato che i puntatori sono tutti uguali, è possibile assegnare l’indirizzo di mock_function() ad un puntatore di tipo void *(*function_pointer)() ingannando il compilatore. Richiamando function_pointer() il compilatore si aspetta che la funzione ritorni un puntatore a void, quindi i = function_pointer() genera un binario in cui il contenuto del registro EAX viene spostato nella variabile i. Naturalmente, nell’architettura x86, sia gli int che i puntatori sono a 32 bit, quindi è possibile passare da intero a puntatore e viceversa ma non è necessariamente così in altre architetture.

Come è possibile far assumere un valore arbitario al registro EAX?

Si potrebbe usare dell’assembly inline, ad esempio il compilatore di Microsoft Visual C++ 2010 Express accetta il seguente codice

__asm mov eax, 10;

mentre gcc gradisce il seguente

asm ("movl $10, %eax");

Una piccola nota per chi fosse curioso: il primo spezzone di codice è scritto in sintassi Intel, mentre il secondo è scritto seguendo la sintassi AT&T.

La funzione modificata diventerebbe

void mock_function() {
   printf("Hi there!\n");
   asm ("movl $10, %eax");
   return;
}

Tuttavia, il codice originario restituise 10 senza utilizzare assembly inline. Qualcuno potrebbe chiedersi perchè ma la risposta è molto semplice: la funzione printf() manipola il registro EAX dato che restituisce il numero di caratteri scritti.

La stringa “Hi there!\n” è esattamente di 10 caratteri.

A.C.

This entry was posted in C, Programmazione, Solaris 10, Windows. Bookmark the permalink.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *