GNU/Linux >> Linux Esercitazione >  >> Linux

Perché il linker Linux/gnu ha scelto l'indirizzo 0x400000?

L'indirizzo iniziale è solitamente impostato da uno script linker.

Ad esempio, su GNU/Linux, guardando /usr/lib/ldscripts/elf_x86_64.x vediamo:

...
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); \
    . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;

Il valore 0x400000 è il valore predefinito per SEGMENT_START() funzione su questa piattaforma.

Puoi trovare ulteriori informazioni sugli script del linker sfogliando il manuale del linker:

% info ld Scripts

ld lo script linker predefinito di ha quel 0x400000 valore incorporato per eseguibili non-PIE.

I PIE (Position Independent Executables) non hanno un indirizzo di base predefinito; sono sempre rilocati dal kernel, con i kernel il valore predefinito è 0x0000555... più alcuni offset ASLR a meno che ASLR non sia disabilitato per questo processo o a livello di sistema. ld non ha alcun controllo su questo. Nota che la maggior parte dei sistemi moderni configura GCC per usare -fPIE -pie per impostazione predefinita, quindi passa -pie a ld , e trasforma C in asm indipendente dalla posizione. L'asm scritto a mano deve seguire le stesse regole se lo colleghi in questo modo.

Ma cosa rende 0x400000 (4 MiB) un buon valore predefinito?

Deve essere superiore a mmap_min_addr =65536 =64K per impostazione predefinita.

Ed essere molto lontano da 0 offre molto più spazio per proteggersi da NULL deref con una lettura offset .text o .data /.bss memoria (array[i] dove array è zero). Anche senza aumentare mmap_min_addr (a cui questo lascia spazio senza rompere gli eseguibili), di solito mmap seleziona casualmente indirizzi alti, quindi in pratica abbiamo almeno 4MiB di protezione contro NULL deref.

L'allineamento 2M va bene

Questo lo pone all'inizio di una directory di pagine nel livello successivo delle tabelle delle pagine, il che significa che lo stesso numero di 4K voci della tabella delle pagine verrà suddiviso in meno 2 milioni di voci della directory delle pagine, risparmiando memoria della tabella delle pagine del kernel e aiutando la pagina -cammina meglio la cache hardware. Per i grandi array statici, va bene anche vicino all'inizio di un sottoalbero 1G del livello successivo.

IDK perché 4MiB invece di 2MiB o quale fosse il ragionamento degli sviluppatori. 4MiB è la dimensione della pagina grande a 32 bit senza PAE (PTE a 4 byte, quindi 10 bit per livello anziché 9), ma una CPU deve utilizzare tabelle di pagina x86-64 per essere in modalità a 64 bit.

Un indirizzo iniziale basso consente quasi 2 GiB di array statici

(Senza utilizzare un modello di codice più grande, in cui almeno gli array di grandi dimensioni devono essere indirizzati in modi che a volte sono meno efficienti. Vedere la sezione 3.5.1 Vincoli architetturali nel documento x86-64 System V ABI per i dettagli sui modelli di codice.)

Il modello di codice predefinito per gli eseguibili non PIE ("piccolo") consente ai programmi di presumere che qualsiasi indirizzo statico si trovi nei 2GiB bassi dello spazio degli indirizzi virtuali. Quindi qualsiasi indirizzo assoluto in .text /.rodata , .data , .bss può essere utilizzato come segno immediato esteso a 32 bit nel codice macchina dove è più efficiente.

(Questo non è il caso di una PIE o di una libreria condivisa:vedi Gli indirizzi assoluti a 32 bit non sono più consentiti in Linux x86-64? per le cose che tu / il compilatore non puoi fare in x86-64 asm di conseguenza, in particolare addss xmm0, [foo + rdi*4] richiede invece un LEA relativo al RIP per ottenere l'indirizzo iniziale dell'array in un registro. L'unica modalità di indirizzamento relativa a RIP di x86-64 è [RIP+rel32], senza registri generici.)

L'avvio delle sezioni/segmenti dell'eseguibile vicino alla parte inferiore dello spazio degli indirizzi virtuali lascia quasi l'intero 2GiB disponibile affinché testo+dati+bss sia così grande. (Potrebbe essere possibile avere un valore predefinito più alto e avere eseguibili di grandi dimensioni che fanno scegliere a ld un indirizzo più basso per adattarli, ma sarebbe uno script di linker più complicato.)

Ciò include gli array con inizializzazione zero nel .bss che non rendono il file eseguibile enorme, solo l'immagine di processo in memoria. In pratica, i programmatori Fortran tendono a imbattersi in questo più di C e C++, poiché gli array statici sono popolari lì. Ad esempio gfortran for dummies:cosa fa esattamente mcmodel=medium? ha una buona spiegazione di un errore di compilazione con il small predefinito model e la risultante differenza asm x86-64 per medium (dove non si presume che gli oggetti al di sopra di una certa soglia di dimensioni si trovino nei 2G bassi o entro +-2G del codice. Ma il codice e i dati statici più piccoli lo sono ancora, quindi la penalità di velocità è minore.)

Ad esempio static float arr[1UL<<28]; è un array da 1 GiB. Se ne avessi 3, non potrebbero iniziare tutti all'interno dei bassi 2 GiB (che potrebbero essere tutto ciò di cui hai bisogno per un asm scritto a mano), figuriamoci avere ogni elemento accessibile.

gcc -fno-pie si aspetta di poter compilare float *p = &arr[size-1]; a mov $arr+1073741820, %edi , un mov $imm32 di 5 byte . Anche il rapporto RIP non funzionerà se l'indirizzo di destinazione è a più di 2GiB di distanza dal codice che genera l'indirizzo (o che viene caricato da esso con movss arr+1073741820(%rip), %xmm0; RIP-relative è il modo normale per caricare/archiviare dati statici anche in un non-PIE, quando non c'è un indice variabile di runtime.) Ecco perché il modello small-PIC ha anche un limite di dimensione di 2GiB su text+data+bss (più gap tra i segmenti):tutti i dati statici e il codice devono trovarsi entro 2GiB da qualsiasi altro che potrebbe volerlo raggiungere.

Se il tuo codice accede solo a elementi alti o ai loro indirizzi tramite indici di variabili di runtime, hai solo bisogno che l'inizio di ogni array, il simbolo stesso, sia nei 2 GiB bassi. Dimentico se il linker impone di avere la fine di bss entro i 2GiB bassi; potrebbe poiché lo script del linker inserisce un simbolo a cui potrebbe fare riferimento un codice di avvio CRT.

Nota a piè di pagina 1 :non sono disponibili dimensioni inferiori utili per un modello di codice inferiore a 2GiB. Il codice macchina x86-64 utilizza 8 o 32 bit per gli immediati e la modalità di indirizzamento. 8 bit (256 byte) è troppo piccolo per essere utilizzabile e molte istruzioni importanti come call rel32 , mov r32, imm32 e [rip+rel32] indirizzamento, sono comunque disponibili solo con costanti a 4 byte e non a 1 byte.

Limitarsi ai bassi 2 GiB (invece di 4) significa che gli indirizzi possono essere tranquillamente estesi a zero come con mov edi, OFFSET arr , o sign-extended, come con mov eax, [arr + rdi*4] . Ricorda che gli indirizzi non sono l'unico caso d'uso per [reg + disp32] modalità di indirizzamento; [rbp - 256] spesso può avere senso, quindi è positivo che il codice macchina x86-64 estenda disp8 e disp32 a 64 bit, non che estenda zero.

L'estensione zero implicita a 64 bit si verifica quando si scrive un registro a 32 bit, come con mov -Immediato per inserire un indirizzo in un registro, dove la dimensione dell'operando a 32 bit è un'istruzione di codice macchina più piccola rispetto alla dimensione dell'operando a 64 bit. Vedere Come caricare l'indirizzo della funzione o dell'etichetta nel registro (che copre anche il LEA relativo al RIP).

Relativo a Windows a 32 bit

Raymond Chen ha scritto un articolo sul perché lo stesso 0x400000 l'indirizzo di base è quello predefinito per Windows a 32 bit .

Afferma che le DLL vengono caricate a indirizzi alti per impostazione predefinita e un indirizzo basso è tutt'altro. Gli oggetti condivisi SysV x86-64 possono essere caricati ovunque ci sia un divario sufficientemente ampio nello spazio degli indirizzi, con il kernel impostato per impostazione predefinita vicino alla parte superiore dello spazio degli indirizzi virtuale dello spazio utente, ovvero la parte superiore dell'intervallo canonico. Ma gli oggetti condivisi ELF devono essere completamente riposizionabili, quindi funzionerebbero bene ovunque.

La scelta di 4 MiB per Windows a 32 bit è stata motivata anche dall'evitare i 64 KB bassi (NULL deref) e dalla scelta dell'inizio di una directory di pagina per le tabelle di pagine legacy a 32 bit. (Dove la dimensione "largepage" è 4M, non 2M per x86-64 o PAE.) Con un mucchio di motivi di mappatura della memoria legacy Win95 e Win3.1 per cui almeno 1MiB o 4MiB erano parzialmente necessari e cose come lavorare intorno alla CPU bug.


Linux
  1. Perché Linux è fondamentale per l'edge computing

  2. Perché mi attengo a xterm

  3. Indirizzo predefinito

  4. Perché GNU/Linux SUS v3+ non è compatibile?

  5. Perché Perl è installato di default con la maggior parte delle distribuzioni Linux?

I 10 motivi principali per utilizzare Linux

Perché i nerd usano Linux

Come trovare l'indirizzo IP in Linux

Come modificare l'indirizzo MAC in Linux

Come modificare l'indirizzo IP su Linux

Come ottenere il tuo indirizzo IP su Linux