
Tout d’abord, 64 bits, qu’est-ce que ça veut dire ?
Quand on parle de 64 bits, on parle en réalité d’une architecture et non pas de logiciels. Certes les logiciels compilés pour une architecture ne tournent que sur ces architectures mais se sont bien les architectures qui sont 64 bits. Je vous renvoie vers la définition de Wikipedia, courte mais claire. Vous l’aurez compris, les processeurs 64 bits, c’est pas nouveau. Mais alors pourquoi en parle-t-on aujourd’hui ?
La réponse est simple, ce n’est qu’aujourd’hui que nous pouvons exploiter pleinement les capacités du 64 bits. Je dis pleinement mais il faut savoir qu’elles sont sous sous-exploitées aujourd’hui.
En réalité, les développeurs semblent un peu perdu avec cette architecture et il n’est pas rare de lire sur internet des informations erronées. C’est encore pire chez nous, petits développeurs français, où les articles sont très peu nombreux. Il n’y a qu’à voir les résultats sur google : développer 64 bits
alors que nos amis anglophones ont des résultats bien plus pertinents : developping 64 bits
On notera toutefois une discussion intéressante sur developpez.com mais là encore de nombreuses informations fuses de tout côté sans que celles-ci ne soient vraiment vérifiées. Par contre, vous trouverez deux articles très intéressant sur le décodage d’instructions x86 x64.
Donc finalement, qu’est-ce que ça change ?
Pour des applications simples, la seule différence sera la possibilité d’utiliser plus de 4 Go de mémoire et le changement de taille des différents types de variables (int, long, etc.). Attention, ces nouvelles règles peuvent entraîner des soucis, d’où la bonne idée de programmer de façon stricte et propre, nous verrons ça à la fin.
Dans cet article, je vous propose de développer un petit programme en C et d’étudier deux versions, l’une compilée pour une architecture 32 bits, l’autre pour une architecture 64 bits. Enfin, nous finirons par quelques petites pistes pour aller plus loin dans le développement 64 bits.
Prenons ce petit main.c, tout simple :
1 2 3
| int main() {
return 0;
} |
Vous constatez que ce programme ne fait rien, mais il le fait bien. En réalité cela nous suffit pour voir quelques différences en les architectures.
Demandons à notre charmant compilateur de nous donner le code assembleur de ce programme :
Etudions ce code :
Voici le code que j’obtiens, vous devriez avoir sensiblement le même, du moins pour la partie qui nous intéresse (le main) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| .file "main.c"
.text
.globl main
.type main, @function
main:
.LFB2:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
movl $0, %eax
leave
ret
.LFE2:
.size main, .-main
.section .eh_frame,"a",@progbits
.Lframe1:
.long .LECIE1-.LSCIE1
.LSCIE1:
.long 0x0
.byte 0x1
.string "zR"
.uleb128 0x1
.sleb128 -8
.byte 0x10
.uleb128 0x1
.byte 0x3
.byte 0xc
.uleb128 0x7
.uleb128 0x8
.byte 0x90 |
La partie qui nous intéresse commence ligne 5 et termine ligne 13. Il s’agit de l’équivalent en assembleur du code en C que j’ai écrit plus haut.
Pour les initiés, vous noterez la présence de registres étranges, où sont EBP et ESP ? Ouf EAX est toujours là!
Pour les non-initiés, vous ne remarquez rien et vous vous posez quelques questions, détaillons donc ce code :
main:
=> nom de la fonction
pushq %rbp
=> sauvegarde en haut de la pile, le registre RBP. RBP est l’équivalent 64 bits du registre EBP.
movq %rsp, %rbp
=> RSP est l’équivalent 64 bits du registre ESP (i386). ESP est le pointeur de pile, l’adresse qu’il contient pointe sur le haut de la pile. On sauvegarde RSP dans RBP. Vous l’avez deviné, gcc veut construire un cadre de pile, étonnant non ?
movl $0, %eax
=> on met la valeur 0 dans le registre EAX.
leave
=> restaure RSP à partir de RBP et dépile RBP.
ret
=> fin de la fonction main.
Vous voyez donc que GCC utilise les registres 64 bits, nous avons bien un programme 64 bits. Vous trouverez une petite liste des autres registres 64 bits sur developpez.com.
Notez que, juste pour me contredire, GCC utilise le registre EAX et non pas RAX. Ceci ne fait aucune différence bien entendue.
Demandons maintenant à GCC de nous donner le code assembleur générer pour une architecture 32 bits :
Et voici le code assembleur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| .file "main.c"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
movl $0, %eax
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3"
.section .note.GNU-stack,"",@progbits |
On peut noter que le code est sensiblement plus long. Ceci dit, le travail effectué est sensiblement le même. GCC constuit un cadre de pile, charge 0 dans le registre EAX ,restaure la pile et enfin quitte la fonction (ret).
Ce qui est intéressant, c’est l’utilisation des registres ESP et EBP, nos bons vieux registres x386.
En assembleur x64, vous pouvez aussi utiliset les registres ESP et EBP mais vous n’accéderez qu’au premier 32 bits des registres RSP et RBP, ce qui posera des problèmes dans le cas de la création d’un cadre de pile puisque le registre RSP contient une adresse codée sur 64 bits.
Notez que le programme 32 bits ne peut pas être compilé tel que sur une distribution Linux 64 bits. Nous avons besoin des headers 32 bits et nous devons indiquer à l’éditeur de liens où se trouvent ces headers pour qu’il puisse compiler notre programme. On parle alors de cross-compilation.
Pour faire une pierre de coup, voici le code assembleur obtenu en demandant une optimisation de niveau 3 à GCC :
Et le code :
1 2 3
| main:
xorl %eax, %eax
ret |
L’instruction xorl %eax, %eax effectue l’opération XOR sur la valeur de deux registres, ce qui équivaut à mettre le registre EAX à 0. Notez que cette instruction est plus rapide que MOVL %eax,0.
Au final, pour un programme très très simple, vous pouvez noter que l’optimisation de niveau 3 n’est pas négligeable et simplifie beaucoup le code.
Pour finir, je vous propose un peu de lecture très intéressante :