Lsd Security Daemon
LSD Security Daemon

Pwning N64 for fun & speedrun : Instruction Set

Io les mousdugenou,

Ouais ouais, je sais, la dernière fois, j’ai dit qu’on ferait du rom hacking sur ce blogpost. J’ai menti, et je suis un méchant pas beau, c’est pas une nouveauté.
Bref, souvenez vous, j’avais parlé de la N64 d’une manière globale, en présentant un peu tous les concepts. Mais je suis passé très très vite sur une partie importante, pour en faire un blogpost complet : les instructions du CPU MIPS. Ca va être un article chiant, pas super instructif (quoi que), et qui va pas plaire. MAIS. Il est important pour assimiler facilement la suite.
Today, on va donc parler de l’Instruction Set en détails, et je vais principalement expliquer les instructions les plus utilisées sur du MIPS*, avec un gros listing des milles-fa, et quelques commentaires.

So, have fun, dudes ! (ou pas. Même moi, j’aurais pas envie de le lire ce post)

Avant de commencer

Il est nécessaire, pour que vous compreniez tous -même pour les sagouins qui n’ont pas lu mon article précédent-, de donner quelques explications globales. On peut comprendre le jeu d’instructions sans ce paragraphe, mais c’est toujours intéressant à connaitre.

Les classes d’instructions

Les instructions MIPS ont été définies en plusieurs classes :

  • I family : ici, ce sont les instructions utilisant un Immediate dans les paramètres.
  • J family : dédié uniquement à J et JAL.
  • R family : pour les instructions tapant les registres. Ce sont ces commandes qui utilisent le paramètre Func, dont je parle dans le paragraphe suivant.

En fait, comme chaque instruction est codée sur 32 bits, on a défini ces classes en fonction de la manière de lire les bits : selon les familles, le découpage et la lecture est fait différemment au niveau binaire, afin d’optimiser le nombre de bits utiles.

Classe I(mmediate)
opcode Source Target Imm
6 bits 5 bits 5 bits 16 bits

Elle est représentée comme ci dessus. Les 6 bits d’opcode sont necessaires pour identifier l’instruction. Ensuite, ona a un registre source et destination de 5 bits chacun, puis un Immediate de 16 bits. Cette classe a été optimisée pour avoir le plus grand Immediate possible.

Classe J(ump)
opcode Address
6 bits 26 bits

Comme précédemment, on a 6 bits d’opcode, puis uniquement une adresse codée sur 26 bits, afin de pouvoir aller le plus loin possible en un seul saut.
Dans cette classe, et ça peut paraître idiot, on ne trouve pas d’autre instruction de saut comme JR.
“Pourquoi ?” allez vous me demander ?
Tout simplement parce que JR utilise un registre comme paramètre, ce qui fait que d’une part, il n’y aucun intérêt à avoir 26 bits pour un registre, et d’autre part, la classe registre est faite pour ça. Ca évite donc ici d’avoir une exception du genre ‘Si c’est JR, alors c’est pas une adresse, mais un registre’.

Classe R(egister)
opcode Source Target Dest Shift function
6 bits 5 bits 5 bits 5 bits 5 bits 6 bits

Dernière classe, mais la plus compliquée à apréhender, principalement en raison du paramètre Func : la famille Register.
Comme toujours, 6 bits pour l’opcode, puis 5 bits de source, 5 de cible, et 5 de destination (attention, faut pas les confondre).
On trouve ensuite 5 bits utilisés uniquement dans le cadre des instructions de bit shifting, puis 6 bits de Func, dont je parle plus loin.

Ces deux derniers paramètres ont l’air d’avoir été un peu “rajoutés” parce qu’on a pas trouvé moyen de faire autrement.
Cela dit, mettre le shift dans un paramètre à part est totalement logique : Source/Target/Destination doivent être des registres, et Func a une fonction totalement différente. Du coup, le seul endroit où mettre les infos de shift, c’était là :D.
Concernant Func, il est là pour palier au “manque de place” sur les opcodes. Ces derniers sont codés sur 6 bits, ce qui ne fait que 32 instructions possibles. Si on rajoute les bits de Func, on augmente considérablement le nombre total d’instructions. Et comme il n’y a besoin que de 26 bits pour contenir les autres informations nécessaires aux instructions de la famille R, le compte est bon.

Mais pourquoi ils ont fait çaaaaaa ?

En fait, tout ce découpage n’a pas réellement de sens niveau dev. Il est même carrément idiot car on se retouve avec des ADD et des Jump dans la même famille. C’est pour ça qu’on trouve plus souvent des listings comme celui que j’ai fait plus bas.
L’avantage principal, c’est que chaque classe est optimisée par rapport à ses paramètres, et on ne perd donc pas de bit inutilement.

Au passage, il existe plus ou moins d’autres familles pour les instructions dédiées aux co-processeurs, mais on va pas charger la mule, sinon vous risquez de péter une durite.

Les opcodes

La notion d’opcode en MIPS est un peu fucked up. Il existe en fait des instructions ayant un opcode unique (les classes I et J), et d’autres (la fameuse classe R) qui se partagent le même opcode (par exemple, ADD et Jump Return ont tous les deux l’opcode 0x00).

Si on regarde le détail du parsing de la classe R, on voit qu’on a 6 bits de Func, qui sont utilisés comme une espèce de “sous opcode” dégueulasse qui permet de distinguer les fonctions. Ainsi, même si ADD et JR ont pour opcode 0x00, le premier aura comme Func 0x20, et le second 0x08.

Au passage, quand je dis dégueulasse, c’est faux, les mecs qui ont pondu ça ont plutôt bien joué leur coup, car cela permet d’optimiser la place de chaque type d’instruction. Comme les instructions sur les registres sont les plus importantes, il n’a pas été jugé utile de mettre un paramètre Func ailleurs.

Le problème, c’est que pour parler de ces opcodes en hexa, c’est relou, car ce n’est pas “juste” l’opcode, mais des fois l’opcode ET une func. Faut aussi prendre en compte que ça sera plus galère de chercher un opcode hexa dans un dump. Et comme en plus ils sont codés sur 6 bits, on peut même pas se baser sur un octet comme recherche, puisque les 2 derniers bits sont utilisés pour des paramètres.

Les bits variables

Ok, ce titre n’a pas de sens, et ce paragraphe n’a d’ailleurs que peu d’utilité pour comprendre l’instruction set, mais j’ai de la confiture à étaler sur ma culture (ou un truc comme ça).

Certaines instructions, comme LUI, ont ce que j’appelle des bits variables, ou encore des “On s’en bat le bit”. En gros, peu importe les bits qu’on met, l’instruction sera la même.
Si on regarde par exemple le détail de LUI, on voit que c’est une instruction de la famille I (cf plus haut), famille qui attend comme argument une source, une cible, et un Immediate.
Mais LUI n’a pas besoin de paramètre Source. Du coup, peu importe ce qu’on met dans les 5 bits de ce champ, le proc ne s’en souciera pas…

Les instructions de calcul

Je met ça ici, mais ça vaut pour tous les paragraphes : dans mon listing, un élément faisant référence à un registre sera noté $XXX. Un élément renvoyant à une adresse mémoire sera noté *XXX. Une valeur fixe sera notée Imm.

Les additions et soustractions

Syntaxe Description Opcode Func Commentaires
ADD $dst, $src1, $src2 $dst = $src1 + $src2 0x00 0x20 Les opérandes sont signées
ADDU $dst, $src1, $src2 $dst = $src1 + $src2 0x00 0x21 Les opérandes ne sont PAS signées
ADDI $dst, $src, Imm $dst = $src + Imm 0x08 N/A Les opérandes sont signées
ADDIU $dst, $src, Imm $dst = $src + Imm 0x09 N/A Les opérandes ne sont PAS signées
SUB $dst, $src1, $src2 $dst = $src1 - $src2 0x00 0x22 Les opérandes sont signées
SUBU $dst, $src1, $src2 $dst = $src1 - $src2 0x00 0x23 Les opérandes ne sont PAS signées

Remarque :

  • Non. Il n’y a pas de soustraction avec des Immediates. Dunno why.
  • Pour les opérations signées, elles génèrent un overflow exception géré par le coprocesseur.

Les multiplications et divisions

Syntaxe Description Opcode Func Commentaires
MULT $src1, $src2 $LO = $src1 * $src2 0x00 0x18 Les registres sont signés
MULTU $src1, $src2 $LO = $src1 * $src2 0x00 0x19 Les registres ne sont PAS signés
DIV $src1, $src2 $LO = $src1 * $src2 0x00 0x1A Les registres sont signés
DIVU $src1, $src2 $LO = $src1 * $src2 0x00 0x1B Les registres ne sont PAS signés

Remarques :

  • $LO est toujours le registre dans lequel on met le résultat d’une multiplication/division.
  • $HI est toujours les registre dans lequel on met le reste d’une division.

Les sauts

Les sauts classiques

Syntaxe Description Opcode Func Commentaires
J Imm Goto Imm 0x02 N/A bahhh, c’est un Jump quoi… Il est aussi noté B des fois (pour Branch). Imm est codé sur 26 bits
JAL Imm $RA = addr JAL+8; Goto Imm N/A 0x03 Jump And Link. J’en ai assez parlé dans l’article précédent. Imm est codé sur 26 bits
JR $src Goto $src 0x00 0x08 Jump Return. Saute vers le registre donné, usuellement $RA

Les branches

Les branches, c’est comme des sauts, mais avec une condition. Il y en a un paquet, mais je ne vais pas mettre les moins utilisées, ça sert à rien.

Syntaxe Description Opcode Func Commentaires
BEQ $src1, $src2, Imm if $src1 == $src2; Goto Imm 0x04 N/A Branch on EQual. Offset sur 2 octets
BNE $src1, $src2, Imm if $src1 != $src2; Goto Imm 0x05 N/A Branch on Not Equal. Offset sur 2 octets
BGEZ $src, Imm if $src >= 0; Goto Imm 0x01 N/A Branch on Greater or Equal to Zero. Offset sur 2 octets
BGTZ $src, Imm if $src > 0; Goto Imm 0x07 N/A Branch on Greater Than Zero. Offset sur 2 octets
BLEZ $src, Imm if $src <= 0; Goto Imm 0x06 N/A Branch on Less or Equal to Zero. Offset sur 2 octets
BLTZ $src, Imm if $src < 0; Goto Imm 0x01 N/A Branch on Less Than Zero. Offset sur 2 octets

Remarque :

  • Ces instructions ont une version Likely (eg. BEQL). En gros, la Branch va forcément charger l’instruction juste en dessous la branche, mais si la condition est fausse, le proc va invalider cette instruction, et on aura “juste” l’impression de sauter une instruction.

Les opérations sur les bits

Syntaxe Description Opcode Func Commentaires
AND $dst, $src1, $src2 $dst = $src1 & $src2 0x00 0x24 Un AND classique
ANDI $dst, $src, Imm $dst = $src & Imm 0x0C N/A Un AND classique avec un Immediate
OR $dst, $src1, $src2 $dst = $src1 | $src2 0x00 0x25 Un OR classique
ORI $dst, $src, Imm $dst = $src | Imm 0x0D N/A Un OR classique avec un Immediate
XOR $dst, $src1, $src2 $dst = $src1 ^ $src2 0x00 0x26 Un XOR classique
XORI $dst, $src, Imm $dst = $src ^ Imm 0x0E N/A Un XOR classique avec un Immediate
SLL $dst, $src, Imm $dst = $src << Imm 0x00 0x00 Shift Left Logical. Shift codé sur 5 bits
SRL $dst, $src, Imm $dst = $src >> Imm 0x00 0x02 Shift Right Logical. Shift codé sur 5 bits
SRA $dst, $src, Imm $dst = $src >> Imm 0x00 0x03 Shift Right Arithmetic. Comme SRL, mais on garde le bit de signature

Les mouvements d’information

C’est un titre un peu foireux, mais en gros, c’est tout ce qui permet de charger vers/depuis la heap, plus deux trois autres trucs du même genre.

Syntaxe Description Opcode Func Commentaires
LB $dst, Imm(*src), $dst = *(*src+Imm) 0x20 N/A Load Byte. On prend *src, auquel on ajoute Imm, et on met la valeur de l’adresse (*src+Imm) dans $dst
LW $dst, Imm(*src) $dst = *(*src+Imm) 0x23 N/A Load Word. Comme LB, mais sur un Word
SB $src, Imm(*dst) *(*dst+Imm) = $src 0x28 N/A Store Byte. C’est l’inverse de LB. On prend la valeur d’un registre, et on le met dans l’adresse spécifiée.
SW $src, Imm(*dst) *(*dst+Imm) = $src 0x2B N/A Store Word. Commet SB, mais sur un Word
LUI $dst, Imm $dst = Imm << 16 0x0F N/A Load Upper Immediate. Ca permet de charger une valeur dans la partie haute d’un registre
MFHI $dst $dst = $HI 0x00 0x10 Move From HI (le reste d’une division)
MFLO $dst $dst= $LO 0x00 0x12 Move From LO (le réstultat d’une multiplication ou division)

Remarque :

  • Pour ceux qui se posent des questions sur tout le bazar sur les chargements/sauvegardes de données, go lire la partie “Quelques tricks” de mon blogpost précédent.

Les conditions

Ces instructions sont plutôt pratiques, elles permmettent de faire des if en une seule ligne :) tldr : ça remplace le flag Zero sur x86.

Syntaxe Description Commentaires
SLT $dst $src1, $src2 $dst = 0; if $src1 < $src2 ; $dst = 1 Set if Less Than. Plutôt obvious comme nom
SLTI $dst $src, Imm $dst = 0; if $src1 < Imm ; $dst = 1 Pareil, mais avec un Immediate

Remarque :

  • Il existe les versions unsigned, mais je ne les ai jamais croisées.

Le coprocesseur

Comme je l’ai dit dans mon précédent blogpost, il existe sur MIPS un coprocesseur qui gère tout ce qui est nombres flottants (FP). Ce coproc a un instruction set dédié, et totalement différent de ce qu’on croise d’habitude.
Globalement, il n’est pas là pour toute “l’intelligence” du programme, mais juste pour la partie calcul des floating. Du coup, le programme principal lui passe les datas, le coproc va effectuer ses calculs et redonner les infos au proc principal. Pour lui envoyer les données, on utilise les fonctions suivantes :

Syntaxe Description Commentaires
MTC1 $src, $dst $dst = $src Move To Coproc 1. La source est un registre général, et la destination un registre floating. Le 1 indique ici qu’on déplace vers le coproc 1. On pourrait virtuellement mettre mtc2 pour déplacer vers le coproc 2**
MFC1 $src, $dst $dst = $src Move From Coproc 1. C’est l’inverse du MTC1
LWC1 $dst, Imm($src) $dst = *(Imm+$src) Load Word to Coproc 1. On met dans $dst (un registre Floating) la valeur mémoire de $src (un registre général) auquel on aura ajouté Imm. Ici, on charge un octet
LDC1 $dst, Imm($src) $dst = *(Imm+$src) Load Double to Coproc 1. Comme LWC1, mais on charge ici un double
SWC1 $dst, Imm($src) $dst = *(Imm+$src) Store Word from Coproc 1. On met dans $dst (un registre général) la valeur mémoire de $src (un registre floating) auquel on aura ajouté Imm. Ici, on charge un octet
SDC1 $dst, Imm($src) $dst = *(Imm+$src) Store Double from Coproc 1. Comme SWC1, mais on charge ici un double

Une fois que les données sont récupérées, ce petit coquinou peut effectuer quelques instructions.
Mais, si tout était simple, ça ne serait pas drôle… En fait ces instructions sont “variables”. Chaque instruction se compose d’une commande et d’un ou plusieurs paramètre(s), ce qui permet de donner une directive.
Les paramètres sont notés X et Y dans le listing suivant.

  • X correspond à un format de nombre et peut prendre les valeurs s (pour single precision), d (double precision), w (word/integer)
  • Y correspond à des conditions de comparaisons, et des valeurs telles que eq (EQual), lt (Less Than), le (Less or Equal), gt (Greater Than), ou ge (Greater or Equal)***.
Syntaxe Description Commentaire
ADD.X $dst, $src1, $src2 X($dst) = X($src1) + X($src2) Une addition quoi…
CVT.X1.X2 $dst, $src X1 $dst = X2($src) conversion entre deux formats de nombres
C.Y.X $src1, $src2 $cc = (X($src1) Y X($src2)) comparaison entre deux nombres. Le résultat est mis dans le registre spécial Condition Code
BC1T $dst if $cc == True; Goto $dst On saute en $dst si le registre Condition Code est vrai
BC1F $dst if $cc == False; Goto $dst On saute en $dst si le registre Condition Code est faux

Pour que ça soit plus clair, et donner un exemple concret, voici un bout de code ASM :

MTC1 $1 $fp0 # on met le contenu du registre général $1 dans le registre $fp0
MTC1 $2 $fp2 # on met le contenu du registre général $2 dans le registre $fp2
CVT.d.w $fp0, $fp0 # on considère que $fp0 était un entier, on le passe en double precision
CVT.d.w $fp2, $fp2 # idem pour $fp2
ADD.d $fp0, $fp0, $fp2 # $fp0 et $fp2 sont considérés comme des double precisions, on les additionne et on met le résultat dans $fp0
C.lt.d $fp0, $fp2 # Si $fp0 est plus petit que $fp2, on met le registre cc a True. Sinon, à False. Dans cet exemple, $fp0 est forcément plus grand que $fp2 en raison de la ligne juste au dessus, donc $cc est égal à False
BC1F 0x1234 # Si $cc est à False, on saute en 0x1234
CVT.w.d $fp0, $fp0 # on repasse $fp0 en entier
MFC1 $1, $fp0 # on renvoie $fp0 dans $1, sous forme d'entier

Ce code n’a pas réellement de sens, mais il est là pour que le fonctionnement des instructions floating soit plus clair. J’espère que c’est le cas, parce que c’était un poil galère à expliquer -_-.

Remarques :

  • Ce n’est pas super important pour lire l’ASM, mais le jour où vous devrez le modifier ou écrire un truc sur le coproc, il faut faire attention : un Floating en double precision peut être mis sur DEUX registres (en fonction di bit FR du registre spécial SR, mais j’en ai pas encore parlé de celui là ^^’).
  • En fait, j’en parle ici, des instructions FP, mais on en croise pas non plus tous les 4 matins. Ca se voit de temps en temps, mais c’est pas non plus ce qu’il y a le plus souvent à reverse.
  • J’ai mis le add, mais il en existe une palanquée d’autres, toutes sur le même principe : SUB, MUL, ABS, SQRT, etc. Je n’ai pas voulu alourdir encore plus le tableau.

Fun Fact

On connait tous l’instruction NOP. Et bien ici, elle n’existe techniquement… pas :D
En fait, un NOP en MIPS a un encoding 0000 0000 0000 0000 0000 0000 0000 0000.
L’instruction SLL a, elle, un encoding de 0000 00ss ssst tttt dddd dhhh hh00 0000.
Donc techniquement, un NOP correspond à un SLL avec comme destination, source et shift 0. Un 0, qu’on shifte de 0 bits, et qu’on met dans 0, bahhhh, ça ne fait rien d’utile, et ça matche avec la définition d’un NOP ^^.

It is the end, my friend

Voilà. Un article dans le genre pas super intéressant à lire. Moi même, l’Instruction set, je me le suis pas parsé entièrement avant de devoir écrire ce post.
Cela dit, il n’y a pas taaaaaaant d’instructions que ça (d’où le R de RISC), et une partie est similaire à du x86, même s’il y a quelques changements. Du coup, ça s’apprend vite, surtout que globalement, on retrouve toujours les mêmes.
Promis, le prochain article sera plus intéressant ^^’ Au passage, je remercie ibileleC pour sa relecture et les erreurs qu’il m’a remontées :)

Enjoy

The lsd

Sources

www.mrc.uidaho.edu : les instructions les plus utilisées. C’est là dessus que je me base principalement en cas de besoin.
hack64.net : la doc complète de la famille R43XX. Ca fait 600 et des bananes de pages. C’est plutôt long à lire ^^’
www.ece.lsu.edu : une doc sur la partie instructions des floating.

* Pour une liste exhaustive de toutes les instructions, ça se passe dans la doc présente sur hack64.net, à partir de la page 371. Bon courage.

** Qui n’est pas utilisé, la datasheet indique qu’ils sont “for future use”. Il existe au passage un maximum de 4 coproc, cela dit, jouer à envoyer des datas dans d’autres coprocs que le 1, c’est pas forcément la gloire assurée.

*** En fait, en potassant le doc complète, on voit qu’il en existe un gros paquet. Tableau 7-11 en page 228 de la doc sur hack64.net.