L'adresse de départ est généralement définie par un script de l'éditeur de liens.
Par exemple, sous GNU/Linux, en regardant /usr/lib/ldscripts/elf_x86_64.x
on voit :
...
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); \
. = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
La valeur 0x400000
est la valeur par défaut pour le SEGMENT_START()
fonctionner sur cette plate-forme.
Vous pouvez en savoir plus sur les scripts de l'éditeur de liens en parcourant le manuel de l'éditeur de liens :
% info ld Scripts
ld
Le script de l'éditeur de liens par défaut a ce 0x400000
valeur intégrée pour les exécutables non-PIE.
Les PIE (Position Independent Executables) n'ont pas d'adresse de base par défaut ; ils sont toujours déplacés par le noyau, avec le noyau la valeur par défaut étant 0x0000555...
plus un décalage ASLR sauf si ASLR est désactivé pour ce processus ou à l'échelle du système. ld
n'a aucun contrôle sur cela. Notez que la plupart des systèmes modernes configurent GCC pour utiliser -fPIE -pie
par défaut, il passe donc -pie
à ld
, et transforme C en asm indépendant de la position. L'asm écrit à la main doit suivre les mêmes règles si vous le liez de cette façon.
Mais qu'est-ce qui fait 0x400000
(4 Mo) un bon défaut ?
Il doit être supérieur à mmap_min_addr
=65536 =64K par défaut.
Et être très éloigné de 0 donne beaucoup plus de place pour se prémunir contre NULL deref avec un décalage de lecture .text
ou .data
/.bss
mémoire (array[i]
où array
est NUL). Même sans augmenter mmap_min_addr
(ce qui laisse de la place sans casser les exécutables), généralement mmap
sélectionne au hasard des adresses élevées, donc en pratique, nous avons au moins 4 Mo de protection contre la deref NULL.
Aligné sur 2 M, c'est bien
Cela le place au début d'un répertoire de pages dans le niveau supérieur des tables de pages, ce qui signifie que le même nombre d'entrées de table de pages de 4K sera réparti sur moins de 2M d'entrées de répertoire de pages, économisant ainsi la mémoire de la table de pages du noyau et la page d'aide cache matériel -walk mieux. Pour les grands tableaux statiques, près du début d'un sous-arbre 1G du niveau supérieur est également bon.
IDK pourquoi 4MiB au lieu de 2MiB, ou quel était le raisonnement des développeurs. 4MiB est la taille de grande page 32 bits sans PAE (PTE 4 octets donc 10 bits par niveau au lieu de 9), mais un processeur doit utiliser des tables de pages x86-64 pour être en mode 64 bits.
Une adresse de départ basse autorise près de 2 Gio de tableaux statiques
(Sans utiliser un modèle de code plus grand, où au moins les grands tableaux doivent être traités de manière parfois moins efficace. Voir la section 3.5.1 Architectural Constraints dans le document x86-64 System V ABI pour plus de détails sur les modèles de code.)
Le modèle de code par défaut pour les exécutables non PIE ("petits") permet aux programmes de supposer que toute adresse statique se trouve dans les 2 Go d'espace d'adressage virtuel. Donc toute adresse absolue en .text
/.rodata
, .data
, .bss
peut être utilisé comme un signe immédiat étendu de 32 bits dans le code machine où c'est plus efficace.
(Ce n'est pas le cas dans un PIE ou une bibliothèque partagée :voir Les adresses absolues 32 bits ne sont plus autorisées dans x86-64 Linux ? pour les choses que vous / le compilateur ne pouvez pas faire dans x86-64 asm en conséquence, notamment addss xmm0, [foo + rdi*4]
nécessite à la place une LEA relative à RIP pour obtenir l'adresse de démarrage du tableau dans un registre. Le seul mode d'adressage relatif au RIP de x86-64 est [RIP+rel32], sans aucun registre à usage général.)
Démarrer les sections/segments de l'exécutable près du bas de l'espace d'adressage virtuel laisse la quasi-totalité des 2 Gio disponibles pour que text+data+bss soit aussi gros. (Il aurait peut-être été possible d'avoir une valeur par défaut plus élevée, et que de gros exécutables obligent ld à choisir une adresse inférieure pour les adapter, mais ce serait un script de liaison plus compliqué.)
Cela inclut les tableaux initialisés à zéro dans le .bss qui ne rendent pas le fichier exécutable énorme, juste l'image de processus en mémoire. En pratique, les programmeurs Fortran ont tendance à se heurter à cela plus que C et C++, car les tableaux statiques y sont populaires. Par exemple gfortran pour les nuls :que fait exactement mcmodel=medium ? a une bonne explication d'une erreur de construction avec le small
par défaut modèle et la différence asm x86-64 résultante pour medium
(où les objets au-dessus d'un certain seuil de taille ne sont pas supposés être dans le bas 2G ou à +-2G du code. Mais le code et les données statiques plus petites le sont toujours, donc la pénalité de vitesse est mineure.)
Par exemple static float arr[1UL<<28];
est un tableau de 1 Gio. Si vous en aviez 3, ils ne pourraient pas tous démarrer à l'intérieur du bas 2 GiB (ce qui peut être tout ce dont vous avez besoin pour l'asm écrit à la main), sans parler de l'accessibilité de chaque élément.
gcc -fno-pie
s'attend à pouvoir compiler float *p = &arr[size-1];
à mov $arr+1073741820, %edi
, un mov $imm32
de 5 octets . RIP-relative ne fonctionnera pas non plus si l'adresse cible est à plus de 2 Go du code générant l'adresse (ou en chargeant à partir de celui-ci avec movss arr+1073741820(%rip), %xmm0
; RIP-relative est le moyen normal de charger/stocker des données statiques même dans un non-PIE, lorsqu'il n'y a pas d'index de variable d'exécution.) C'est pourquoi le petit modèle PIC a également une limite de taille de 2 Go sur text + data + bss (plus espaces entre les segments) :toutes les données statiques et le code doivent se trouver à moins de 2 Gio de tout autre qui pourrait vouloir y accéder.
Si votre code n'accède qu'aux éléments élevés ou à leurs adresses via des indices de variable d'exécution, vous n'avez besoin que du début de chaque tableau, le symbole lui-même, pour être dans les 2 Gio inférieurs. J'oublie si l'éditeur de liens impose d'avoir la fin de bss dans le bas 2GiB; c'est possible puisque le script de l'éditeur de liens y place un symbole auquel certains codes de démarrage CRT pourraient faire référence.
Note de bas de page 1 :Il n'y a pas de tailles plus petites utiles pour un modèle de code inférieur à 2 Gio. Le code machine x86-64 utilise 8 ou 32 bits pour les modes immédiats et d'adressage. 8 bits (256 octets) est trop petit pour être utilisable, et de nombreuses instructions importantes comme call rel32
, mov r32, imm32
, et [rip+rel32]
l'adressage, ne sont de toute façon disponibles qu'avec des constantes de 4 octets et non de 1 octet.
La limitation à 2 Gio (au lieu de 4) signifie que les adresses peuvent être étendues à zéro en toute sécurité, comme avec mov edi, OFFSET arr
, ou extension de signe, comme avec mov eax, [arr + rdi*4]
. N'oubliez pas que les adresses ne sont pas le seul cas d'utilisation pour [reg + disp32]
modes d'adressage ; [rbp - 256]
peut souvent avoir un sens, il est donc bon que le signe du code machine x86-64 étende disp8 et disp32 à 64 bits, et non à zéro.
L'extension zéro implicite à 64 bits se produit lors de l'écriture d'un registre 32 bits, comme avec mov
-immédiat pour mettre une adresse dans un registre, où la taille d'opérande 32 bits est une instruction de code machine plus petite que la taille d'opérande 64 bits. Voir Comment charger l'adresse de la fonction ou de l'étiquette dans le registre (qui couvre également le LEA relatif à RIP).
Connexe pour Windows 32 bits
Raymond Chen a écrit un article expliquant pourquoi le même 0x400000
l'adresse de base est la valeur par défaut pour Windows 32 bits .
Il mentionne que les DLL sont chargées à des adresses élevées par défaut, et une adresse basse est loin de cela. Les objets partagés x86-64 SysV peuvent être chargés partout où il y a un espace d'adressage suffisamment grand, le noyau étant par défaut proche du haut de l'espace d'adressage virtuel de l'espace utilisateur, c'est-à-dire le haut de la plage canonique. Mais les objets partagés ELF doivent être entièrement relocalisables, donc ils fonctionneraient bien n'importe où.
Le choix de 4 Mo pour Windows 32 bits a également été motivé en évitant le faible 64 Ko (NULL deref) et en choisissant le début d'un répertoire de pages pour les anciennes tables de pages 32 bits. (Où la taille "largepage" est de 4 Mo, pas de 2 Mo pour x86-64 ou PAE.) Avec un tas de raisons de carte mémoire héritées Win95 et Win3.1 pour lesquelles au moins 1 Mo ou 4 Mo étaient partiellement nécessaires, et des trucs comme travailler autour du CPU bogues.