Mois de l'assembleur x86, semaine 3 - Structures logiques
Le mois de Juin prend une saveur particulière cette année sur le blog du Hackfest, en effet, quatre articles sur la programmation assembleur seront publiés, un à chaque semaine, et le mois sera couronné d’une présentation HackerSpace sur le sujet. Espérons que vous y apprendrez quelque chose! Voici le troisième article de cette série sur le sujet. Les articles des semaine 1 et semaine 2 sont aussi disponibles.
Jusqu'a maintenant, tout ce que nous avons fait ne nous permet pas de faire des applications très élaborées. Nous avons parlé de la structure du processeur ainsi que de la structure de base des applications elles-mêmes. Aucune considération n’a encore été portée sur le contrôle logique de l’exécution d’une application. C’est aujourd’hui que les choses changent. Le thème aujourd’hui: contrôle du flux d’exécution de l’application.
Avant d’aller plus loin, un petit retour sur le premier article de la série s’impose. Comme nous discutons principalement du registre eip, je vous propose de prendre quelques minutes pour se remettre son fonctionnement en tête. Vous devriez tous reconnaître le graphique suivant.
Tel que le montre le graphique précédent, le registre eip pointe sur la prochaine instruction devant être exécutée par le processeur. Ainsi, en réalité, lorsque nous allons modifier le cours logique du déroullement d’une application, nous modifierons, de manière détournée, la valeur du registre eip de manière à être en mesure de déterminer nous même la prochaine instruction devant être exécutée sans que le flo d’exécution soit linéaire. Pour commencer, afin de comprendre le flo d’exécution d’une application “plate” au sens qu’elle ne possède pas de variations, nous allons prendre l’application que nous avons programmée la semaine dernière. Cependant, afin d’avoir un visuel sur les adresses utilisées en cours d’exécution, nous allons désassembler le fichier binaire que nous avons préparé la semaine dernière. Pour effectuer le désassemblage, nous allons utiliser l’utilitaire objdump. Plus précisément, nous utiliserons l’option -D qui permet de désassembler toutes les section d’un fichier binaire. Le résultat de cette commande est montré à la figure suivante.
$ objdump -D movingDataProject movingDataProject: file format elf64-x86-64 Disassembly of section .text: 00000000004000b0 <_start>: 4000b0: b8 c8 00 60 00 mov $0x6000c8,%eax 4000b5: 67 83 00 32 addl $0x32,(%eax) 4000b9: 67 ff 00 incl (%eax) 4000bc: b8 01 00 00 00 mov $0x1,%eax 4000c1: bb 00 00 00 00 mov $0x0,%ebx 4000c6: cd 80 int $0x80 Disassembly of section .data: 00000000006000c8 : 6000c8: ff 00 incl (%rax) ... Disassembly of section .stab: 0000000000000000 : 0: 01 00 add %eax,(%rax) 2: 00 00 add %al,(%rax) 4: 00 00 add %al,(%rax) *****************************Code omis********************************* |
Comme vous pouvez le voir, l’ensemble des section de l’application ont été désassemblées. Dans notre cas, nous nous intéresserons seulement au label _start de la section .text. Vous remarquerez que les points où nous utilisions des adresses mémoires ont été modifiés et que ces adresses mémoires sont maintenant dans le code. Au moment de lier l’application, les labels ont été remplacé par les vraies adresses mémoires. Aussi, vous pouvez voir les adresses des instructions en début de ligne. Par exemple, l’instruction mov $0x6000c8, %eax est à l’adresse 4000b0. Avec le code désassemblé en main, nous allons lancer l’application à l’aide de gdb et mettre un point d’arrêt sur le label _start. Une fois arrivé au point d’arrêt, nous allons faire afficher la valeur des registres. La figure suivante montre l’ensemble des étapes décrites précédement.
$ gdb movingDataProject GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04 Copyright (C) 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". For bug reporting instructions, please see: Reading symbols from movingDataProject...done. (gdb) break _start Breakpoint 1 at 0x4000b0: file movingDataProject.s, line 18. (gdb) run Starting program:movingDataProject Breakpoint 1, _start () at movingDataProject.s:18 18 movl $nbPommes, %eax ←Prochaine ligne qui sera exécutée (gdb) info registers rax 0x0 0 rbx 0x0 0 rcx 0x0 0 rdx 0x0 0 rsi 0x0 0 rdi 0x0 0 rbp 0x0 0x0 rsp 0x7fffffffe1f0 0x7fffffffe1f0 r8 0x0 0 r9 0x0 0 r10 0x0 0 r11 0x200 512 r12 0x0 0 r13 0x0 0 r14 0x0 0 r15 0x0 0 rip 0x4000b0 0x4000b0 <_start> eflags 0x202 [ IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 (gdb) |
N’oubliez pas, sur un système 64 bits, le registre eip porte le nom rip. L’analyse des informations de registre montre clairement que la prochaine instruction à être exécuté se trouve à l’adresse 4000b0. Laissez le programme s’exécuté étape par étape en observant la valeur du registre eip par rapport aux adresses inscrite dans la version désassemblée de l’application. Il sera facile de comprendre le lien entre le registre eip et la prochaine instruction à exécuter. Bon, passons aux choses “sérieuses”. Le but pour aujourd’hui est d’apprendre à implémenter des structures conditionnelles ainsi que des structures répétitives. Assez simple bref! Mais avec ces petits éléments, vous aurez ce qu’il faut en mains pour commencer à comprendre la logique des applications lorsque vous regardez le code assembleur de celles-ci.
Structure conditionnelle
La première structure dont nous allons parler, le if est assez simple à produire en assembleur. Par contre, afin de se donner une base de comparaison, nous allons commencer par écrire une version C de l’application test que nous allons développer en assembleur. Ainsi, commencez par programmer l’exemple suivant en C et compiler l’exemple (gcc -o fichier.c).
int nbPommes[2] = {200, 100}; int main(){ if (nbPommes[0] > nbPommes[1]){ nbPommes[0]++; } return 0; } |
L’application possède un tableau dans lequel deux chiffres sont présents. Si le premier indice est plus grand que le deuxième, le premier indice est incrémenté. Nous allons maintenant traduire cette application en assembleur. Voici le code assembleur pour une application très similaire.
.section .data nbPommes: .int 200, 100 .section .text .globl _start _start: movl $1, %esi movl nbPommes(,%esi,4), %eax cmpl %eax, nbPommes jle less incl nbPommes less: movl $1, %eax movl $0, %ebx int $0x80 |
La première chose que vous devriez remarquer dans cette application est la section .data qui comporte une déclaration dont nous n’avons pas parlé jusqu’à maintenant. En fait, déclarer une variable sous cette forme cause en réalité l’utilisation de plusieurs cases mémoires contigues. Bref, c’est comme si on avait fait un tableau en C. Dans le cas actuel, ce tableau contient les chiffres 200 et 100. La deuxième instruction inconnue est movl nbPommes(,%esi,4), %eax. Cette instruction permettant d’atteindre une plage mémoire se décompose comme suit:
adresseDeBase ( décallage, index, taille)
L’adresse de base est l’adresse à partir de laquelle l’instruction doit travailler. Le décallage représente un déplacement en mémoire à partir de l’adresse de base. Dans notre cas, comme nous ne voulons pas de décallage ce paramètre est laissé vide. L’index représente l’index dans le tableau de l’élément recherché et la taille représente la taille en octets des données devant être obtenues. Dans le cas actuelle, l’instruction permet d’atteindre le chiffre “100”. Ce chiffre est copié dans le registre eax. Arrive enfin la série d’instructions qui permet de traiter la condition. J’ai recoppié le code dans l’encadré suivant.
cmpl %eax, nbPommes jle less incl nbPommes less: movl $1, %eax movl $0, %ebx int $0x80 |
L’opération cmpl permet de comparer deux nombres. Dans le cas présent, le nombre contenu par le registre eax (100) est comparré au nombre qui se trouve dans le premier index de la variable nbPommes. Avec l’assembleur GNU, la comparaison est toujours faite selon la relation du deuxième nombre face au premier nombre (un peu plus dans un instant). L’instruction suivante, jle less permet de “sauter” au code situé au label “less” si la condition est respectée. Dans le cas actuel, la condition est “jle” donc “Jump Less or Equals”. Ainsi, si le deuxième nombre fournis à l’opérateur de comparaison est plus petit que le premier nombre, le saut sera fait et l’exécution continuera à partir du label “less”. Dans cette situation, la ligne suivant immédiatement la commande jle ne serait pas exécutée. Le tableau suivant montre une table de vérité appliquée à l’opérateur.
cmpl |
%eax |
nbPommes |
Résultat |
100 |
200 |
> | |
200 |
100 |
< | |
100 |
100 |
= |
Vous pouvez donc voir qu’en réalité, la lecture se fait de droite à gauche en ce qui concerne le résultat de l’opération cmpl. Aussi, remarquez que en raison de la philosophie de fonctionnement du langage, nous vérifions en réalité le “contraire” de ce que nous vérifions dans la version C de l’application. Vous êtes fortement encourragé à exécuter cette petite application avec différentes valeurs afin de vous faire une idée du fonctionnement des opérateurs de comparaisons.
En terminant cet exemple, la figure suivante présente le code désassemblé de la version C du petit programme montré en exemple. Bien entendu, il y a quelques différences, mais les concepts de base vus dans le présent exemple y sont présents.
00000000004004b4 : 4004b4: 55 push %rbp 4004b5: 48 89 e5 mov %rsp,%rbp 4004b8: 8b 15 5a 0b 20 00 mov 0x200b5a(%rip),%edx # 601018 4004be: 8b 05 58 0b 20 00 mov 0x200b58(%rip),%eax #60101c 4004c4: 39 c2 cmp %eax,%edx 4004c6: 7e 0f jle 4004d7 4004c8: 8b 05 4a 0b 20 00 mov 0x200b4a(%rip),%eax # 601018 4004ce: 83 c0 01 add $0x1,%eax 4004d1: 89 05 41 0b 20 00 mov %eax,0x200b41(%rip) # 601018 4004d7: b8 00 00 00 00 mov $0x0,%eax 4004dc: 5d pop %rbp 4004dd: c3 retq 4004de: 90 nop 4004df: 90 nop |
Avant d’aller plus loin, comme les instructions jump sont très importantes dans tous traitements logiques, voici une table qui présente les opérateurs de saut les plus fréquents.
Opérateur |
Signification |
je |
Jump equals |
jg |
Jump greater |
jge |
Jump greater or equals |
jl |
Jump less |
jle |
Jump less or equals |
jmp |
unconditional jump, always taken |
jz |
Jump if “Zero” flag is set |
Plusieurs autres instruction jump existent. Nous en parlerons en temps et lieux si besoin est.
La suite de l’article présente la structure if then else et les boucles. L’analyse précise du code n’est pas présentée. Cependant, la version C de l’application est toujours présentée suivant la version assembleur pour vous aider à analyser vous même le code. La meilleure solution reste, et de loin, l’analyse du code dans un débugueur. Cependant, n’oubliez pas d’observer la variation de la valeur du registre eip (rip) pendant l’opération de débuguage.
L’exemple de code suivant reprend l’exemple précédent. Cependant, dans l’éventualité où le deuxième index du tableau n’est pas plus petit que le premier, le deuxième élément est incrémenté.
.section .data nbPommes: .int 99, 100 .section .text .globl _start _start: nop movl $1, %esi movl nbPommes(,%esi,4), %eax cmpl %eax, nbPommes jle less incl nbPommes jmp end less: incl %eax movl %eax, nbPommes(,%esi,4) end: movl $1, %eax movl $0, %ebx int $0x80 |
int nbPommes[2] = {200, 100}; int main(){ if (nbPommes[0] > nbPommes[1]){ nbPommes[0]++; }else{ nbPommes[1]++; } return 0; } |
Maintenant, puisque l’assemblage (jeu de mot...) des structures logiques en assembleur se ressemble beaucoup pour l’ensemble de celles-ci, voici maintenant un exemple de comment une boucle pourrait être implémentée en assembleur. Cette fois, le code C n’est pas disponible! À vous d’en faire l’analyse! Mais attention, celui-ci demandera peut-être un brin de recherche de votre part ;)
.section .data nbPommes: .int 99, 100 .section .text .globl _start _start: nop movl $4, %ecx movl $0, %eax loop: addl %ecx, %eax decl %ecx jz end jmp loop end: movl $1, %eax movl $0, %ebx int $0x80 |
Voilà! C’est ainsi que se termine la semaine 3 du mois de l’assembleur x86! D’ici la semaine prochaine, en cas de questionnement, n’hésitez surtout pas à nous contacter!