Pourquoi l’adresse des variables statiques est-elle relative au pointeur d’instruction?

Je suis ce tutoriel sur l’assemblage.

Selon le tutoriel (que j’ai également essayé localement et obtenu des résultats similaires), le code source suivant:

int natural_generator() { int a = 1; static int b = -1; b += 1; /* (1, 2) */ return a + b; } 

Comstack à ces instructions de assembly:

 $ gdb static (gdb) break natural_generator (gdb) run (gdb) disassemble Dump of assembler code for function natural_generator: push %rbp mov %rsp,%rbp movl $0x1,-0x4(%rbp) mov 0x177(%rip),%eax # (1) add $0x1,%eax mov %eax,0x16c(%rip) # (2) mov -0x4(%rbp),%eax add 0x163(%rip),%eax # 0x100001018  pop %rbp retq End of assembler dump. 

(Commentaires de numéro de ligne (1) , (2) et (1, 2) ajoutés par moi.)

Question : pourquoi est, dans le code compilé, l’adresse de la variable statique b rapport au pointeur d’instruction (RIP), qui change constamment (voir lignes (1) et (2) ), et génère donc un code assembleur plus compliqué, plutôt que d’être relatif à la section spécifique de l’exécutable, où de telles variables sont stockées?

Selon le tutoriel mentionné, il existe une telle section:

En effet, la valeur de b est codée en dur dans une section différente de l’exemple d’exécutable, et elle est chargée en mémoire avec tout le code machine par le chargeur du système d’exploitation lors du lancement du processus.

(Souligné par moi.)

L’adressage relatif au protocole RIP est utilisé pour accéder à la variable statique b principalement pour deux raisons. La première est que cela rend le code indépendant de la position, ce qui signifie que s’il est utilisé dans une bibliothèque partagée ou un exécutable indépendant du poste, le code peut être plus facilement déplacé. La seconde est que cela permet au code d’être chargé n’importe où dans l’espace d’adressage 64 bits sans nécessiter d’énormes déplacements de 8 octets (64 bits) à coder dans l’instruction, qui ne sont de toute façon pas pris en charge par les CPU x86 64 bits.

Vous indiquez que le compilateur pourrait plutôt générer du code référençant la variable par rapport au début de la section dans laquelle il réside. Bien que cette opération présente également les mêmes avantages que ceux indiqués ci-dessus, cela ne rendrait pas l’assemblage moins compliqué. En fait, cela compliquera les choses. Le code d’assemblage généré devrait d’abord calculer l’adresse de la section dans laquelle vit la variable, car il ne connaîtrait que son emplacement par rapport au pointeur d’instruction. Il devrait ensuite la stocker dans un registre, afin que les access à b (et à toute autre variable de la section) puissent être effectués par rapport à cette adresse.

Le code x86 32 bits ne prenant pas en charge l’adressage relatif au RIP, votre solution de rechange est ce que le compilateur fait lorsqu’il génère un code indépendant de la position 32 bits. Il place la variable b dans la table de décalage global (GOT), puis accède à la variable par rapport à la base du GOT. Voici l’assembly généré par votre code lors de la compilation avec gcc -m32 -O3 -fPIC -S test.c :

 natural_generator: call __x86.get_pc_thunk.cx addl $_GLOBAL_OFFSET_TABLE_, %ecx movl b.1392@GOTOFF(%ecx), %eax leal 1(%eax), %edx addl $2, %eax movl %edx, b.1392@GOTOFF(%ecx) ret 

Le premier appel de fonction place l’adresse de l’instruction suivante dans ECX. L’instruction suivante calcule l’adresse du GOT en ajoutant le décalage relatif du GOT depuis le début de l’instruction. La variable ECX contient maintenant l’adresse du GOT et sert de base pour accéder à la variable b dans le rest du code.

Comparez cela au code 64 bits généré par gcc -m64 -O3 -S test.c :

 natural_generator: movl b.1745(%rip), %eax leal 1(%rax), %edx addl $2, %eax movl %edx, b.1745(%rip) ret 

(Le code est différent de l’exemple de votre question car l’optimisation est activée. En général, il est conseillé de ne regarder que la sortie optimisée, car sans optimisation, le compilateur génère souvent un code terrible qui fait beaucoup de choses inutiles. -fPIC indicateur -fPIC n’a pas besoin d’être utilisé, car le compilateur génère un code indépendant de la position 64 bits quel qu’il soit.)

Notez qu’il y a deux instructions d’assemblage de moins dans la version 64 bits, ce qui en fait la version moins compliquée. Vous pouvez également voir que le code utilise un registre de moins (ECX). Bien que cela ne fasse pas une grande différence dans votre code, dans un exemple plus compliqué, c’est un registre qui aurait pu être utilisé pour autre chose. Cela rend le code encore plus compliqué car le compilateur doit jongler davantage avec les registres.