Skip to content

Qual è l'indirizzo di una funzione in un programma C++?

Non devi più cercare altri siti poiché sei arrivato nel posto giusto, abbiamo la soluzione che stai cercando ma senza complicarla.

Soluzione:

Quindi, in questo caso l'indirizzo della funzione e l'indirizzo della prima variabile della funzione non sono gli stessi. Perché?

Perché è così? Un puntatore a funzione è un puntatore che punta alla funzione. Non punta comunque alla prima variabile all'interno della funzione.

Per approfondire, una funzione (o una subroutine) è un insieme di istruzioni (tra cui la definizione di una variabile e diverse istruzioni/operazioni) che esegue un lavoro specifico, per lo più più più volte, come richiesto. Non si tratta solo di un puntatore al file elementi presenti all'interno della funzione.

Le variabili definite all'interno della funzione non sono memorizzate nella stessa area di memoria del codice macchina eseguibile. In base al tipo di memoria, le variabili che sono presenti all'interno della funzione si trovano in un'altra parte della memoria del programma in esecuzione.

Quando un programma viene costruito (compilato in un file oggetto), le diverse parti del programma vengono organizzate in modo diverso.

  • Di solito, la funzione (codice eseguibile) risiede in un segmento separato chiamato segmento di codice, di solito una posizione di memoria di sola lettura.

  • Il tempo di compilazione allocato sono memorizzate nel segmento dati.

  • Le variabili locali delle funzioni, di solito, vengono popolate nella memoria dello stack, quando e se necessario.

Quindi, non esiste una relazione tale per cui un puntatore a una funzione restituisca l'indirizzo della prima variabile presente nella funzione, come si vede nel codice sorgente.

A questo proposito, per citare l'articolo di wiki,

Invece di riferirsi a valori di dati, un puntatore a funzione punta al codice eseguibile all'interno della memoria.

Quindi, in parole povere, l'indirizzo di una funzione è una posizione di memoria all'interno del segmento di codice (testo) in cui risiedono le istruzioni eseguibili.

L'indirizzo di una funzione è solo un modo simbolico per distribuire questa funzione, come passarla in una chiamata o simili. Potenzialmente, il valore ottenuto per l'indirizzo di una funzione non è nemmeno un puntatore alla memoria.

Gli indirizzi delle funzioni servono esattamente a due cose:

  1. per confrontare l'uguaglianza p==qe

  2. per dereferenziare e chiamare (*p)()

Qualsiasi altra cosa si tenti di fare non è definita, potrebbe funzionare o meno, ed è una decisione del compilatore.

Bene, questo sarà divertente. Passiamo dal concetto estremamente astratto di cosa sia un puntatore a funzione in C++ fino al livello del codice assembly e, grazie ad alcune particolari confusioni che stiamo avendo, arriviamo anche a discutere di stack!

Cominciamo dal lato altamente astratto, perché è chiaramente il lato delle cose da cui si parte. abbiamo una funzione char** fun() con cui si sta giocando. Ora, a questo livello di astrazione, possiamo vedere quali operazioni sono consentite sui puntatori di funzione:

  • Possiamo verificare se due puntatori a funzione sono uguali. Due puntatori a funzione sono uguali se puntano alla stessa funzione.
  • Possiamo eseguire test di disuguaglianza su tali puntatori, consentendoci di eseguire l'ordinamento di tali puntatori.
  • È possibile differenziare un puntatore a una funzione, il che si traduce in un tipo "function" che è davvero confuso da usare e che per ora sceglierò di ignorare.
  • Possiamo "chiamare" un puntatore a funzione, usando la notazione che avete usato: fun_ptr(). Il significato di questa operazione è identico a quello di chiamare la funzione a cui si punta.

Questo è tutto ciò che fanno a livello astratto. Al di sotto di questo, i compilatori sono liberi di implementarlo come meglio credono. Se un compilatore volesse avere una funzione FunctionPtrType che è in realtà un indice in una grande tabella di ogni funzione del programma, potrebbe farlo.

Tuttavia, in genere non è così che viene implementato. Quando si compila il C++ in codice assembly/macchina, si tende a sfruttare il maggior numero possibile di trucchi specifici per l'architettura, per risparmiare tempo di esecuzione. Nei computer reali, c'è quasi sempre un'operazione di "salto indiretto", che legge una variabile (di solito un registro) e salta per iniziare l'esecuzione del codice memorizzato a quell'indirizzo di memoria. È quasi universale che le funzioni siano compilate in blocchi contigui di istruzioni, quindi se si salta alla prima istruzione del blocco, si ha l'effetto logico di chiamare quella funzione. Si dà il caso che l'indirizzo della prima istruzione soddisfi tutti i confronti richiesti dal concetto astratto di puntatore a funzione del C++ e si dà il caso che sia esattamente il valore di cui l'hardware ha bisogno per usare un salto indiretto per chiamare la funzione! È così comodo che praticamente tutti i compilatori scelgono di implementarlo in questo modo!

Tuttavia, quando iniziamo a parlare del motivo per cui il puntatore che si pensava di guardare era lo stesso puntatore alla funzione, dobbiamo entrare in qualcosa di un po' più sfumato: i segmenti.

Le variabili statiche sono memorizzate separatamente dal codice. Ci sono alcune ragioni per questo. Uno è che si vuole che il codice sia il più compatto possibile. Non si vuole che il codice sia costellato di spazi di memoria per memorizzare le variabili. Sarebbe inefficiente. Si dovrebbe saltare ogni sorta di cose, invece di poterle semplicemente attraversare. C'è anche una ragione più moderna: la maggior parte dei computer consente di contrassegnare alcune memorie come "eseguibili" e altre come "scrivibili". Questo aiuta enormemente per affrontare alcuni trucchi hacker davvero malvagi. Cerchiamo di non contrassegnare mai qualcosa come eseguibile e scrivibile allo stesso tempo, nel caso in cui un hacker trovi un modo intelligente per ingannare il nostro programma e sovrascrivere alcune delle nostre funzioni con le proprie!

Di conseguenza, di solito c'è un elemento .code (usando la notazione tratteggiata semplicemente perché è un modo popolare di annotarlo in molte architetture). In questo segmento si trova tutto il codice. I dati statici andranno in un posto come .bss. Quindi, la stringa statica può essere memorizzata a una certa distanza dal codice che opera su di essa (in genere ad almeno 4kb di distanza, perché la maggior parte dell'hardware moderno consente di impostare i permessi di esecuzione o scrittura a livello di pagina: le pagine sono di 4kb in molti sistemi moderni).

Ora l'ultimo pezzo... lo stack. Avete accennato alla memorizzazione di cose sullo stack in modo confuso, il che suggerisce che potrebbe essere utile dare una rapida ripassata. Vorrei fare una rapida funzione ricorsiva, perché è più efficace per dimostrare cosa succede nello stack.

int fib(int x) {
    if (x == 0)
        return 0;

    if (x == 1)
        return 1;

    return fib(x-1)+fib(x-2);
}

Questa funzione calcola la sequenza di Fibonacci utilizzando un metodo piuttosto inefficiente ma chiaro.

Abbiamo una funzione, fib. Ciò significa che &fib è sempre un puntatore allo stesso posto, ma è chiaro che stiamo chiamando fib molte volte, quindi ognuna ha bisogno del proprio spazio, giusto?

Nello stack ci sono i cosiddetti "frame". I frame sono non le funzioni stesse, ma piuttosto sono sezioni di memoria che questa particolare invocazione della funzione è autorizzata a utilizzare. Ogni volta che si chiama una funzione, come fibsi alloca un po' di spazio in più sullo stack per il suo frame (o, più pedantemente, lo si alloca dopo la chiamata).

Nel nostro caso, fib(x) ha chiaramente bisogno di memorizzare il risultato di fib(x-1) mentre si esegue fib(x-2). Non può memorizzarlo nella funzione stessa, e nemmeno nella cartella .bss perché non sappiamo quante volte verrà ricorretta. Invece, alloca spazio sullo stack per memorizzare la propria copia del risultato di fib(x-1) mentre fib(x-2) sta operando nel proprio frame (utilizzando la stessa funzione e lo stesso indirizzo di funzione). Quando fib(x-2) ritorna, fib(x) carica semplicemente il vecchio valore, che è certo non sia stato toccato da nessun altro, aggiunge i risultati e li restituisce!

Come fa a fare questo? Praticamente ogni processore ha un supporto hardware per lo stack. Su x86, questo è noto come registro ESP (extended-stack pointer). I programmi generalmente accettano di trattare questo registro come un puntatore al punto successivo dello stack in cui è possibile iniziare a memorizzare i dati. È possibile spostare questo puntatore per creare spazio per un frame e poi procedere. Quando si termina l'esecuzione, ci si aspetta che si sposti tutto indietro.

In effetti, sulla maggior parte delle piattaforme, la prima istruzione della vostra funzione è non la prima istruzione nella versione finale compilata. I compilatori iniettano alcune operazioni extra per gestire questo puntatore allo stack, in modo da non doversene preoccupare. Su alcune piattaforme, come x86_64, questo comportamento è spesso obbligatorio e specificato nell'ABI!

Quindi in tutto abbiamo:

  • .code segmento - dove sono memorizzate le istruzioni della funzione. Il puntatore della funzione punterà alla prima istruzione presente in questo segmento. Questo segmento è tipicamente contrassegnato come "execute/read only", impedendo al programma di scriverci sopra dopo che è stato caricato.
  • .bss segmento - dove vengono memorizzati i dati statici, perché non possono far parte del segmento "solo esecuzione". .code se vuole essere un dato.
  • lo stack - dove le funzioni possono memorizzare i frame, che tengono traccia dei dati necessari solo per quell'istante e niente di più. (La maggior parte delle piattaforme lo usa anche per memorizzare le informazioni su dove restituire i file a al termine di una funzione)
  • l'heap - Non è stato inserito in questa risposta, perché la domanda non include alcuna attività sull'heap. Tuttavia, per completezza, l'ho lasciato qui in modo che non vi sorprenda in seguito.

Se scorri trovi le recensioni di altri project manager, hai anche la possibilità di mostrare la tua se lo desideri.



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.