Pourquoi le point d'entrée de l'exécution ELF est-il l'adresse virtuelle du formulaire 0x80xxxxx et non zéro 0x0?

une fois exécuté, le programme commencera à courir à partir de l'adresse virtuelle 0x80482c0. Cette adresse ne renvoie pas à notre procédure main() , mais à une procédure appelée _start qui est créée par le linker.

Mes recherches sur Google jusqu'à présent m'ont mené à des spéculations historiques (vagues) comme celle-ci:

il y a du folklore que 0x08048000 était autrefois STACK_TOP (c'est-à-dire que la pile a augmenté vers le bas de près de 0x08048000 vers 0) sur un port de * NIX à i386 qui a été promulgué par un groupe de Santa Cruz, Californie. C'était alors que 128Mo de RAM était cher, et 4GO DE RAM était impensable.

quelqu'un peut-il confirmer ou infirmer cela?

18
demandé sur Paolo Forgia 2010-02-02 23:33:57

2 réponses

comme Mads l'a souligné, afin de capter la plupart des accès par des pointeurs null, les systèmes de type Unix ont tendance à rendre la page à l'adresse zéro"non cartographiée". Ainsi, l'accès déclenche immédiatement une exception CPU, en d'autres termes une segfault. C'est bien mieux que de laisser l'application aller voyous. La table de vecteur d'exception, cependant, peut être à n'importe quelle adresse, au moins sur les processeurs x86 (il y a un registre spécial pour cela, chargé avec l'opcode lidt ).

le l'adresse du point de départ fait partie d'un ensemble de conventions qui décrivent comment la mémoire est présentée. Le linker, lorsqu'il produit un exécutable binaire, doit connaître ces conventions, de sorte qu'elles ne sont pas susceptibles de changer. Fondamentalement, Pour Linux, les conventions de mise en page de la mémoire sont héritées des toutes premières versions de Linux, au début des années 90. Un processus doit avoir accès à plusieurs domaines:

  • le code doit être dans une plage qui inclut le point de départ.
  • Il doit y avoir une pile.
  • il doit y avoir un tas, avec une limite qui est augmentée avec les appels système brk() et sbrk() .
  • il doit y avoir de la place pour les appels système mmap() , y compris le chargement en bibliothèque partagée.

de nos jours, le tas, où malloc() VA, est soutenu par des appels mmap() qui obtiennent des morceaux de mémoire à n'importe quelle adresse que le noyau juge appropriée. Mais dans les plus vieux à L'époque, Linux était comme les systèmes Unix précédents, et son tas nécessitait une grande zone en un seul morceau ininterrompu, qui pouvait se développer vers des adresses croissantes. Donc quelle que soit la convention, elle devait bourrer le code et empiler vers les adresses basses, et donner chaque morceau de l'espace d'adresse après un point donné à la pile.

mais il y a aussi la pile, qui est habituellement assez petite, mais qui pourrait croître de façon spectaculaire dans certaines occasions. La pile grandit vers le bas, et quand la pile est complet, nous voulons vraiment que le processus de plantage prévisible plutôt que d'écraser certaines données. Donc il devait y avoir une large zone pour la pile, avec, au bas de cette zone, une page non cartographiée. Et voilà! Il y a une page non cartographiée à l'adresse zéro, pour saisir les déréférences nulles de pointeur. Par conséquent, il a été défini que la pile obtiendrait les 128 premiers Mo d'espace d'adresse, à l'exception de la première page. Cela signifie que le code devait aller après ces 128 Mo, à une adresse similaire à 0x080xxxxx.

comme le souligne Michael, "perdre" 128 Mo d'espace d'adresse n'était pas une grosse affaire parce que l'espace d'adresse était très grand en ce qui concerne ce qui pourrait être réellement utilisé. À cette époque, le noyau Linux limitait l'espace d'adresse pour un seul processus à 1 Go, au-dessus d'un maximum de 4 Go autorisé par le matériel, et ce n'était pas considéré comme un gros problème.

32
répondu Thomas Pornin 2010-02-02 21:13:07

Pourquoi ne pas commencer à l'adresse 0x0? Il y a au moins deux raisons à cela:

  • parce que address zero est connu comme un pointeur nul, et utilisé par les langages de programmation pour vérifier les pointeurs. Vous ne pouvez pas utiliser une valeur d'adresse pour cela, si vous allez y exécuter du code.
  • le contenu réel à l'adresse 0 est souvent (mais pas toujours) la table vectorielle d'exception, et n'est donc pas accessible en mode non privilégié. Consulter l' documentation de votre architecture.

Comme pour le point d'entrée _start vs main : Si vous vous liez avec L'exécution C (les bibliothèques C standard), la bibliothèque enveloppe la fonction nommée main , de sorte qu'elle puisse initialiser l'environnement avant que main ne soit appelé. Sur Linux, ce sont les paramètres argc et argv de l'application, les variables env , et probablement des primitives de synchronisation et des serrures. Il s'assure également que le retour de main passe sur le code d'état, et appelle la fonction _exit , qui termine le processus.

6
répondu Bulat M. 2016-09-25 12:45:59