Perché l’indirizzo virtuale del punto di ingresso per l’esecuzione ELF del formato 0x80xxxxx e non zero 0x0?

Quando viene eseguito, il programma inizierà a essere eseguito dall’indirizzo virtuale 0x80482c0. Questo indirizzo non punta alla nostra procedura main() , ma a una procedura chiamata _start che viene creata dal linker.

La mia ricerca su Google finora mi ha portato ad alcune speculazioni storiche (vaghe) come questa:

C’è folclore che 0x08048000 una volta era STACK_TOP (cioè, lo stack crebbe verso il basso da vicino a 0x08048000 verso 0) su una porta di * NIX a i386 che fu promulgato da un gruppo di Santa Cruz, in California. Questo era quando 128 MB di RAM erano costosi e 4 GB di RAM erano impensabili.

Qualcuno può confermare / negare questo?

    Come ha sottolineato Mads, al fine di catturare la maggior parte degli accessi tramite puntatori nulli, i sistemi di tipo Unix tendono a rendere la pagina all’indirizzo zero “non mappata”. Pertanto, gli accessi triggersno immediatamente un’eccezione CPU, in altre parole un segfault. Questo è molto meglio che lasciare che l’applicazione diventi canaglia. La tabella dei vettori di eccezioni, tuttavia, può trovarsi a qualsiasi indirizzo, almeno sui processori x86 (esiste un registro speciale per questo, caricato con l’opcode lidt ).

    L’indirizzo del punto di partenza fa parte di un insieme di convenzioni che descrivono la disposizione della memoria. Il linker, quando produce un eseguibile binario, deve conoscere queste convenzioni, quindi non è probabile che cambi. Fondamentalmente, per Linux, le convenzioni del layout di memoria sono ereditate dalle primissime versioni di Linux, nei primi anni ’90. Un processo deve avere accesso a diverse aree:

    • Il codice deve essere compreso nell’intervallo che include il punto di partenza.
    • Ci deve essere una pila.
    • Ci deve essere un heap, con un limite che viene aumentato con le brk() sistema brk() e sbrk() .
    • Ci deve essere spazio per le chiamate di sistema mmap() , incluso il caricamento della libreria condivisa.

    Oggigiorno, l’heap, dove malloc() va, è supportato da chiamate mmap() che ottengono blocchi di memoria su qualunque indirizzo il kernel ritenga opportuno. Ma nei tempi più antichi, Linux era come i precedenti sistemi simili a Unix, e il suo heap richiedeva una grande area in un blocco ininterrotto, che poteva crescere verso indirizzi crescenti. Quindi qualunque fosse la convenzione, ha dovuto riempire il codice e impilare gli indirizzi bassi, e dare ogni pezzo dello spazio degli indirizzi dopo un determinato punto all’heap.

    Ma c’è anche lo stack, che di solito è piuttosto piccolo, ma potrebbe crescere in modo drammatico in alcune occasioni. Lo stack si riduce e, quando lo stack è pieno, vogliamo davvero che il processo si arresti in modo prevedibile anziché sovrascrivere alcuni dati. Quindi doveva esserci un’area ampia per lo stack, con, nella parte bassa di quell’area, una pagina non mappata. Ed ecco! C’è una pagina non mappata all’indirizzo zero, per catturare le dereferenze del puntatore nullo. Quindi è stato definito che lo stack avrebbe ottenuto i primi 128 MB di spazio di indirizzamento, fatta eccezione per la prima pagina. Ciò significa che il codice doveva andare dopo quei 128 MB, a un indirizzo simile a 0x080xxxxx.

    Come sottolinea Michael, “perdere” 128 MB di spazio di indirizzamento non era un grosso problema, perché lo spazio degli indirizzi era molto ampio per quanto riguarda ciò che potrebbe essere effettivamente utilizzato. A quel tempo, il kernel di Linux limitava lo spazio degli indirizzi per un singolo processo a 1 GB, su un massimo di 4 GB consentito dall’hardware, e questo non era considerato un grosso problema.

    Perché non iniziare all’indirizzo 0x0? Ci sono almeno due ragioni per questo:

    • Poiché l’indirizzo zero è notoriamente conosciuto come puntatore NULL e utilizzato dai linguaggi di programmazione per verificare con facilità i puntatori. Non è ansible utilizzare un valore di indirizzo per questo, se si sta per eseguire il codice lì.
    • Il contenuto effettivo all’indirizzo 0 è spesso (ma non sempre) la tabella vettoriale delle eccezioni e pertanto non è accessibile in modalità non privilegiate. Consulta la documentazione della tua architettura specifica.

    Per quanto riguarda il punto di ingresso _start vs main : se si collega al runtime C (le librerie C standard), la libreria esegue il wrapping della funzione denominata main , in modo che possa inizializzare l’ambiente prima che venga chiamato main . Su Linux, questi sono i parametri argc e argv per l’applicazione, le variabili env e probabilmente alcuni primitivi e blocchi di sincronizzazione. Si assicura anche che il ritorno dai passaggi principali sul codice di stato e chiama la funzione _exit , che termina il processo.