16 nov. 2010

Assembleur sur Macbook (Macintel)

J'ai pris comme option ce semestre "Langage d'assembleur et jeux d'instructions". Dans ce module nous nous rapprochons du hardware plus particulièrement de l'architecture 8086 d'Intel.
Les TP se font normalement sur les machines du Petit Valrose (des PC Core 2 Duo sous Fedora), mais comme j'ai un mac et que je suis assez aventurier (ce que je regrette des fois), je me suis dit que normalement ca ne devrait pas trop changer, c'est quand même un Core 2 Duo que j'ai dans le macbook.



Sur les PC de la fac, nous utilisons Nasm pour compiler notre code assembleur et ald pour le débuggage; d'ailleurs le TP considère que l'on utilise ces programmes.
Sur mac, on a de la chance, Nasm est porté, par contre ald ne fonctionne pas, il est pourtant censé tourner sur Linux et Bsd mais lors de la compilation d'ald, il reconnaît le mac comme une plateforme Darwin (too bad).
Pour éclaircir un peu, depuis Mac OS X, Apple a opéré une refonte complète de son OS et est partie d'une base Unix, en particulier Bsd.
J'ai distingué 2 écoles de programmation assembleur, en me basant sur les appels de fonctions:
D'un côté Linux, qui prefère que l'on utilise les registres pour les paramètres des fonctions. Bsd de son côté nous laisse le choix, on peut utiliser les registres, ou alors utiliser la pile, en pushant les paramètres dans la pile dans le sens inverse du prototype de la fonction que vous allez appeler. C'est mieux avec un exemple.
Le prototype du syscall exit que l'on va appeler:
void exit(int)
La version Linux:
section .text
global _start
_start :
mov eax,1 ; numéro du syscall exit
mov ebx,0 ; paramètre d'exit, on va retourner 0
int 0x80 ; c'est comme ca qu'on appelle un syscall

La version Bsd:
section .text
global _start
_start :
mov eax,0
push eax ; je met le paramètre dans la pile
mov eax,1 ; numéro du syscall exit
int 0x80 ; appel du syscall

Les deux codes sont équivalents. Dans l'absolu je préfère la façon de Bsd, qui est beaucoup plus élégante, l'utilisation de la pile permet de faire passer beaucoup plus de paramètres que par l'utilisation des registres, car en 8086, il n'y a que 16 registres généraux de 32 bits, sachant que ESP est utilisé comme pointeur de sommet de pile, EIP comme compteur de boucle, et EAX forcément serra utilisé pour indiquer le numéro du syscall à appeler.
Malheureusement sous mac, et malgré la forte influence Bsd, nous somme limité à l'utilisation de la pile pour le passage de paramètres, du moins pour l'appel des syscall. De plus, et c'est quelque chose que j'ai découvert bien après, et c'est d'ailleurs pourquoi je ne compte pas continuer l'assembleur sur mac, c'est l'alignement du pointeur de pile.
Je m'explique. La pile est à l'envers, en fait le sommet de pile commence vers la fin de la mémoire, et lorsque l'on empile quelque chose, l'adresse du pointeur de pile décrémente, ce qui est normal ici, aussi faut-il comprendre comment fonctionne une pile. Un paramètre empilé prendra 4 byte, donc l'adresse de la pile après le push serra (adresse précédente - 4). Le problème est que le mac mange du code par bouchée de 16 byte, donc il faut absolument que le pointeur de pile soit aligné comme il faut avant l'appel de votre fonction, sinon l'appel se ferra bien, mais les paramètres passé ne seront pas interprétés comme il faut. J'ai passé une semaine à chercher pourquoi le code précédent (sous Bsd) me retournait 1 au lieux du 0 que j'ai pourtant donné. Tout n'est pas perdu car la pile commence toujours à une adresse du style 0xnnnnnnnC, (l'adresse est en héxa, C fait 12 en décimal). Voyons ça avec un exemple.
Reprenons le code précédent (Bsd style):
section .text
global _main
_main :
mov eax,0
push eax ; je met le paramètre dans la pile
mov eax,1 ; numéro du syscall exit
int 0x80 ; appel du syscall

Au début ESP est à 0xnnnnnn9C, mais après le push eax, ESP est à 0xnnnnnn98 (on a bien 12 - 4 = 8), on à un problème. Pour contourner ça, on va réaligner ESP pour qu'on ait bien un morceau de 16 byte:
section .text
global _main
_main :
mov eax,0
push eax ; je met le paramètre dans la pile
mov eax,1 ; numéro du syscall exit
sub esp,12 ; 4 + 12 = 16
int 0x80 ; appel du syscall

On commence avec ESP à 0xnnnnn9C, après le push eax, ESP est à 0xnnnnn98, on modifie directement l'adresse dans ESP, et on le met à 0xnnnnnn8C, OUF !
Et il faudra faire de même à chaque fois que vous appelez un syscall, en tenant compte du nombre de paramètres, si par exemple nous avions utilisé un syscall qui prend 2 paramètres, nous n'aurions eu qu'à faire un sub,8 pour aligner ESP.
Pour les téméraires, j'indique que les numéros des syscall sous mac sont dans /usr/include/sys/syscall.h, et les prototypes dans /usr/include/unistd.h

La compilation aussi et l'édition de liens est quelque peu exotique. Par défaut Nasm va vouloir compiler votre code pour une architecture x64_64, mais il faut utiliser le format macho:

nasm -f macho monCode.asm

Pour l'édition de liens, j'utilise gcc directement (en précisant que le code qu'on lui donne est en 32 bits):
gcc -arch i386 monCode.o -o monCode

une petite remarque: gdb se cale sur l'interruption au lieu de faire du pas à pas, et je n'ai toujours pas trouvé comment outrepasser ça, si quelque à une idée...

2 commentaires:

DaStatikS a dit…

Interessant comme article ! Je n'ai pas de notions en assembleur mais ca a l'air passionnant ! Continues à poster des petits articles pour les geeks que nous sommes ! :)

Paraita a dit…

De l'assembleur sur mac, c'est pas si dur que ça, faut juste penser à réaligner ESP avant de CALLer.

merci pour le commentaire, tu peux m'ajouter sur twitter si tu l'utilises je suis plus actif que là. Les articles que j'écris sont plus destiné à montrer ce que je fais, une sorte de cahier de bord, et surtout quand je trouve pas d'aide sur un sujet en particulier sur internet, je poste mes résultats ici.
Je cible surtout les étudiants qui vont se retrouver devant les mêmes problèmes que j'ai eût ^^