Codice indipendente dalla posizione - Position-independent code

In informatica , il codice indipendente dalla posizione ( PIC ) o l' eseguibile indipendente dalla posizione ( PIE ) è un corpo di codice macchina che, essendo posizionato da qualche parte nella memoria primaria , viene eseguito correttamente indipendentemente dal suo indirizzo assoluto . Il PIC è comunemente usato per le librerie condivise , in modo che lo stesso codice della libreria possa essere caricato in una posizione nello spazio degli indirizzi di ogni programma in cui non si sovrapponga ad altra memoria in uso (ad esempio, altre librerie condivise). PIC è stato utilizzato anche su vecchi sistemi di computer privi di MMU , in modo che il sistema operativo potesse tenere le applicazioni lontane l'una dall'altra anche all'interno del singolo spazio di indirizzi di un sistema senza MMU.

Il codice indipendente dalla posizione può essere eseguito a qualsiasi indirizzo di memoria senza modifiche. Questo differisce dal codice assoluto , che deve essere caricato in una posizione specifica per funzionare correttamente, e dal codice LTL ( load-time locatable ), in cui un linker o un program loader modifica un programma prima dell'esecuzione in modo che possa essere eseguito solo da una particolare memoria Posizione. La generazione di codice indipendente dalla posizione è spesso il comportamento predefinito per i compilatori , ma possono porre restrizioni all'uso di alcune funzionalità del linguaggio, come impedire l'uso di indirizzi assoluti (il codice indipendente dalla posizione deve utilizzare l'indirizzamento relativo ). Le istruzioni che fanno riferimento direttamente a indirizzi di memoria specifici a volte vengono eseguite più velocemente e la loro sostituzione con istruzioni di indirizzamento relativo equivalenti può comportare un'esecuzione leggermente più lenta, sebbene i processori moderni facciano la differenza praticamente trascurabile.

Storia

Nei primi computer come l' IBM 701 (29 aprile 1952) o l' UNIVAC I (31 marzo 1951) il codice era dipendente dalla posizione: ogni programma era costruito per essere caricato ed eseguito da un particolare indirizzo. Quei primi computer non avevano un sistema operativo e non erano capaci di multitasking. I programmi venivano caricati nella memoria principale (o anche memorizzati su un tamburo magnetico per l'esecuzione direttamente da lì) ed eseguiti uno alla volta. In un simile contesto operativo, non era necessario un codice indipendente dalla posizione.

L' IBM System/360 (7 aprile 1964) è stato progettato con un indirizzamento troncato simile a quello dell'UNIVAC III , con in mente l'indipendenza dalla posizione del codice. Nell'indirizzamento troncato, gli indirizzi di memoria vengono calcolati da un registro di base e da un offset. All'inizio di un programma, il programmatore deve stabilire l' indirizzabilità caricando un registro di base; normalmente il programmatore informa anche l'assemblatore con una pseudo-operazione USING . Il programmatore può caricare il registro di base da un registro noto per contenere l'indirizzo del punto di ingresso, tipicamente R15, o può utilizzare l' istruzione BALR (Branch And Link, Register form) (con un valore R2 di 0) per memorizzare l'indirizzo dell'istruzione sequenziale successiva nel registro di base, che è stato poi codificato esplicitamente o implicitamente in ciascuna istruzione che si riferiva a una locazione di memoria all'interno del programma. Potrebbero essere utilizzati più registri di base, per codice o per dati. Tali istruzioni richiedono meno memoria perché non devono contenere un indirizzo completo a 24, 31, 32 o 64 bit (4 o 8 byte), ma invece un numero di registro di base (codificato in 4 bit) e un offset di indirizzo a 12 bit (codificato in 12 bit), richiedendo solo due byte.

Questa tecnica di programmazione è standard sui sistemi di tipo IBM S/360. È stato utilizzato fino all'attuale IBM System/z. Quando si codifica in linguaggio assembly, il programmatore deve stabilire l'indirizzabilità per il programma come descritto sopra e utilizzare anche altri registri di base per l'archiviazione allocata dinamicamente. I compilatori si occupano automaticamente di questo tipo di indirizzamento.

Il primo sistema operativo IBM DOS/360 (1966) non utilizzava l'archiviazione virtuale (poiché i primi modelli di System S/360 non lo supportavano), ma aveva la capacità di posizionare i programmi in una posizione di archiviazione arbitraria (o scelta automaticamente) durante il caricamento tramite il nome PHASE,* istruzione JCL (Job Control Language).

Quindi, sui sistemi S/360 senza memoria virtuale, un programma potrebbe essere caricato in qualsiasi posizione di memoria, ma ciò richiedeva un'area di memoria contigua abbastanza grande da contenere quel programma. A volte si verificava la frammentazione della memoria dal caricamento e dallo scaricamento di moduli di dimensioni diverse. L'archiviazione virtuale, in base alla progettazione, non ha questa limitazione.

Sebbene DOS/360 e OS/360 non supportassero PIC, le routine SVC transitorie in OS/360 non potevano contenere costanti di indirizzo rilocabili e potevano essere eseguite in qualsiasi area transitoria senza riposizionamento .

Lo storage virtuale è stato introdotto per la prima volta su IBM System/360 modello 67 nel (1965) per supportare il primo sistema operativo multi-tasking e time-sharing di IBM TSS/360. Le versioni successive di DOS/360 (DOS/VS ecc.) e i successivi sistemi operativi IBM utilizzavano tutte lo storage virtuale. L'indirizzamento troncato è rimasto come parte dell'architettura di base ed è ancora vantaggioso quando più moduli devono essere caricati nello stesso spazio di indirizzi virtuale.

Altri primi sistemi segmentati come Burroughs MCP su Burroughs B5000 (1961) e Multics (1964), sistemi di paging come IBM TSS/360 (1967) o sistemi di base e limiti come GECOS su GE 625 e EXEC su UNIVAC 1107 , il codice era anche intrinsecamente indipendente dalla posizione, poiché gli indirizzi in un programma erano relativi al segmento corrente anziché assoluti.

L'invenzione della traduzione dinamica degli indirizzi (la funzione fornita da una MMU ) originariamente ridusse la necessità di codice indipendente dalla posizione perché ogni processo poteva avere il proprio spazio di indirizzi indipendente (intervallo di indirizzi). Tuttavia, più lavori simultanei che utilizzano lo stesso codice hanno creato uno spreco di memoria fisica. Se due lavori eseguono programmi completamente identici, la traduzione dinamica degli indirizzi fornisce una soluzione consentendo al sistema semplicemente di mappare l'indirizzo 32K di due lavori diversi sugli stessi byte di memoria reale, contenente la singola copia del programma.

Programmi diversi possono condividere un codice comune. Ad esempio, il programma di gestione stipendi e il programma di contabilità clienti possono contenere entrambi una subroutine di ordinamento identica. Un modulo condiviso (una libreria condivisa è una forma di modulo condiviso) viene caricato una volta e mappato nei due spazi di indirizzi.

Dettagli tecnici

Le chiamate di procedura all'interno di una libreria condivisa vengono in genere effettuate tramite piccoli stub di tabelle di collegamento delle procedure , che quindi chiamano la funzione definitiva. Ciò consente in particolare a una libreria condivisa di ereditare determinate chiamate di funzione da librerie caricate in precedenza anziché utilizzare le proprie versioni.

I riferimenti ai dati dal codice indipendente dalla posizione vengono solitamente effettuati indirettamente, tramite Global Offset Tables (GOT), che memorizzano gli indirizzi di tutte le variabili globali a cui si accede . Esiste un GOT per unità di compilazione o modulo oggetto e si trova a un offset fisso dal codice (sebbene questo offset non sia noto finché la libreria non è collegata ). Quando un linker collega i moduli per creare una libreria condivisa, unisce i GOT e imposta gli offset finali nel codice. Non è necessario regolare gli offset durante il caricamento successivo della libreria condivisa.

Le funzioni indipendenti dalla posizione che accedono ai dati globali iniziano determinando l'indirizzo assoluto del GOT dato il proprio valore di contatore del programma corrente. Questo spesso assume la forma di una falsa chiamata di funzione per ottenere il valore di ritorno sullo stack ( x86 ), in uno specifico registro standard ( SPARC , MIPS ), o in un registro speciale ( POWER / PowerPC / Power ISA ), che può quindi essere spostato in un registro standard predefinito, o per ottenere in quel registro standard ( PA-RISC , alfa , ESA / 390 e z / Architecture ). Alcune architetture di processori, come Motorola 68000 , Motorola 6809 , WDC 65C816 , MMIX di Knuth , ARM e x86-64 consentono di fare riferimento ai dati per offset dal contatore del programma . Questo è specificamente mirato a rendere il codice indipendente dalla posizione più piccolo, meno impegnativo per il registro e quindi più efficiente.

DLL di Windows

Le librerie a collegamento dinamico (DLL) in Microsoft Windows utilizzano la variante E8 dell'istruzione CALL (Call near, relative, displacement rispetto all'istruzione successiva). Queste istruzioni non devono essere corrette quando viene caricata una DLL.

Ci si aspetta che alcune variabili globali (es. array di stringhe letterali, tabelle di funzioni virtuali) contengano un indirizzo di un oggetto nella sezione dati rispettivamente nella sezione codice della libreria dinamica; l'indirizzo memorizzato nella variabile globale pertanto deve essere aggiornato per riflettere l'indirizzo in cui è stata caricata la DLL. Il caricatore dinamico calcola l'indirizzo a cui fa riferimento una variabile globale e memorizza il valore in tale variabile globale; questo attiva la copia su scrittura di una pagina di memoria contenente tale variabile globale. Le pagine con codice e le pagine con variabili globali che non contengono puntatori a codice o dati globali rimangono condivise tra i processi. Questa operazione deve essere eseguita in qualsiasi sistema operativo in grado di caricare una libreria dinamica a un indirizzo arbitrario.

In Windows Vista e nelle versioni successive di Windows, il trasferimento di DLL ed eseguibili viene eseguito dal gestore della memoria del kernel, che condivide i file binari trasferiti su più processi. Le immagini vengono sempre ricollocate dai loro indirizzi di base preferiti, ottenendo la randomizzazione del layout dello spazio degli indirizzi (ASLR).

Le versioni di Windows precedenti a Vista richiedono che le DLL di sistema siano precollegate a indirizzi fissi non in conflitto al momento del collegamento per evitare il trasferimento delle immagini in fase di esecuzione. Il riposizionamento del runtime in queste versioni precedenti di Windows viene eseguito dal caricatore DLL nel contesto di ciascun processo e le parti rilocate risultanti di ciascuna immagine non possono più essere condivise tra i processi.

La gestione delle DLL in Windows è diversa dalla precedente procedura OS/2 da cui deriva. OS/2 presenta una terza alternativa e tenta di caricare DLL che non sono indipendenti dalla posizione in una "arena condivisa" dedicata in memoria e le mappa una volta caricate. Tutti gli utenti della DLL possono utilizzare la stessa copia in memoria.

Multics

In Multics ogni procedura ha concettualmente un segmento di codice e un segmento di collegamento. Il segmento di codice contiene solo codice e la sezione di collegamento funge da modello per un nuovo segmento di collegamento. Il registro del puntatore 4 (PR4) punta al segmento di collegamento della procedura. Una chiamata a una procedura salva PR4 nello stack prima di caricarlo con un puntatore al segmento di collegamento del chiamato. La chiamata di procedura utilizza una coppia di puntatori indiretti con un flag per causare un trap alla prima chiamata in modo che il meccanismo di collegamento dinamico possa aggiungere la nuova procedura e il relativo segmento di collegamento alla tabella dei segmenti conosciuti (KST), costruire un nuovo segmento di collegamento, inserire i loro numeri di segmento nella sezione di collegamento del chiamante e reimpostare il flag nella coppia di puntatori indiretti.

TSS

In IBM S/360 Time Sharing System (TSS/360 e TSS/370) ogni procedura può avere un CSECT pubblico di sola lettura e una sezione prototipi privata scrivibile (PSECT). Un chiamante carica una costante V per la routine nel Registro Generale 15 (GR15) e copia una costante R per PSECT della routine nella 19a parola dell'area di salvataggio indicata come GR13.

Il caricatore dinamico non carica le pagine del programma né risolve le costanti di indirizzo fino al primo errore di pagina.

Eseguibili indipendenti dalla posizione

Gli eseguibili indipendenti dalla posizione (PIE) sono binari eseguibili realizzati interamente da codice indipendente dalla posizione. Mentre alcuni sistemi eseguono solo eseguibili PIC, ci sono altri motivi per cui vengono utilizzati. I binari PIE vengono utilizzati in alcune distribuzioni Linux incentrate sulla sicurezza per consentire a PaX o Exec Shield di utilizzare la randomizzazione del layout dello spazio degli indirizzi per impedire agli aggressori di sapere dove si trova il codice eseguibile esistente durante un attacco alla sicurezza utilizzando exploit che si basano sulla conoscenza dell'offset del codice eseguibile in il binario, come gli attacchi return-to-libc .

macOS e iOS di Apple supportano completamente gli eseguibili PIE a partire dalle versioni 10.7 e 4.3, rispettivamente; viene emesso un avviso quando gli eseguibili iOS non PIE vengono inviati per l'approvazione all'App Store di Apple ma non ci sono ancora requisiti rigidi e le applicazioni non PIE non vengono rifiutate.

OpenBSD ha PIE abilitato per impostazione predefinita sulla maggior parte delle architetture a partire da OpenBSD 5.3, rilasciato il 1 maggio 2013. Il supporto per PIE nei binari collegati staticamente , come gli eseguibili /bine le /sbindirectory, è stato aggiunto verso la fine del 2014. openSUSE ha aggiunto PIE come impostazione predefinita in 2015-02. A partire da Fedora  23, i manutentori di Fedora hanno deciso di creare pacchetti con PIE abilitato come impostazione predefinita. Ubuntu 17.10 ha PIE abilitato per impostazione predefinita su tutte le architetture. I nuovi profili di Gentoo ora supportano PIE per impostazione predefinita. Intorno a luglio 2017, Debian ha abilitato PIE per impostazione predefinita.

Android ha abilitato il supporto per PIE in Jelly Bean e rimosso il supporto per linker non PIE in Lollipop .

Guarda anche

Appunti

Riferimenti

link esterno