Cours
d'assembleur
NASM, un ami qui vous veut du bien.
NASM, un ami qui vous veut du bien.
- Sommaire
Préface
Introduction
Base de chiffres
Les processeurs
Les registres
Les mnémoniques
Le format ELF
Les syscalls
La Libc
Etude de cas
Spécifications
Déclaration des variables
Inclure un fichier
Adresses spéciales
Définition de constantes
Répétition d'instructions
Opérateurs
SEG et WRT
Annuler l'optimisation
Expressions critiques
Labels locaux
Définition de structures
Les comparaisons
Macros
Macros simples
Macros sur plusieurs lignes
Analyse de codes
Macro ENUM
Recup de argc
Code polymorphique
Debuggers
Ndisasm
GBD
Strace
Application
Conclusion
Références
Remerciements
Introduction
Base de chiffres
Les processeurs
Les registres
Les mnémoniques
Le format ELF
Les syscalls
La Libc
Etude de cas
Spécifications
Déclaration des variables
Inclure un fichier
Adresses spéciales
Définition de constantes
Répétition d'instructions
Opérateurs
SEG et WRT
Annuler l'optimisation
Expressions critiques
Labels locaux
Définition de structures
Les comparaisons
Macros
Macros simples
Macros sur plusieurs lignes
Analyse de codes
Macro ENUM
Recup de argc
Code polymorphique
Debuggers
Ndisasm
GBD
Strace
Application
Conclusion
Références
Remerciements
Je suis tombé sur un article écrit il y a plusieurs années sur la programmation assembleur. Je n'avais jamais pris le temps de le mettre sur mon blog mais je vais enfin pouvoir partager cette expérience
avec toi publique :).
Vous me direz, mais à quoi ça sert de savoir programmer en assembleur ? Et je vous répondrez que l'assembleur est un langage très proche du langage machine. De ce fait, en sachant programmer en assembleur vous comprendrez beaucoup mieux les mécanismes qui régissent le fonctionnement de votre cpu, et de ses périphériques.
D'un point de vu pratique nous allons, ensemble, apprendre l'assembleur pour, dans une suite d'articles prochains, comprendre les principes du reversing et peut-être commencer à programmer nos premiers virus.
Nous allons travailler sur un i386 (comme 95% des PC) sous linux (debian sid). J'ai préféré travailler avec NASM car il adopte la syntaxe intel ce qui, pour un passage d'os à windows, pourrait être pratique.
Avant de commencer je voudrais casser quelques mythes, l'assembleur n'est pas plus dur à assimiler que le C, on peut l'utiliser pour faire des programmes de haut niveau (sockets, environnement graphique ...). Si l'assembleur n'est pas aussi utiliser que le C c'est principalement dûe au fait que le code n'est pas portable et qu'il est plus fastidieux à créer.
Si vous êtes déjà familier avec le fonctionnement d'un cpu et que vous avez déjà les bases en assembleur, passez l'introduction.
Bien, nous pouvons maintenant rentrer dans le vif du sujet. Entrez dans mon monde.
Vous me direz, mais à quoi ça sert de savoir programmer en assembleur ? Et je vous répondrez que l'assembleur est un langage très proche du langage machine. De ce fait, en sachant programmer en assembleur vous comprendrez beaucoup mieux les mécanismes qui régissent le fonctionnement de votre cpu, et de ses périphériques.
D'un point de vu pratique nous allons, ensemble, apprendre l'assembleur pour, dans une suite d'articles prochains, comprendre les principes du reversing et peut-être commencer à programmer nos premiers virus.
Nous allons travailler sur un i386 (comme 95% des PC) sous linux (debian sid). J'ai préféré travailler avec NASM car il adopte la syntaxe intel ce qui, pour un passage d'os à windows, pourrait être pratique.
Avant de commencer je voudrais casser quelques mythes, l'assembleur n'est pas plus dur à assimiler que le C, on peut l'utiliser pour faire des programmes de haut niveau (sockets, environnement graphique ...). Si l'assembleur n'est pas aussi utiliser que le C c'est principalement dûe au fait que le code n'est pas portable et qu'il est plus fastidieux à créer.
Si vous êtes déjà familier avec le fonctionnement d'un cpu et que vous avez déjà les bases en assembleur, passez l'introduction.
Bien, nous pouvons maintenant rentrer dans le vif du sujet. Entrez dans mon monde.
- Base de chiffres :Il y a la base décimale, celle que nous connaissons tous : en base 10 (de 0 à 9)
La base binaire : en base 2 (de 0 à 1)
110012 = 1 x 24 + 1 x 23 + 0 x 22 + 0 x 21 + 1 x 20
= 16 + 8 + 1 = 25
On appel chaque “chiffre”, 0 ou 1 dans le cas présent, un bit (à ne pas confondre avec Byte qui est un octet).
La base héxadécimale : en base 16 (de 0 à F)
On compte ainsi en héxadécimal.
0=0; 1=1; 2=2; 3=3; 4=4; 5=5; 6=6; 7=7; 8=8; 9=9; A=10; B=11; C=12; D=13; E=14; F=15.
2AF16 = 2 x 162 + 10 x 161 + 15 x 160 = 512 + 160 + 15 = 687
L'intérêt de l'héxadécimal est qu'il permet de représenter plus simplement des valeurs binaires. Comme le binaire est en base 2 et l'héxadécimal en base 16 il est simple de représenter 4 bits (24 = 16).
Par exemple : 1110 1001 0110 1110
E 9 6 E
Il faut également savoir qu'un ensemble de 8 bits s'appel un octet. De là vos fichiers en ko, Mo, Go. - Les processeurs :Un processeur est un composant électrique composé, pour ce qui nous intéresse, de registres, pour stocker des informations temporairement. D'une horloge envoyant des pulsations toutes les microsecondes ou nanosecondes. Lorsque vous achetez un processeur de type 1Ghz cela représente la vitesse de cette horloge ; pour savoir en secondes la durée d'un cycle de votre horloge faîtes 1/f où f est la fréquence (soit 1/1e9 = 1 ns pour notre cas). Les autres composants électronique se basent sur cette horloge pour effectuer leurs tâches.
Parlons maintenant de l'organisation de la mémoire – non, pas celle de votre disque dur ; la mémoire du processeur et dans certains cas de la RAM –. Chaque octet est repéré par un nombre unique dans la mémoire de votre processeur. Mais le processeur traitant des quantités incroyable de données, il ne traite que rarement de simples octets. C'est pour cela que l'on a nommé des ensembles d'octets.word2 octetsdouble word4 octetsquad word8 octetsparagraph16 octetsToutes les données en informatique sont des nombres. Par exemple les caractères sont sous forme de chiffre. On a seulement créé une table de correspondance entre les valeurs décimales et les caractères.
Actuellement, la plus grande différence entre les différents types de processeurs se situe donc au niveau de la taille des registres et de la rapidité de l'horloge mais pas seulement. - Les registres :Nous travaillerons, comme je l'ai déjà dit, sur un processeur de type 80386. Ce qui veut dire que les registres auront une taille de 32 bits.
Les registres sont des espaces mémoires nous permettant de stocker des valeurs de 32 bits. Grâce à eux nous pourrons accéder à une donnée précise, récupérer la valeur de retour d'une fonction, passer des arguments à une fonction, etc...
Ils sont organisés de cette façon :EAXEBXECXEDXregistre généralregistre généralregistre généralregistre généralESIEDIEBPEIPESPoffset mémoireoffset mémoireoffset mémoire gardant l'adresse de la fonctionoffset mémoire du codeoffset mémoire de la pileLes registres généraux servent de foure-tout. Tandis que les registres d'offset pointent généralement sur une adresse mémoire utile. Vous apprendrez au fur et à mesure quel registre utiliser pour quoi faire. Il existe d'autres registres plus spécifiques. - La pile :Sur les processeurs de type x86, la pile est un outil permettant de stocker des données de façon temporaire car rapide d'accès. La pile est dite LIFO (Last In First Out), ce qui veut dire que la première chose mise dans la pile sera la dernière chose enlevée de la pile. C'est donc pour cela que l'on a l'habitude de s'imaginer la pile comme des assiettes empilées les unes sur les autres, où chaque assiette représenterait une donnée, puis que l'on désempilerait de haut en bas.
Les mnémoniques pour empiler et désempiler sont respectivement “push” et “pop”. Un petit code pour illustrer ça :push val1 ; val1 = 10 push val2 ; val2 = 20 pop val1 ; val1 = 20 pop val2 ; val2 = 10
Vous verrez plus loin que la pile est un outil indispensable dans la programmation assembleur. - Les mnémoniques :Toute instruction NASM peut être résumée ainsi :
label : mnémonique opérandes ;commentaire
Il n'est pas obligatoire de retrouver tous les éléments sur la même ligne. Les mnémoniques représentent les instructions assembleur, ce qui nous permet de programmer. Par exemple.addition: add eax,4 ; j'ajoute 4 à eax et je stock le résultat dans eax
Il existe beaucoup de mnémoniques, pour un listing complet allez voir le manuel de NASM.
- Le format ELF :Sous linux les éxécutables ont le format ELF pour Executable and Linking Format. Ce format offre un découpage modulaire de l'en-tête de l'éxécutable sous cette forme : (Représentation très simplifiée)
ELF Header Program Header Table Segment #1 Segment #2 . . . Section Header Table Section 1 . . . Section n
Nous utiliserons trois segments dans notre code :
.data.bss.textSert aux variables de tailles fixes. Variables initialisées. Sert aux variables de tailles inconnues. Variables non initialisées. C'est là que se trouve le code du programme.
cat /usr/include/elf.h
- Les
syscall :En assembleur sous linux, il existe une mnémonique permettant de faire appel au noyau. C'est à dire que grâce à cette mnémonique on peut demander au noyau d'effectuer une action pour nous. La mnémonique en question se nomme int pour “interruption”. Pour faire appel au kernel il faut lui passer l'argument 0x80.Nous verrons son utilisation dans un code expliquer et commenter.
Pour une liste assez exhaustive des différents syscalls : http://www.lxhp.in-berlin.de/lhpsyscal.html
- La
libc :Il est également possible d'utiliser les fonctions de la libc sous certaines conditions. Cela nous permet de faire pas mal de choses simplement.
- Etude
de cas :Nous allons étudier là deux petits programmes tout ce qu'il y a de plus banal, j'ai nommé des “hello world”.
Le premier utilisant les syscalls. Le syscall permettant d'écrire (write) est le 4.
Il prend en paramètres :
edx : la longueur de la chaîne à affichée
ecx : un pointeur sur le début de la chaîne
ebx : le handle où l'on veut écrire (en l'occurence 1 pour l'écran)
eax : le numéro du syscall à appeler
; NASM -f elf hello_world.asm ; ld hello_world.o -o hello_world segment .data hello db "Hello World !", 0xa ; 0xa équivaut à \0 len equ $ - hello ; taille de la chaîne (strlen(hello)) segment .text global _start _start: mov edx, len ; edx = longueur de la chaîne a afficher mov ecx, dword hello ; ecx pointe sur l'adresse du début de la chaîne mov ebx, 1 ; file handle, ou l'on écrit (STDOUT) mov eax, 4 ; sys_write int 0x80 ; call kernel mov eax,1 ; sys_exit xor ebx,ebx ; ebx = 0 (soit exit(0) en c) int 80h ; call kernel
Au début on défini les variables dans les bons segments (rappelez-vous, variables de longueurs fixes dans data ; variables de longueurs non définies dans bss). len contient la taille de la chaîne hello, nous verrons plus tard ce que représente la ligne qui définie cette variable. global sert à définir le point d'entrée du code (_start quand on compile avec ld et main quand on compile avec gcc). Ensuite le remplissage des paramètres pour appeler sys_write. dword est un mot clé pour dire que la variable est de type doubleword. Puis on quitte proprement avec un sys_exit.
Le second utilisant la fonction printf() de la libc.Pour utiliser la libc il faut lier le .o avec gcc. Pour appeler une fonction de la libc il faut empiler les arguments dans la pile (push) dans l'ordre inverse qu'ils sont demandés par la fonction. Un exemple :int fprintf(FILE *stream, const char *format, ...);
Il faudra donc empiler le format en premier puis le stream soit :
push format push stream call fprintf pop eax ; valeur de retour de la fonction
extern permet de dire au compilateur que l'on va appeler une fonction extérieure à notre programme.
Il faut nettoyer la stack après chaque appel à une fonction de la libc.
; NASM -f elf hello_printf.asm ; gcc hello_printf.o -o hello_printf global main extern printf ; on déclare la fonction printf comme externe section .data msg db "Hello, World",0Dh,0Ah,0 section .text main: push dword msg ; pointe vers l'adresse du début de la chaîne à afficher call printf ; printf() pop eax ; on nettoie la stack ret
Dans la programmation NASM les mot-clés du type dword, byte ... servent à spécifier que l'on pointe sur l'adresse de la variable et non pas sur son contenu. Comme dans notre cas :
dword msg
- Spécifications
Nous allons maintenant apprendre les bases nécessaires pour comprendre un code NASM : la signification des mots clés, la déclaration des variables, l'étude de quelques mnémoniques ...
- Déclaration
des variables :
Comme nous l'avons déjà vu, il existe deux grands types de variables en assembleur : les variables non-initialisées et les variables initialisées déclarées respectivement dans les sections .bss et .data. NASM nous permet de définir à l'aide de lettres la taille de l'espace mémoire que nous allouons à ces données.
La section .data :
db 0x55 ; un octet : 0x55 db 0x55, 0x56, 0x57 ; trois octets db 'a', 0x55 ; deux octets (a = 0x41) db 'hello', 13, 10,'$' ; ça marche aussi avec les chaînes de caractères dw 0x1234 ; deux octets : 0x34 0x12 dw 'a' ; 0x41 0x00 dw 'abc' ; 0x41 0x42 0x43 0x00 dd 0x12345678 ; quatre octets : 0x78 0x56 0x34 0x12 dd 1.234567e20 ; nombre à virgule de précision float dq 1.234567e20 ; nombre à virgule de double précision float dt 1.234567e20 ; nombre à virgule de précision float étendue
La précision des nombres de type “float” désigne le nombre de chiffres, après la virgule, gardés en mémoire.
La section .bss :
resb 255 ; REServe 255 octets resb 1 ; REServe 1 octets resw 1 ; REServe 1 Word (1 Word = 2 octets ) resd 1 ; REServe 1 Double word (soit 4 octets) resq 1 ; REServe 1 float à double précision rest 1 ; REServe 1 float à précision étendue
- Inclure
un fichier :
Le mot réservé INCBIN permet d'inclure un fichier binaire dans votre source.
incbin "file.dat" ; inclue un fichier entier incbin "file.dat", 1024 ; inclue le fichier sans les 1024 premiers octets incbin "file.dat", 1024,512 ; inclue le fichier sans les 1024 premiers octets ; et jusqu'à 512 octets
- Adresses
spéciales :Les jetons $ et $$ désignent eux
des adresses spécifiques : $ représente l'adresse de
l'instruction par rapport au début du code et $$ l'adresse de
l'instruction par rapport au début de la section. Ainsi pour
créé une boucle infinie ou pourrait faire
jmp $
- Définition
de constantes : EQU :EQU permet de définir une
constante dont la valeur ne pourra donc pas changée. Par
exemple :
message db 'hello, world' msglen equ $-message
msglen représente la taille de message soit 12 octets.
- Répétition
d'instructions : TIMES :
Le préfixe TIMES fait que l'instruction est assemblée plusieurs fois. L'avantage de cette instruction est qu'elle peut permettre d'effectuer plusieurs fois un ensemble d'action plus ou moins complexes.
buffer: db 'hello, world' times 64-$+buffer db ' '
Cet exemple va donc réserver 64 octets pour y stocker buffer.
On peut utiliser TIMES pour répéter n'importe quelle mnémonique.
- Opérateurs
:NASM, tout comme le C accepte des opérateurs
arithmétiques forts utiles. A la différence que les
opérateurs Nasm ne sont utilisables que dans le préprocesseur
(calcul d'adresse, macros ...), cf : expressions critiques.
| : OR
^ : XOR
& : AND
<< et >> : Décalage binaire
+ et - : addition, soustraction
*, /, //, % et %%+, -, ~ : opérateurs s'applicant sur un argumentTout comme en C et en électronique, cet opérateur effectue un OR bit par bit à vos données. Tout comme en C et en électronique, cet opérateur effectue un XOR bit par bit à vos données. Tout comme en C et en électronique, cet opérateur effectue un AND bit par bit à vos données. Comme en C cet opérateur permet de copier le nombre de bits (spécifier par la deuxième opérande) de la première opérande en commençant par le bit de poid faible. Tout comme en C, ces opérandes permettent de faire une addition ou une soustraction. * permet de faire une multiplication
/ permet de faire une division non signée
// permet de faire une division signée
% permet de faire un modulo non signé
%% permet de faire un modulo signéLorsque vous mettez un + ou un – devant une seul variable vous rendez respectivement la variable en signe positif ou en signe négatif.
~ quant à lui agit comme une porte NOT en électronique.
Comme il est essentiel de comprendre le fonctionnement de ces opérateurs nous allons développer un peu les opérateurs agissant au niveau binaire :
L'opérateur OR.Bit 1Bit 2Bit 1 | Bit 2000011101111
L'opérateur AND.Bit 1Bit 2Bit 1 & Bit 2000010100111
L'opérateur XOR.Bit 1Bit 2Bit 1 ^ Bit 2000011101110
L'opérateur NOT.Bit~Bit0110
L'opérateur de décalage binaire.
Prenons l'exemple de 42 << 2 :42 en binaire.00101010Le résultat de 42 << 2 soit 168 en décimal.0010101000 - SEG
et WRT :Dans un
programme en assembleur, il existe différents types de
segments, dont les plus connues sont CS/DS/SS pour Code Segment,
Data Segment et Stack Segment. Ceux-ci permettent d'atteindre des
adresses lointaines. Ainsi en mode 16 bits, ces segments sont
éparpillés car ne pouvant être placés
dans la même zone mémoire tandis qu'en mode 32 bits,
les segments sont accessibles dans la même zone mémoire.
SEG et WRT permettent d'accéder à l'adresse d'une donnée lointaine. Ce qui n'est que très rarement utile en mode 32 bits. Les seuls différences de ces mnémoniques sont au niveau de la syntaxe et au niveau du résultat obtenu.
Quelques exemples :
mov ax, seg donnee mov es, ax mov bx, donnee ; ES:BX contient un pointeur valide vers donnee
La mnémonique SEG récupère donc l'offset de donnee. Pour cela elle utilise un “base segment” par défaut pour obtenir un offset exploitable.
Ce que l'on peut spécifier grâce à la mnémonique WRT :
mov ax, base_segment mov es, ax mov bx, donnee wrt base_segment ; ES:BX contient un pointeur different mais tout aussi fonctionnel vers donnee
Ici base_segment représente donc le “base segment” :).
NASM permet de faire des appels et des sauts vers des labels lointains sous la forme :
call segment:offset ; ou segment et offset sont des valeurs numériques
On peut donc à l'aide de SEG et WRT réaliser le calcul immédiatement :
call (seg procedure):procedure ; les parentheses ne sont call weird_seg:(procedure wrt weird_seg) ; pas obligatoire en pratique.
- Annuler
l'optimisation : STRICT :Lorsque l'on passe à NASM
l'argument “-On”, où n est un chiffre spécifiant
le niveau d'optimisation, NASM effectue une optimisation du code. Il
est possible que pour certaines instructions vous ne vouliez pas que
l'optimisation ai lieu. C'est pour cela que STRICT a été
inventé.
Par exemple avec l'optimisation :
push dword 33 ; 66 6A 21 push strict dword 33 ; 66 68 21 00 00 00
- Expressions
critiques :Une limitation de NASM est qu'il assemble le code
en deux phases minimum (sans optimisation) ; pas comme tasm ou
d'autres. La première phase (préprocesseur) consiste
pour NASM à récupérer la taille globale du
programme, des segments de données ... Ainsi lors de la
deuxième phase (runtime), la génération de
l'exécutable, NASM connaît toutes les adresses des
données dont fait référence le code
source.
Donc ce que NASM ne peux prendre en compte sont les instructions du code source faisant référence à une donnée déclarée après.
Par exemple :
times (label-$) db 0 label: db 'Where am I ?'
NASM ne pourra compiler ce code car la ligne déclarant label est après celle lui allouant de l'espace mémoire.
Ce concept a été appelé expression critique. Il existe différentes instructions qui peuvent être restreintent par ces expressions critiques tel que TIMES, RES*, EQU ...
- Labels
locaux :
Le point permet à NASM de déclarer des labels locaux, comme on le voit dans l'exemple. En plus, NASM permet de faire référence à un label en-dehors du domaine local.
label1 ; some code .loop ; some more code jne .loop ret label2 ; some code .loop ; some more code jne .loop ret label3 ; some code ; some more code jmp label1.loop
Ainsi le premier jne .loop ira directement à label1.loop, le deuxième jne .loop ira à label2.loop et le dernier jmp label1.loop ira au début du code (label1.loop).
- Définition
de structures : STRUC :Pour définir une structure on
fait appel au préprocesseur par le biais du mot-clé
STRUC. Il ne prend qu'un argument. STRUC n'est qu'une macro qui
effectue en réalité plusieurs EQU.
struc mytype .long: resd 1 .word: resw 1 .byte: resb 1 .str: resb 32 endstruc
On déclare donc la structure mytype avec des variables locales de types différents : mytype.long, mytype.word, mytype.byte et mytype.str.
- Les
comparaisons : CMP :Les langages de programmation habituels
tels que le C utilisent des structures de contrôle du flux
d'éxécution du programme (if, while, switch) ce qui
n'existe pas en assembleur. Pour pallier à cela on a recoure
à la mnémonique CMP qui effectue une simple
soustraction et stock le résultat dans un registre spécial,
le registre FLAGS.
Pour les entiers non-signés il y a deux bits importants dans le registre FLAGS : le zero (ZF) et la portée (CF)
cmp vleft, vright
La soustraction entre vleft et vright est effectuée et
- si vleft = vright, alors ZF est mis à 1 et CF est mis à 0
- si vleft > vright, alors ZF est mis à 0 et CF est mis à 0
- si vleft < vright, alors ZF est mis à 0 et CF est mis à 1
Pour les entiers signés il y a trois bits importants dans le registre FLAGS : le zero (ZF), la surcharge (OF) et le signe (SF)
- si vleft = vright, alors ZF est mis à 1
- si vleft > vright, alors ZF est mis à 0 et SF = OF
- si vleft < vright, alors ZF est mis à 0 et SF != OF
Les valeurs que prennent ces bits servent ensuite à éguiller le flux d'éxécution du code à l'aide de mnémoniques d'aiguillage dont voici un bref tableau récapitualtif :
JZ
JNZ
JO
JNO
JS
JNS
JC
JNC
JP
JNPCommute seulement si ZF = 1
Commute seulement si ZF = 0
Commute seulement si OF = 1
Commute seulement si OF = 0
Commute seulement si SF = 1
Commute seulement si SF = 0
Commute seulement si CF = 1
Commute seulement si CF = 0
Commute seulement si PF = 1
Commute seulement si PF = 0
L'instruction JMP saute quelque soit la valeur du registre FLAGS. On peut spécifier une certaine portée à nos mnémoniques d'aiguillage pour minimiser la taille du code par exemple :
- SHORT permet de sauter à 128 octets vers le haut ou vers le bas du code.
- NEAR permet de sauter n'importe où dans le segment courant.
- FAR permet de sauter dans n'importe quel autre segment.
De plus les processeurs de type 80x86 offrent la possibilités de rendre ces tests plus simples par le biais de mnémoniques d'aiguillage plus adaptées :
Entiers signésEntiers non signésJE Commute si vleft = vright
JNE Commute si vleft != vright
JL, JNGE Commute si vleft < vright
JLE, JNG Commute si vleft <= vright
JG, JNLE Commute si vleft > vright
JGE, JNL Commute si vleft >= vrightJE Commute si vleft = vright
JNE Commute si vleft != vright
JB, JNAE Commute si vleft < vright
JBE, JNA Commute si vleft <= vright
JA, JNBE Commute si vleft > vright
JAE, JNB Commute si vleft >= vright
Et pour finir un petit bout de code exemple :
cmp eax, 0xffffffff ; si eax != -1 jne thenblock ; on saute vers thenblock mov DWORD PTR [esp], 0x1 ; on empile les arguments pour le call call ptrace ; on appel la fonction jmp next ; on passe à la suite thenblock : mov DWORD PTR [esp], 0x8048564 ; on empile d'autres arguments call ptrace ; on appel la fonction next : ; on continue l'éxécution
Il ne faut pas oublier que le registre FLAGS est modifiable via d'autres mnémoniques.
-
NASM comporte un outil très puissant, j'ai nommé le processeur de macros. Cet outil nous permet de définir des instructions plus ou moins complexes réutilisables de façon assez simple une fois que l'on a maîtriser la syntaxe. Dans ce chapitre nous allons apprendre à utiliser les macros déjà disponibles et à créer nos macros.
- Les
macros simples :
Les macros simples permettent par le biais du préprocesseur de définir de nouvelles macros tout comme en C. Le mot clé permettant cette prouesse est “%define” et ses dérivés.
%define ctrl 0x1F & ; définition de ctrl = 0x1F & %define param(a,b) ((a)+(a)*(b)) ; définition de param() mov byte [param(2,ebx)], ctrl 'D' ; = mov byte [(2)+(2)*(ebx)], 0x1F & 'D'
La macro %define ne subit pas la limitation des expressions critiques.
%define a(x) 1+b(x) %define b(x) 2*x mov ax, a(8) ; mov ax, 1+2*8
Les noms des macros sont sensibles à la casse. C'est pour cela qu'il y a “%idefine” qui permet de définir un ensemble de noms pour la macro.
%define foo bar ; foo %idefine foo bar ; foo, FOO, fOo, foO, ...
Il est également possible de surcharger les arguments d'une macro :
%define foo(x) 1+x %define foo(x, y) 1+x*y mov ax, foo(4) ; ax = 1+4 mov ax, foo(4,5) ; ax = 1+4*5
Parlons un peu de “%xdefine”. Cette instruction permet de déclarer une macro en fonction de son contexte et non pas comme “%define” qui elle déclare une macro en fonction de la valeur au moment de l'appelle à celle-ci. Un exemple :
%define isTrue 1 %define isFalse isTrue %define isTrue 0 val1: db isFalse ; val1 = 0 %define isTrue 1 val2: db isFalse ; val2 = 1
%xdefine isTrue 1 %xdefine isFalse isTrue %xdefine isTrue 0 val1: db isFalse ; val1 = 1 %xdefine isTrue 1 val2: db isFalse ; val2 = 1
Dans le cas de “%define”, val1 prend la valeur 0 car lorsque l'on appel la macro “isFalse”, “isTrue” vaut 0 ; de même pour val2.
Pour “%xdefine” val1 vaut 1 car lorsque l'on déclare “isFalse”, “isTrue” vaut 1. Ainsi “isFalse” vaudra toujours 1 quelque soit la valeur de “isTrue“ plus loin dans le code.
Il existe une autre instruction pour créer des macros : “%assign”. Celle-ci n'est utilisée que pour les macros d'une ligne ne prenant aucuns arguments et de valeur numérique. On la retrouve souvent lorsqu'il faut effectuer une incrémentation ou décrémentation dans une macro.
Pour détruire une macro on utilise le mot clé “%undef”
- Les
macros sur plusieurs lignes :
La définition d'une macro de plusieurs lignes passe par le mot-clé “%macro” avec cette syntaxe :
%macro nom_de_macro nb_arguments ; un peu de code %endmacro
Où nom_de_macro est le nom de la macro et nb_arguments un nombre représentant le nombre d'arguments passés à la macro. Un exemple :
%macro silly 2 %2: db %1 %endmacro
Où %2 représente le deuxième argument et %1 le premier argument.
silly 'hello_world', hello ; hello: db 'hello_world'
De plus on peut déclarer un intervalle d'arguments et des valeurs par défaut :
%macro silly 2+ ; 2 arguments ou plus %endmacro %macro foobar 1-3 eax, [ebx+2] ; au minimum 1 argument ; et pas plus de 3 arguments %endmacro
Pour la macro foobar, lorsque les arguments %2 et %3 ne sont pas déclarés, %2 = eax et %3 = [ebx+2].
L'astérisque (*) permet de déclarer un nombre infini d'arguments.
L'argument %0 est le nombre d'arguments passés à la macro.
L'instruction %rotate permet de faire tourner les arguments de la macro de x vers la gauche ou la droite respectivement si x est positif ou si x est négatif.
%macro multipush 1-* ; 1 arguments minimum %rep %0 ; boucle tant qu'il y a des arguments push %1 ; on push l'argument courant %rotate 1 ; on passe à l'argument suivant %endrep %endmacro
- Analyse
de Code
Maintenant que nous possédons les notions théoriques essentielles (et un peu plus quand même :D) nous allons étudier quelques codes nasm. Histoire de voir à quoi cela peut nous servir en pratique.
- Macro
ENUM :
Un petit code source de mammon_ tiré du Assembly Programming Journal. Cette macro permet de créer une énumération de variables associées à des valeurs numériques. Tout comme le fait le mot-clé enum en C.
;Summary: A NASM macro emulating the C 'ENUM" command ;Assembler: NASM ;by mammon_ && modified by Flyers %macro ENUM 2-* ;Usage: ENUM int SYMBOLS %assign i %1 ; where int is the number to begin enumeration at [0] %rep %0 -1 ; SYMBOLS is a list of Symbols to define %2 EQU i ;Example: ENUM 0, TRUE, FALSE %assign i i+1 ; this EQUates TRUE to 0 and FALSE to 1 %rotate 1 ;Example: ENUM 11, JACK, QUEEN, KING %endrep ; this EQUs JACK to 11, QUEEN to 12, KING to 13 %endmacro
Ce code ne devrait pas vous être si dur à assimiler maintenant mais je vais vous aider.
On commence par créer la macro avec au minimum deux paramètres.
On initialise i à la valeur du premier élément à énumérer (soit le premier argument de la macro).
On attribue ensuite à chaque argument (-1 pour ne pas prendre en compte le premier argument) la valeur de i que l'on incrémente. On peut résumer cela par une boucle “for ( i = %1; i < %0 -1; i++)”.
- Recup de argc :
Nous allons étudier un bout de code qui s'occupe de récupérer argc (vous savez c'est la variable contenant le nombre d'arguments passés au programme en ligne de commande), qui le transforme en caractère et qui l'affiche à l'écran.
Avant de commencer il faut que vous ayez quelques connaissances indispensables :
- La pile, lorsqu'un programme est lancer sous linux, ressemble à ça :argc[dword] compteur d'arguments (integer)argv[0][dword] nom du programme (pointer)argv[1]...argv[argc-1][dword] arguments du programme (pointers)NULL[dword] fin des arguments (integer)env[0]env[1]...env[n][dword] variables d'environnement (pointers)NULL[dword] fin des variables d'environnement (integer)
- En informatique on a créé des tables de caractères qui sont des correspondances entre des caractères et des valeurs numériques. La plus vieille est la table de caractère ASCII. On remarque dans celle-ci que le nombre 0 est représenter par la valeur 48 en décimal soit 0x30 en héxadécimal. C'est donc en ajoutant 0x30 à la valeur numérique que j'obtiens la string correspondante. Il y a un problème majeur à cette astuce : je ne peux afficher que les valeurs ASCII des nombres allant de 0 à 9 seulement. Donc le programme souffre d'un bug connu (dommage pour les bidouilleurs qui auraient cru faire une trouvaille).
- Il faut se souvenir que le registre esp pointe sur le haut de la pile.
Voila, maintenant le code en question commenter et j'espère compréhensible :
; nasm -f elf argc.asm ; ld argc.o -o argc ; pour ne pas passer par la libc ; gcc argc.o -o argc ; pour utiliser la libc ; by Flyers segment .text: global _start ; gcc : global main _start: ; gcc : main: pop eax ; ld : argc ; gcc : valeur de retour de la libc ; pop eax ; gcc : argc ; pop eax ; argv[0] ; pop eax ; the first real arg push 0x0A30 ; on push 30 pour convertir argc en ASCII ; et 0A pour la fin de chaîne add [esp],eax ; on ajoute ce que contient la stack à argc mov edx, 2 ; on affichera 2 bytes (argc+\0) mov ecx, esp ; la chaîne à afficher mov ebx,1 ; file handle, ou l'on écrit (STDOUT) mov eax,4 ; sys_write int 80h ; call kernel ; pour éviter que le programme ne segfault à la fin on appel sys_exit mov eax,1 xor ebx,ebx int 80h
- Code
s'auto-modifiant :
Un code s'auto-modifiant est un code capable de se modifier lui même lors de l'éxécution. L'étude d'un tel code peut être une bonne introduction à la programmation de virii polymorphiques. Pour créé un code s'auto-modifiant, il faut être capable de jongler entre les adresses des différentes instructions car on ne peut spécifier d'adresse statique lorsque le programme est en éxécution, on utilise donc des adresses relatives comme vous allez le voir.
Karsten Scheibler a écrit un très bon article dont voici une brève traduction :
L'idée principale : il existe un syscall, sys_mprotect, qui permet de modifier les flags pour (presque) toutes les pages. Une page est la plus petite unité dans la gestion de la mémoire virtuelle. Sur les processeurs x86, la taille de la page est de 4Ko. Mais il n'est pas nécessaire de faire appel à ce syscall pour donner à la section .bss les droits en éxécution car sur les processeurs x86, une page avec les droits de lecture a également les droits en éxécution et la section .bss est en lecture/écriture. Mais ce syscall risque de devenir obsolète avec l'apparition du NX-flag sur les nouveaux processeurs.
Les deux premiers exemples copient un bout de code dans la section .bss puis l'éxécute. Parce que nous avons les droits en lecture/écriture/éxécution dans cette zone mémoire, le programme peut s'auto-modifier. Le premier exemple (code1_start) copie un simple hello_world (utilisant sys_write), mais avant de l'éxécuter nous modifions quelques valeurs dans le code (le début et la taille de la chaîne à afficher). Le deuxième (code2_start) effectue une vraie auto-modification. L'instruction “rep stosb” écrase les quatre premiers “inc ebx” avec des “nop”, ainsi la chaîne à afficher à l'écran contient 04h au lieu de 08h attendu. Le troisième exemple (endless), quant à lui, modifie du code dans la section .text en y ajoutant les droits en lecture/écriture/éxécution via sys_mprotect.
Note : Si vous voyez un 08h au lieu de 04h à l'écran, vous devriez connaître un drôle de comportement des codes s'auto-modifiants. Sur les processeurs clone du Pentium, la modification de la queue n'est pas prise en compte tout de suite. Il faut en quelque sorte rafraîchir la queue, pour cela un simple jmp suffit (essayez avec un “jmp” juste après le “rep stosb”).
;**************************************************************************** ;**************************************************************************** ;* ;* USING SELF MODIFYING CODE UNDER LINUX ;* ;* written by Karsten Scheibler, 2004-AUG-09 ;* ;**************************************************************************** ;**************************************************************************** global _start ;**************************************************************************** ;* some assign's ;**************************************************************************** %assign SYS_WRITE 4 %assign SYS_MPROTECT 125 %assign PROT_READ 1 %assign PROT_WRITE 2 %assign PROT_EXEC 4 ;**************************************************************************** ;* data ;**************************************************************************** section .bss alignb 4 modified_code: resb 0x2000 ;**************************************************************************** ;* smc_start ;**************************************************************************** section .text _start: ;calcul l'adresse de la section .bss, elle doit se trouvée entre ;deux pages (x86: 4KB = 0x1000) ;NOTE: Dans cet exemple c'est inutile car chaque segment est aligné ; en fonction des pages et nous ne l'utilisons qu'une fois, ; donc nous savons qu'il est entre deux pages, mais si vous avez ; plus d'une section .bss dans votre programme vous ne pouvez ; en être sûr. mov dword ebp, (modified_code + 0x1000) and dword ebp, 0xfffff000 ;change les flags de la section en lecture/écriture/éxécution, ;NOTE: Sur les processeurs x86 cela est inutile comme cela a ; déjà été vu plus haut. mov dword eax, SYS_MPROTECT mov dword ebx, ebp mov dword ecx, 0x1000 mov dword edx, (PROT_READ | PROT_WRITE | PROT_EXEC) int byte 0x80 test dword eax, eax js near smc_error ;éxécute le code non modifié code1_start: mov dword eax, SYS_WRITE mov dword ebx, 1 mov dword ecx, hello_world_1 code1_mark_1: mov dword edx, (hello_world_2 - hello_world_1) code1_mark_2: int byte 0x80 code1_end: ;copie le code vers la bonne page (dont l'adresse est encore dans ebp) mov dword ecx, (code1_end - code1_start) mov dword esi, code1_start mov dword edi, ebp cld rep movsb ;on y ajoute le code héxa de l'instruction 'ret', comme cela ;nous pourrons utiliser call mov byte al, [return] stosb ;change quelques valeurs dans le code: l'adresse de début du texte ;et sa taille mov dword eax, hello_world_2 mov dword ebx, (code1_mark_1 - code1_start) mov dword [ebx + ebp - 4], eax mov dword eax, (hello_world_3 - hello_world_2) mov dword ebx, (code1_mark_2 - code1_start) mov dword [ebx + ebp - 4], eax ;finalement on l'appel call dword ebp ;copie le deuxième exemple mov dword ecx, (code2_end - code2_start) mov dword esi, code2_start mov dword edi, ebp rep movsb ;fait quelque chose de vraiment méchant: edi pointe juste après ;l'instruction 'rep stosb', donc cela va réellement modifier le code mov dword edi, ebp add dword edi, (code2_mark - code2_start) call dword ebp ;modifie le code dans la section .text endless: ;ajoute les droits à la section .text pour la modifiée mov dword eax, SYS_MPROTECT mov dword ebx, smc_start and dword ebx, 0xfffff000 mov dword ecx, 0x2000 mov dword edx, (PROT_READ | PROT_WRITE | PROT_EXEC) int byte 0x80 test dword eax, eax js near smc_error ;affiche le message à l'écran mov dword eax, SYS_WRITE mov dword ebx, 1 mov dword ecx, endless_loop mov dword edx, (hello_world_1 - endless_loop) int byte 0x80 ;ici, les instructions empêchant la boucle sans fin mov dword ecx, (smc_end_1 - smc_end) mov dword esi, smc_end mov dword edi, endless rep movsb ;et on recommence jmp short endless ;**************************************************************************** ;* code2 ;**************************************************************************** ;ici les adresses des instructions dont on rajoute les ;codes héxa dans notre code return: ret no_operation: nop ;ici du vrai code s'auto-modifiant, s'il est bien ;copier dans .bss et edi bien charger, ebx doit contenir ;0x4 au lieu de 0x8 code2_start: mov byte al, [no_operation] xor dword ebx, ebx mov dword ecx, 0x04 rep stosb code2_mark: inc dword ebx inc dword ebx inc dword ebx inc dword ebx inc dword ebx inc dword ebx inc dword ebx inc dword ebx call dword [function_pointer] ret code2_end: align 4 function_pointer: dd write_hex ;**************************************************************************** ;* write_hex ;**************************************************************************** write_hex: mov byte bh, bl shr byte bl, 4 add byte bl, 0x30 cmp byte bl, 0x3a jb short .number_1 add byte bl, 0x07 .number_1: mov byte [hex_number], bl and byte bh, 0x0f add byte bh, 0x30 cmp byte bh, 0x3a jb short .number_2 add byte bh, 0x07 .number_2: mov byte [hex_number + 1], bh mov dword eax, SYS_WRITE mov dword ebx, 1 mov dword ecx, hex_text mov dword edx, 9 int byte 0x80 ret section .data hex_text: db "ebx: " hex_number: db "00h", 10 ;**************************************************************************** ;* some text ;**************************************************************************** endless_loop: db "No endless loop here!", 10 hello_world_1: db "Hello World!", 10 hello_world_2: db "This code was modified!", 10 hello_world_3: ;**************************************************************************** ;* smc_error ;**************************************************************************** section .text smc_error: xor dword eax, eax inc dword eax mov dword ebx, eax int byte 0x80 ;**************************************************************************** ;* smc_end ;**************************************************************************** section .text smc_end: xor dword eax, eax xor dword ebx, ebx inc dword eax int byte 0x80 smc_end_1: ;*********************************************** linuxassembly@unusedino.de *
Vous avez toutes les connaissances théoriques pour comprendre le fonctionnement du code en général. Si vous voulez le comprendre plus en détail, allez voir les mnémoniques que vous ne comprenez pas dans le manuel de nasm ou essayez de calculer les adresses relatives en imaginant que les labels représentent des adresses fixes.
- Debuggers
Pour découvrir les sources de bugs dans un code compiler on utilise des debuggers. Il en existe de deux sortes : les debuggers passifs et les debuggers actifs. Les debuggers passifs sont également appelés désassembleurs car ils ne font qu'afficher le code source de l'éxécutable. Ainsi, ils sont passifs du fait qu'ils n'éxécutent pas une ligne de code. Les debuggers actifs, eux, éxécutent le code tout en permettant grâce à certains signaux système de bloquer l'éxécution du programme.
- Ndisasm
:Cet outil est le désassembleur Nasm. Créé
par les développeurs de notre compilo favori, il affiche en
sortie du code nasm (cad avec la syntaxe intel). Bien que le code de
sortie puisse paraître différent du fichier source.
Une petite astuce : si vous voulez que le code désassembler ressemble un peu plus à ce que vous avez coder utilisez l'option -b 32. Qui spécifie que le code sera désassemblé en utilisant des registres de 32 bits.
ndisasm -b 32 hello_world | less
- GDB
:gdb est le debugger fournie par défaut avec Linux,
il est à gcc ce qu'est ndisasm à nasm. Bien qu'il soit
beaucoup plus développé. gdb est un debugger ce qui
veut dire qu'il est capable de lancer un processus, de le bloquer,
de le désassembler ...
Nous allons voir comment effectuer quelques opérations de bases avec gdb via un exemple.
Nous allons donc chercher à debugger un programme simple qui s'occupe de nous donner la moyenne de plusieurs nombres contenus dans un tableau.
#include <stdio.h> int main(void) { int a[10] = {1, 58, 45, 87, 78, 98, 15, 56, 78, 21}; printf ("Moyenne du tableau a : %d", moyenne(a, sizeof(a))); return 0; } int moyenne(int tableau[], int taille) { int total = 0, moyenne, i; for( i=0; i<taille; i++) total += tableau[i]; return total/taille; }
Pour compiler ce code nous allons utiliser l'option -g de gcc permettant d'avoir des informations de debuggage plus précises : gcc moyenne.c -g -o moyenne.
flyers@Cyfik:~$ gdb moyenne (gdb) run Starting program: /home/flyers/moyenne Moyenne du tableau a : 10027686 Program exited normally. (gdb) break main Breakpoint 1 at 0x8048394: file moyenne.c, line 12.
On place un breakpoint à l'adresse de la fonction main() : 0x8048394. Et on relance le programme.
(gdb) run Starting program: /home/flyers/moyenne Breakpoint 1, main () at moyenne.c:12 12 int a[10] = {1, 58, 45, 87, 78, 98, 15, 56, 78, 21}; (gdb) print a $1 = {0, 134518396, -1073742984, 134513293, -1208116144, -1073742972, -1073742952, 134513787, -1073742804, -1208116128}
On affiche notre tableau qui n'est pas encore initialisé (c'est pour ça qu'il contient des valeurs fantaisistes).
(gdb) next 13 printf ("Moyenne du tableau a : %d", moyenne(a, sizeof(a))); (gdb) print a $2 = {1, 58, 45, 87, 78, 98, 15, 56, 78, 21}
Après initialisation, il est tout beau le tableau :)
(gdb) step moyenne (tableau=0xbffffb60, taille=40) at moyenne.c:18 18 int total = 0, moyenne, i;
next permet de passer à l'instruction suivante mais pas à sauter vers la fonction appelée tandis que step sert justement à cela. On saute dans la fonction moyenne()
(gdb) display tableau[i] 1: tableau[i] = 1 (gdb) display total 2: total = -1073742896
display permet d'afficher les valeurs des variables de façon actualisée.
(gdb) next 19 for( i=0; i<taille; i++) 2: total = -1208115591 1: tableau[i] = -1208116128
Après une bonne vingtaine de next (lorsque l'on valide une ligne vide, gdb refait l'instruction précédente), on se rend compte que des valeurs bizarres sont ajoutées à la variable total. Cela est dûe à la variable taille qui est à la base du fait que l'on effectue trop d'itérations, ce qui fait que la variable tableau[i] prend des valeurs au hasard dans la pile et celles-ci sont ajoutées à la valeur de la variable total.
L'initialisation de la variable taille est donc foireuse. En effet, sizeof ne renvoie pas le nombre d'éléments contenues dans le tableau mais la taille en octet de celui-ci ce qui donne donc 40 (10 * int où int = 4 sur notre x86).
(gdb) quit The program is running. Exit anyway? (y or n) y
Ainsi si l'on modifie la ligne
moyenne(a, sizeof(a))
par
moyenne(a, sizeof(a)/sizeof(*a))
notre programme fonctionnera parfaitement avec une variable taille contenant 10.
gdb permet d'utiliser de nombreuses commandes, ce qui risque d'être fastidieux à apprendre et à taper surtout pour les gros programmes à debugger c'est pour cela qu'il existe des abbréviations de ces commandes (ainsi que l'auto-complétion cf: touche “TAB”) dont voici un petit récapitulatif :
helphAccéder à l'aiderunrLancer le processusnextnLigne suivante dans le codestepsSauter dans une fonctionbreakbMettre en place un breakpointprintpAfficher une variablequitqQuitter gdbframefAffiche des informations sur la page de pile courantebacktracebtAffiche la pile d'appelsinfoiDonne des informations (info sans arguments pour savoir sur quoi)disabledisDésactiver momentanément un breakpointenableenActiver un breakpointdeletedSupprimer un breakpointcontinuecContinuer le programme après un breakpointdisassembledisassDésassemble tout ou partie du code.
La commande set permet de définir toute sorte de paramètres (dans gdb ou dans le programme en cours de debuggage). Pour voir tout ce que l'on peut définir : “h set“. Une petite astuce pour nous autre asmongueurs :) à la sauce intel : gdb de base désassemble le code dans la syntaxe gas ce qui n'est pas très compréhensible pour nous, qu'à cela ne tienne, la commande “set disassembly-flavor intel” réparera cet affront et faira en sorte que le code désassemblé sera dans la syntaxe intel.
- Strace
:Strace est un debugger actif permettant de suivre tous les
appels et les signaux émis par le programme. Cet utilitaire
est très utile pour commencer l'étude du
fonctionnement d'un binaire au sein du système.
Par exemple, si l'on strace notre hello_world de début d'article, on ne verra que les appels aux syscall write et exit. Tandis que le strace de hello_printf nous montreras tous les appels et signaux de la libc lors du lancement du programme ainsi que lors de la sortie. Je vous laisse tester par vous mêmes :).
- Application
:Nous allons maintenant appliquer les notions que nous avons
apprises ci-avant. Pour cela on va tenter d'outre-passer une
protection de type anti-ptrace. Le code contenant la protection
“ptraced.c”:
#include <sys/ptrace.h> int main(){ if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) exit(1); puts("FOO"); }
Ainsi donc, si le programme est ptracé alors le programme quitte. Sinon il nous affiche la chaîne “FOO”.
flyers@Cyfik:~$ gdb ptraced (gdb) r Starting program: /home/flyers/Data/Code/tmp/ptraced Program exited with code 01. # Le code a bien repéré qu'on le ptrace (gdb) set disassembly-flavor intel (gdb) disass main Dump of assembler code for function main: 0x080483f4 : push ebp 0x080483f5 : mov ebp,esp 0x080483f7 : sub esp,0x18 0x080483fa : and esp,0xfffffff0 0x080483fd : mov eax,0x0 0x08048402 : sub esp,eax 0x08048404 : mov DWORD PTR [esp+12],0x0 0x0804840c : mov DWORD PTR [esp+8],0x1 0x08048414 : mov DWORD PTR [esp+4],0x0 0x0804841c : mov DWORD PTR [esp],0x0 0x08048423 : call 0x80482f8 <_init+56> 0x08048428 : cmp eax,0xffffffff # c'est ici que le code vérifie qu'on # le ptrace 0x0804842b : jne 0x8048439 # si eax != -1 on saute à 0x8048439 0x0804842d : mov DWORD PTR [esp],0x1 0x08048434 : call 0x8048318 <_init+88> 0x08048439 : mov DWORD PTR [esp],0x8048564 0x08048440 : call 0x80482e8 <_init+40> 0x08048445 : leave 0x08048446 : ret End of assembler dump. (gdb) b *0x08048428 # un breakpoint juste avant le jne Breakpoint 1 at 0x8048428: file ptraced.c, line 4. (gdb) r Starting program: /home/flyers/Data/Code/tmp/ptraced Breakpoint 1, 0x08048428 in main () at ptraced.c:4 4 if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) exit(1); (gdb) p $eax $1 = -1 (gdb) set $eax=0 # on change la valeur de eax pour faire croire au programme qu'il # n'est pas ptracé (gdb) c Continuing. FOO Program exited with code 06. # et ça marche ! (gdb)
Comme le montre cette session, la méthodologie est tout d'abord de regarder (si on le peut) où se trouve la protection puis de trouver la méthode la plus adaptée pour l'outre-passer.
-
Vous devriez maintenant posséder toutes les notions pour faire vos propres programmes mais également pour comprendre ceux des autres. N'oubliez pas que si vous ne comprenez pas une mnémonique, le manuel nasm est là pour vous aidez ; si vous ne comprenez pas un syscall, il y a la liste des syscalls et enfin, si votre problème est impénétrable, pensez à nos amis debuggers :)
-
Un grand merci à tous ceux qui m'ont aidés à écrire cet article et à mieux comprendre l'assembleur. J'ai nommé :
edcba
Karsten Scheibler
kaze
mammon_
neil
viriiz
Par Flyers
No comments:
Post a Comment
Merci de votre commentaire. Celui-ci est en cours de modération.