2011
01.04

Voici donc la troisième et dernière partie de cette série d'articles sur la lecture de disques Wii en Python. Maintenant que tout le travail de documentation des différentes structures et des différents formats dont on a besoin a été fait, la dernière étape est de faire quelque chose de propre et d'utilisable à partir de tout ça. En effet, des bouts de code à balancer dans un shell Python c'est loin d'être propre, même si c'est bien pour expérimenter rapidement.

Le résultat de tout ça s'appelle wiiodfs, pour Wii Optical Disc FileSystem. Il s'agit d'une application capable de monter une image de disque Wii sous Linux, mais également d'une bibliothèque pouvant être utilisée dans d'autres programmes qui auraient besoin d'accèder à un disque de Wii. Il reste encore un peu de travail à faire au niveau du packaging mais 99% du code utile est là, utilisable et fonctionnel.

La première grande différence avec ce qui a été fait lors des deux articles précédents est qu'on ne peut pas se permettre de dumper toute la partition dans un fichier temporaire, ou dumper tous les fichiers dans un dossier. C'est sale, lent et ça a peu d'intérêt. Dans wiiodfs, toute la partie déchiffrement et accès aux fichiers est faite à la volée lorsqu'on en a besoin. Toute l'architecture de la bibliothèque est donc basée sur ce besoin.

wiiodfs est séparé en 4 couches de différents niveaux. La couche de niveau n peut utiliser toutes les couches de niveau inférieur. Voici ces différentes couches expliquées en détail :

  • L'accès brut à l'image disque, aux métadonnées et à la table des partitions/volume groups. Très simple, pas de déchiffrement, pas de gestion du filesystem, uniquement de quoi lire l'image d'un point A à un point B et de quoi récupérer les informations sur les partitions du disque.
  • L'accès brut aux données d'une partition déchiffrée. C'est surement la partie la plus intéressante donc je reviendrai dessus plus tard, mais elle va en gros lire les données brutes sur la partition, les déchiffrer et retourner ces données à l'utilisateur.
  • L'accès aux fichiers sur la partition avec une API facile à utiliser : on utilise Filesystem.open qui nous renvoie un objet qui se comporte comme un fichier Python, sur lequel on peut appeler les méthodes read ou seek par exemple. On peut également lister les dossiers, récupérer la taille d'un fichier, vérifier si un fichier existe, etc.
  • Enfin, une interface permettant d'utiliser le filesystem via PyFS, une bibliothèque dont le but est de définir une interface commune pour différentes classes de gestion des systèmes de fichier afin de pouvoir les utiliser uniformément.

Comme dit juste au dessus, je pense que la partie sur laquelle il faut le plus s'attarder est la deuxième couche, celle qui gère le déchiffrement des partitions. En effet, déchiffrer des blocs de 0x8000 octets n'est pas spécialement rapide et il faut donc un moyen de garder en cache les clusters déchiffrés pour ne pas déchiffrer 20x le même cluster à la suite. Pour cela, j'ai réimplémenté un cache de type LRU. Les caches LRU sont les caches les plus simples, qui gardent un certain nombre de valeurs dans leur ordre de dernière utilisation. Ainsi, toutes les valeurs les plus récemment utilisées sont dans le cache, et les autres sont éliminées du cache au fur et à mesure qu'elles ne sont plus utilisées. On pourrait surement faire beaucoup plus intelligent mais ça n'était pas mon but ici (et de toute façon c'est déjà bien rapide).

wiiodfs fournit également un script nommé wiiodmount qui permet donc de monter une image disque de Wii sur un dossier. Pour cela, le script utilise la 4ème couche ainsi qu'une fonctionnalité de PyFS qui permet d'exporter un filesystem conforme à l'API de PyFS via FUSE. C'est magique et ça marche plutôt très bien lors de mes tests.

Pour conclure cette série d'articles, je vais simplement dire que c'était une aventure très intéressante pour moi : je connaissais très peu la Wii il y a une semaine et l'implémentation de wiiodfs m'a permi de comprendre un certain nombre de notions utilisées par la console de Nintendo, par exemple tout ce qui concerne le chiffrement des données. wiiodfs est surement loin d'être fini ou même utilisable pour tout le monde (il faudrait que je prenne le temps de faire le packaging par exemple) mais c'est une implémentation simple et efficace d'un filesystem assez peu documenté, dont j'espère qu'elle sera utile à des gens 🙂

Finissons par des liens et des remerciements :

  • Wiiodfs, dépot Mercurial du projet.
  • Wiibrew, une de mes principales sources de documentation.
  • Dolphin, un émulateur GC/Wii assez génial et libre.

Pendant les prochains jours j'essaierai de prendre le temps d'écrire de nouveaux articles sur les formats de fichier que j'ai trouvé dans le FS de Tales of Symphonia : Dawn of the New World. Des trucs comme les textures, les modèles, les sons, les musiques, etc. Stay tuned!

2011
01.02

Comme promis, cette deuxième partie va parler du système de fichiers utilisé sur les DVD de jeux de Wii, et plus précisément sur leur partition de données. La partie 1 de cette série de 3 articles présentait comment accèder à cette partition de données à partir d'un DVD et comment la déchiffrer. Pour cet article nous allons supposer que la partition de données a été déchiffrée et dumpée afin de se simplifier la tâche, mais ça n'est pas nécessaire (on peut déchiffer à la volée). Commençons par de la théorie 🙂

Lorsque la Wii charge un jeu sur DVD, elle commence par charger un bootloader très léger qui se trouve à l'adresse 0x2440. Ce bootloader va ensuite charger un fichier DOL, en gros l'OS tournant derrière le jeu, qui va lui même charger l'exécutable du jeu qui se trouve sur le système de fichiers du DVD (dont le nom dépend de l'ID du jeu). Le bootloader et l'OS ne sont pas sur le système de fichiers et ne nous intéressent pas tant que ça : pour les jeux commerciaux, ce sont toujours les mêmes (ils font partie du SDK de Nintendo). À côté de ces deux composants il y a ce que l'on va tenter d'ouvrir : le système de fichiers.

Sa structure est très simple, et c'est plutôt normal pour un système de fichiers minimaliste en lecture seule (pas de fragmentation à gérer vu qu'on peut l'éviter lorsqu'on build le FS). Déjà, toutes les données sont rassemblées à un endroit et toutes les métadonnées sont rassemblées à un autre endroit. Les métadonnées sont contenues dans ce qu'on appelle la FST pour FileSystem Table et contient l'ensemble des informations sur les fichiers. En pratique, ces informations sont juste le nom du fichier et l'adresse et la taille des données dans le cas d'un fichier classique, ou le nom et la liste des fils pour un dossier. L'emplacement de cette FST n'est pas fixe : il est indiqué à l'offset 0x424. Comme la Wii compte en blocs de 32 bits et que l'on veut une adresse en octets, on multiplie par 4 l'entier trouvé à l'adresse 0x424.

La FST est construite avec une brique de base : le descripteur de fichier. Un descripteur pèse 12 octets et représente les métadonnées d'un fichier ou d'un dossier. Sur ces 12 octets, les 4 premiers sont l'offset vers le nom du fichier, les 4 suivants sont l'offset vers les données (toujours en bloc de 32 bits, donc à multiplier par 4 pour avoir en octets) et les 4 derniers sont la taille des données (en octets cette fois, la cohérence est d'ordre). Pour savoir si un descripteur représente un fichier ou un dossier, on regarde les 8 bits de poids fort de l'offset vers le nom du fichier : s'ils sont tous à 0, c'est un fichier, sinon c'est un dossier.

Qui dit système de fichier dit hiérarchie. Sauf que la FST est une structure linéaire : tous les descripteurs sont les uns à la suite des autres. Pour représenter la hiérarchie, les descripteurs de dossiers vont se servir de leur champ « taille » pour stocker le premier descripteur dont ils ne sont pas père. Par exemple, si la racine contient 3 fichiers, son champ « taille » vaudra 4. Un petit exemple en ASCII art :

+---------------------+
| Dossier 1           |
|   size = 7          |
+---------------------+
    +---------------------+
    | Fichier 1           |
    |   size = 42         |
    +---------------------+
    +---------------------+
    | Dossier 2           |
    |   size = 6          |
    +---------------------+
        +---------------------+
        | Fichier 2           |
        |   size = 1337       |
        +---------------------+
        +---------------------+
        | Fichier 3           |
        |   size = 1234       |
        +---------------------+
        +---------------------+
        | Fichier 4           |
        |   size = 4321       |
        +---------------------+
    +---------------------+
    | Fichier 5           |
    |   size = 101010     |
    +---------------------+

Enfin, le premier descripteur représente la racine, donc sa « taille » est le nombre total de descripteurs. Il n'a pas de nom. Après tous les descripteurs vient la zone où sont stockés les noms de fichiers. Tous les offsets de noms sont relatifs à cet emplacement. Et c'est tout ! Plutôt simple quand on compare à ce qui se fait ailleurs, et efficace pour le besoin d'un jeu de Wii. Passons donc à l'implémentation. Comme précédemment, je ne code rien de « propre » dans cet article : c'est uniquement un transcript d'une session interactive Python. Le beau code viendra dans la dernière partie. On va commencer par importer les modules et ouvrir l'image dumpée de la partition :

Python 2.7.1 (r271:86832, Dec 20 2010, 11:54:29) 
[GCC 4.5.1 20101125 (prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import namedtuple
>>> from struct import unpack as up
>>> fp = open('tos-2-dumped.img', 'rb')

Ensuite, quelque chose que j'aurais déjà dû faire dans l'article précédent : une tout petite fonction bien utile qui seek puis qui lit. C'est très pratique et ça évite d'oublier de seek après un read :

>>> def read_at(off, len):
...     fp.seek(off)
...     return fp.read(len)

La première étape est de récupérer l'adresse de la FST. On n'oublie pas de la multiplier par 4 :

>>> fst_off = up('>L', read_at(0x424, 4))[0] * 4

On récupère ensuite l'adresse de la fin de la FST, donc du début de la zone qui contient les noms de fichiers. Pour cela, on récupère la taille du premier descripteur (le dossier racine), on la multiplie par 12 pour avoir la taille en octets de la FST et on l'ajoute à l'offset de la FST :

>>> str_off = fst_off + up('>8xL', read_at(fst_off, 12))[0] * 12

La zone contenant les noms de fichiers est une suite de chaines de longueur max 256 séparées par des \0. Python n'a rien de standard pour lire ça (malheureusement !), on va donc faire une petite fonction qui lit un nom de fichier à partir de son offset :

>>> def read_filename(off):
...     s = read_at(str_off + off, 256)
...     return s[:s.index('\0')]
... 
>>> read_filename(0)
'BATTLE'
>>> read_filename(7)
'BG'
>>> read_filename(10)
'btl000.brres'

On va ensuite faire une fonction qui lit un descripteur de fichier et tous ses fils récursivement pour construire l'arborescence. C'est pas forcément simple à comprendre à la première lecture, je commente tout ça après :

>>> def read_descr(idx):
...     descr = read_at(fst_off + 12 * idx, 12)
...     (name_off, data_off, size) = up('>LLL', descr)
...     data_off *= 4
...     
...     is_dir = bool(name_off & 0xFF000000)
...     name_off &= ~0xFF000000
...     name = read_filename(name_off) if idx else ''
...     
...     if not is_dir:
...         return idx + 1, name, (data_off, size)
...     else:
...         children = {}
...         idx += 1
...         while idx < size:
...             idx, child_name, child = read_descr(idx)
...             children[child_name] = child
...         return idx, name, children

Déjà, identifions le retour de la fonction : un triplet qui contient l'index du premier descripteur qui n'a pas été traité (ça nous permet de faciliter la récursion), le nom du fichier ou du dossier représenté par le descripteur, et enfin selon qu'il s'agisse d'un dossier ou d'un fichier, les fils ou l'emplacement des données. Ensuite, au niveau du code, on commence à lire les 12 octets du descripteur. On regarde si c'est un dossier ou nom et on récupére le vrai offset du nom avec un petit masque binaire, en n'oubliant pas que le nom ne veut rien dire pour la racine (idx == 0). Ensuite, si le descripteur est un fichier, on retourne directement son couple (data, size), sinon on va itérer sur tous les indexes entre le premier fils et le dernier fils et on va ajouter tous les descripteurs dans l'ensemble des fils. Et c'est fini !

Pour conclure cet article, on va dumper tous les fichiers et dossiers dans un répertoire sur notre PC. Il suffit de parcourir l'arbre que read_descr, appliqué à la racine, nous donne :

>>> from os.path import exists, join
>>> from os import mkdir
>>> def dump(name, data, where):
...     print 'dumping', name, 'to', where
...     if isinstance(data, dict): # directory
...         path = join(where, name)
...         if not exists(path):
...             mkdir(path)
...         for name, data in data.iteritems():
...             dump(name, data, path)
...     else:
...         print data[0], data[1]
...         data = read_at(data[0], data[1])
...         open(join(where, name), 'wb').write(data)

C'est gagné © ! Pour la prochaine et dernière partie nous verrons comment implémenter un filesystem sous Linux pour pouvoir faire simplement un mount sur notre image de DVD de Wii et y accèder comme on accèderait à une clé USB, sans avoir besoin de tout dumper.

2011
01.01

En cette nuit qui marque le passage vers l'année 2011, je me lance dans l'écriture d'une petite série d'articles sur la lecture d'un disque de Wii en Python. Lire un disque de Wii, pour moi, c'est être capable de lire tout d'abord le nom et l'ID du jeu, mais aussi être capable de lire le système de fichiers du disque pour accèder à l'exécutable ou aux données « brutes » du jeu. Je prévois pour cela 3 articles : le premier, celui ci, ira jusqu'à la lecture des secteurs décryptés de la partition de jeu du disque. Le deuxième parlera de la lecture du système de fichiers de la console : répertoires, fichiers, etc. Enfin, le troisième implémentera ce système de fichier via fuse-python ou PyFS selon mes envies pour pouvoir simplement le monter sur un système Linux. C'est parti !

Je n'ai pour le moment qu'une seule image sous la main : celle de Tales of Symphonia : Dawn of the new World (dont je ferai surement une review ici dans une semaine, par ailleurs), version PAL, dont l'ID est RT4PAF (sha1sum: b2fb05a7fdf172ea61b5d1872e6b121140c95822). Je vais donc me baser là dessus pour faire mes tests, puis si nécessaire fixer des choses quand je trouverai une autre image qui ne fonctionne pas. Au niveau de la documentation sur laquelle je me suis basé, il y a tout d'abord le code source de l'émulateur Dolphin (dans le dossier Source/Core/DiscIO) et le site WiiBrew, un wiki qui contient beaucoup d'informations techniques sur la Wii. Merci beaucoup à eux \o/

Quelques facts sur les disques de Wii : ils commencent par un header qui contient des metadata sur le jeu (son nom, son ID, le numéro du CD, le type de disque, etc.) à l'adresse 0. À l'adresse 0x40000 se trouvent 32 octets qui représentent la table des partitions du disque. En effet, un disque de Wii contient en général plusieurs partitions : une pour le jeu et d'autres qui contiennent par exemple les upgrades. Chacune de ces partitions a un type; celui qui nous intéressera est le type 0, partition de données. Enfin, cette partition commence elle même par un header qui contient notamment des informations nécessaires au déchiffrement des données du disque. En effet, toutes les données contenues dans une partition de Wii sont chiffrées en utilisant le bien connu algorithme AES avec une clé de 128 bits.

Tous les exemples que je donne ici seront uniquement des transcripts d'un shell Python. Je publierai un module qui marchera bien quand j'aurai tout fini et testé, donc surement en même temps que la partie 3 de cette série. Commençons par importer des modules utiles et ouvrons notre fichier :

Python 2.7.1 (r271:86832, Dec 20 2010, 11:54:29) 
[GCC 4.5.1 20101125 (prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import namedtuple
>>> from struct import unpack as up
>>> from Crypto.Cipher import AES
>>> fp = open('tos-2.img', 'rb')

Ensuite on va lire le header. C'est plutôt simple car il a un format fixe et des champs de longueur non variables :

>>> DiscHeader = namedtuple('DiscHeader',
...     'disc_id game_code region_code maker_code disc_number disc_version '
...     'audio_streaming stream_bufsize wii_magic gc_magic title'
... )
>>> disc_hdr = DiscHeader(*up('>c2sc2sBBBB14xLL64s', fp.read(96)))
>>> disc_hdr
DiscHeader(disc_id='R', game_code='T4', region_code='P', maker_code='AF',
           disc_number=0, disc_version=0, audio_streaming=0,
           stream_bufsize=0, wii_magic=1562156707, gc_magic=0,
           title='Tales of Symphonia: Dawn of the New World' + n*'\x00')

Étape suivante, la table des partitions. On va déjà regarder à quoi s'attendre en utilisant xxd :

$ xxd -s 0x40000 -l 48 tos-2.img
0040000: 0000 0002 0001 0008 0000 0000 0000 0000  ................
0040010: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Elle est organisée en 4 volume group qui contiennent plusieurs partitions. Une partition est donc indexée par son numéro de VG et son index dans le VG. La table que l'on vient de dumper contient pour chaque VG deux entiers de 32 bits : le premier est le nombre de partitions dans le VG, le deuxième est l'offset en blocs de 32 bits (et pas en octets, il faut donc multiplier par 4) de la table des partitions du VG. Ici, on a donc le premier VG qui contient 2 partitions, et la table qui décrit ces partitions se trouve à l'adresse 0x10008 * 4 = 0x40020 :

$ xxd -s 0x40020 -l 16 tos-2.img
0040020: 0001 4000 0000 0001 03e0 0000 0000 0000  ..@.............

Chaque entrée de cette table contient tout d'abord l'offset vers la partition (en blocs de 32 bits comme au dessus) mais également le type de partition. En gros, il faut retenir que type 1 = update firmware et type 0 = données du jeu. On a donc une partition d'update et une partition de données ici. On va coder un petit truc pour dumper toutes les entrées des VG avec Python histoire de pouvoir visualiser ça un peu mieux :

>>> PartEntry = namedtuple('PartEntry', 'offset type')
>>> def read_part_entry(offset):
...     fp.seek(offset)
...     (data_offset, type) = up('>LL', fp.read(8))
...     data_offset *= 4
...     return PartEntry(data_offset, type)
>>> 
>>> read_part_entry(0x40020)
PartEntry(offset=327680, type=1)
>>> read_part_entry(0x40028)
PartEntry(offset=260046848, type=0)
>>> 
>>> VGEntry = namedtuple('VGEntry', 'part_count table_offset')
>>> def read_vg_entry(offset):
...     fp.seek(offset)
...     (part_count, table_offset) = up('>LL', fp.read(8))
...     table_offset *= 4
...     return VGEntry(part_count, table_offset)
... 
>>> read_vg_entry(0x40000)
VGEntry(part_count=2, table_offset=262176)
>>> read_vg_entry(0x40008)
VGEntry(part_count=0, table_offset=0)
>>> 
>>> def read_part_table():
...     base_off = 0x40000
...     vgs = {}
...     for vg_num in xrange(4):
...         vg_ent = read_vg_entry(base_off + 8 * vg_num)
...         if vg_ent.part_count == 0:
...             continue
...         vgs[vg_num] = {}
...         for part_num in xrange(vg_ent.part_count):
...             off = vg_ent.table_offset + 8 * part_num
...             part = read_part_entry(off)
...             vgs[vg_num][part_num] = part
...     return vgs
... 
>>> read_part_table()
{0: {0: PartEntry(offset=327680, type=1),
     1: PartEntry(offset=260046848, type=0)}}

Concrétement, la partition d'upgrade firmware ne m'intéresse vraiment pas. Je vais me concentrer sur la partition qui contient les données du jeu, donc la deuxième partition du VG 0 (qui est de type 0, donc données de jeu). Comme à peu près tout sur la Wii, c'est chiffré en AES avec une clé qui est sur le DVD. Cette clé est elle même chiffrée avec une master key qui est dans le firmware de la console. Le tout est signé, mais comme on ne veut rien modifier on ignore totalement la signature : elle ne nous intéresse pas.

AES est un algorithme de chiffrement symétrique qui procède par blocs de 16 octets. Il peut être utilisé dans différents modes : ECB, CBC ou CFB. CBC, le mode que l'on va utiliser, prend en entrée la valeur précédente (ou, dans le cas du premier bloc, l'initial value aka. IV), le bloc courant et la clé. À partir de cela il chiffre ou déchiffre le bloc, qui devient la valeur précédente pour le bloc suivant, et ainsi de suite. Notre premier objectif va être de récupérer ce que l'on appelle la title key, qui est la clé de chiffrement de la partition de données. Elle est chiffrée avec un IV fourni sur le DVD (nommé title id) et une clé qui se trouve dans le firmware et qui est maintenant publique. Toutes les infos qu'ils nous faut sont donc à notre disposition, soit dans le header de la partition (appelé ticket) soit en dur. On va donc commencer par parser le header de la partition :

>>> Ticket = namedtuple('Ticket',
...     'enc_tit_key tit_id data_off data_len'
... )
>>> part = read_part_table()[0][1]
>>> fp.seek(part.offset)
>>> ticket = Ticket(*up('>447x16s13x16s204xLL', fp.read(704)))

Pour une raison louche que je ne connais pas (ping Nintendo ? 🙂 ), seuls les 8 premiers octets de l'IV sont utilisées, les 8 autres sont mis à 0. Ça ne change pas grand chose en vrai. On va donc se servir de tout ce qu'on a pour initialiser notre décodeur AES et décrypter la clé :

>>> master_key = '\xeb\xe4\x2a\x22\x5e\x85\x93\xe4'
>>> master_key += '\x48\xd9\xc5\x45\x73\x81\xaa\xf7'
>>> 
>>> iv = ticket.tit_id[:8] + '\x00' * 8
>>> 
>>> aes = AES.new(master_key, AES.MODE_CBC, iv)
>>> key = aes.decrypt(ticket.enc_tit_key)
>>> key
'U\x84\xfb\x8b\x10\xdfu=B;\xdcyF\xd4G\x9d'

Voilà, nous sommes maintenant en mesure de déchiffrer le contenu de la partition. Les données en elles mêmes commencent après le ticket, à un offset qui se trouve dans ce ticket. À partir de là, on trouve une succession de clusters de 0x8000 octets. Ces clusters sont séparés en tout d'abord 0x400 octets de hashs pour vérifier l'intégrité des données, et 0x7C00 de données chiffrées. La clé de chiffrement est celle que l'on a récupéré précédemment, l'IV se trouve dans le bloc d'intégrité, à l'offset 0x3D0 par rapport au début de la partition. Avec tout ça il est aisé de coder la fonction de lecture d'un cluster :

>>> def read_cluster(idx):
...     data_offset = part.offset + ticket.data_off * 4
...     cluster_offset = data_offset + idx * 0x8000
...     fp.seek(cluster_offset)
...     data_enc = fp.read(0x8000)
...     iv = data_enc[0x3D0:0x3E0]
...     aes = AES.new(key, AES.MODE_CBC, iv)
...     return aes.decrypt(data_enc[0x400:])

Testons ça en décodant les 20 premiers clusters et en regardant leur contenu :

>>> for i in xrange(20):
...     open('/tmp/cluster%d' % i, 'wb').write(read_cluster(i))

$ strings /tmp/cluster* | less
[...]
This Apploader built %s %s for RVL
APPLOADER WARNING >>> Older version of DEVKIT BOOT PROGRAM.
APPLOADER WARNING >>> Use v1.07 or later.
APPLOADER ERROR >>> FSTLength(%d) in BB2 is greater than FSTMaxLength(%d)
APPLOADER ERROR >>> Debug monitor size (%d) should be a multiple of 32
APPLOADER ERROR >>> Simulated memory size (%d) should be a multiple of 32
[...]

Une seule chose à dire : success ! A priori il y a toutes les chances que les données soient bien déchiffrées (ça marche pour les 20 premiers clusters). On va terminer en faisant un dump complet de la partition pour pouvoir étudier ça plus facilement après. On a également la taille de la partition dans le ticket, donc c'est parti !

>>> nclusters = ticket.data_len * 4 / 0x8000
>>> out_fp = open('/path/to/tos-2-dumped.img', 'wb')
>>> for i in xrange(nclusters):
...     print '%f%%' % (i * 100.0 / nclusters)
...     out_fp.write(read_cluster(i))

Et voilà qui conclut cette première partie. Comme dit plus haut, dans la deuxième partie je vais me concentrer sur le système de fichier de la partition que je viens de récupérer. Ça promet 🙂 .

2010
12.24

Je viens de rentrer chez moi pour les vacances de noël (mes seules vacances de l’année \o/), du coup j’en profite bien évidemment pour squatter la PS3 familiale accompagnée de son ampli surround 5.1 qui embellit l’output HDMI (upscale, modification du contraste, etc.). Il se trouve que j’étais très frustré lorsque juste avant ma rentrée en septembre je me suis retrouvé bloqué devant l’avant dernier boss d’Eternal Sonata, du coup j’ai terminé ça tout de suite pour ne pas l’oublier 🙂 .

D’abord, qu’est-ce qu’Eternal Sonata ? C’est un RPG fait par les gens de tri-Crescendo (des anciens de chez Namco qui ont bossé sur Tales of Phantasia, Star Ocean, Baten Kaitos et quelques autres J-RPG) qui est sorti tout d’abord sur Xbox 360 puis sur PS3. Adoptant un graphisme très simple basé sur du cell shading, c’est un jeu que j’ai trouvé très agréable graphiquement (c’est un style que j’apprécie particulièrement, d’ailleurs les Tales of récents ont un style graphique très proche). C’est simplement joli et épuré tout en étant détaillé. Une galerie de screenshots est disponible sur jeuxvideo.com. Mais le graphisme n’est pas la seule chose élégante dans ce jeu : la musique est simplement sublime. Et bien heureusement pour un jeu comme celui ci, vous verrez pourquoi un peu plus loin si vous ne vous en doutez pas. Motoi Sakuraba (grand compositeur de musiques de J-RPG, qui s’occupe des Tales of, Star Ocean, Baten Kaitos, et plein d’autres jeux qui ont des musiques géniales) a simplement tout donné sur ce jeu, et c’est sans aucun doute à mes oreilles sa meilleure OST. Je vous laisse le soin de trouver des extraits en ligne (Youtube en a plein), ça en vaut la peine.

Parlons un peu plus en détail du scénario, tout en restant relativement spoiler-free. Première chose surprenante : le personnage principal du jeu est Frédéric Chopin, le compositeur français du 19ème siècle. Tout le jeu se déroule dans un de ses rèves qu’il a avant de mourir (ou pas ? haha, doutez !). Du coup, la musique “classique” (romantique plus précisément, mais accordons nous ce raccourci) a une grande part dans le jeu : la plupart des personnages et des lieux ont des noms issus du champ lexical de la musique (une liste loin d’être exhaustive : Beat, Allegretto, Staccato, Falsetto, Jazz, Legato, Polka, Mazurka et Crescendo sont des noms de personnages, le pays voisin s’appelle Baroque). C’est un choix comme les autres, au moins on voit autour de quoi ça tourne. Le scénario est aussi très proche de la vie du compositeur, et entre les différents chapitres (au nombre de 7 ou 8 il me semble) un interlude culturel nous présente à chaque fois une oeuvre de Chopin et le contexte dans lequel il l’a composé. Certains diront que c’est très bien, d’autres diront que ça casse l’immersion, à vrai dire je suis assez d’accord avec ces deux avis (bonne manière de ne pas trancher !).

Dans ce contexte musical, un scénario à la trame assez voire trop simple se déroule entre les différents personnages dont se composent le groupe. J’ai été assez déçu par le scénario, qui est vraiment sans trop de rebondissements (le grand méchant est nommé au début et il reste le grand méchant jusqu’à la fin du jeu, par exemple) et très classique (un pays dont le dirigeant est corrompu par le pouvoir, un groupe de rebelles qui jurent tout sur la justice). Pour plus d’informations et pour perdre votre vie, tvtropes répértorie pas mal de “clichés” (tropes are not clichés, je sais, mais je ne vois pas de meilleure traduction) qui se retrouvent dans ce jeu et notamment dans son scénario. C’est un peu triste et c’est surement le plus gros point négatif de ce jeu. Il y a quand même pas mal d’originalité autour qui sauve largement la mise.

Qui dit J-RPG dit combats (j’offre un #FollowFriday si un nerd me trouve un J-RPG sans combats). Et le système mis en place dans Eternal Sonata est génial. Entrainant, dynamique, demandant de la réflexion et de l’habilité, et qui change en plus régulièrement au fur et à mesure du jeu. Que demande le peuple. En très gros, chaque personnage (ami ou ennemi) participant au combat (maximum 3 dans votre groupe) a un temps d’action alloué à chaque tour. Ce temps se répartit en temps tactique (qui se termine dès que le personnage réalise une action, il permet juste de réflechir et d’observer) et temps d’action (qui lui inclut les déplacements, les attaques, les utilisations d’items et tout le reste). Les personnages sont disposés sur une map sur laquelle tout le monde peut se déplacer librement (pas de grille ni de position fixe, on se déplace vraiment comme on veut). Cette map a des zones de lumière et des zones d’ombre, qui influencent plusieurs éléments : le type de monstre (parfois certains monstres se comportent différemment a la lumière et à l’ombre) et les attaques spéciales réalisables par vos personnages (chaque personnage peut utiliser une attaque de lumière ou une attaque d’ombre selon là où il se trouve). La puissance des attaques spéciales dépend du nombre de coups consécutifs portés aux ennemis avant l’utilisation de l’attaque. Il est donc souvent intéressant de ne pas utiliser une attaque spéciale pour maximiser le compteur de coups consécutifs. Pour rendre tout ça très drôle, toutes les constantes et certaines règles sont modifiées à chaque chapitre du jeu. Par exemple, au dernier chapitre il n’y a pas de temps tactique et 4s de temps d’action, mais si un personnage utilise une attaque spéciale avec plus de 24 coups consécutifs avant, il est possible d’enchainer jusque 3 attaques. Ça peut parraitre complexe mais tous les concepts sont introduits petit à petit, et c’est très vite entrainant.

Quelques déceptions quand même en plus du scénario : déjà, la durée de vie est assez courte. Je l’ai terminé en 35h sachant que je suis loin d’être un hardcore gamer (je suis même plutôt mauvais). Ensuite, certains combats de boss sont très durs alors que les monstres qui se trouvent avant sont ridiculement simples (je pense notamment à l’avant dernier boss du jeu où je suis resté coincé un bon moment et qui demande pas mal de skill et de réflèxes pour le battre). Aussi, les différences entre la version Xbox 360 et la version PS3 sont assez énormes, et même si c’est dans ce cas à mon avantage c’est quelque chose que je ne cautionne pas trop. La fin du scénario est même complétement différente sur PS3, et il y a 2 ou 3 personnages jouables supplémentaires.

Pour résumer (TL;DR toussa), c’est un jeu que je conseille vraiment si vous aimez les JRPG. Même si son scénario reste classique, le pari de raconter la vie du compositeur Frédéric Chopin au travers d’un jeu vidéo est assez osé et plutôt bien réussi. Les graphismes sont attrayants, la musique est géniale, le système de combats est très entrainant et la difficulté n’est ni trop basse, ni trop haute à mon goût. Si vous avez une PS3, faites vous un joli cadeau de noël 🙂 .

2010
12.13

Dans le cadre d'un des projets sur lesquels je travaille, je dois récupérer les données de localisation d'un programme depuis des DLL de ressources. C'est une méthode assez usuelle sous Windows pour gérer la localisation d'un projet : toutes les chaines sont référencées dans le code par un ID, et elles sont stockées dans une DLL à part qui peut être différente selon la langue. C'est un peu ce qui est fait sous Linux avec gettext, mais en moins complexe et légèrement plus intégré.

Sous Windows, tous les fichiers exécutables (EXE, DLL, OCX, SYS, etc.) sont stockés au format PE (Portable Executable). Comme avec ELF sous Linux, ce format est composé de différentes sections. Celle qui va nous intéresser dans cet article est la section .rsrc, qui contient toutes les ressources contenues dans un exécutable. Une ressource, ça peut être tout et n'importe quoi. Windows en a quelques types prédéfinis qu'il sait gérer (icone, description d'une interface graphique, string table) mais il y a un type passe partout, BINARY, qui permet de stocker ce que l'on veut.

Notre but est ici d'accèder à une ressource de type String Table dans une DLL, afin dans un premier temps d'afficher toutes les chaines, puis d'en récupérer une à partir de son ID.

Dans un premier temps, on va créer une DLL contenant une string table avec MinGW pour pouvoir tester avec des données que l'on connait.

$ cat > foo.rc <<EOF
#define IDS_HELLO 1
#define IDS_WORLD 2

STRINGTABLE
{
IDS_HELLO, "Hello"
IDS_WORLD, "World"
}
EOF
$ mingw32-windres foo.rc -o foo.o
$ mingw32-gcc -shared -o foo.dll foo.o
$ strings -e l foo.dll
Hello
World

La prochaine étape, c'est de lire la documentation du format PE qui se trouve sur le site de Microsoft. En vrai, ça n'est pas important car je vais détailler les choses ici. Commençons donc le parsing de notre PE.

À la recherche de .rsrc

On va commencer par lire notre fichier DLL. En entier car je suis un bourrin et car je n'ai pas envie de me faire chier pour cet article, mais on pourrait très bien être plus fin.

>>> dll = open('foo.dll', 'rb').read()

Un exécutable au format PE commence par du code compilé qui affiche « This program cannot be run in DOS mode. », ceci pour la compatiblité MS-DOS. À l'offset 0x3C se trouve l'offset du header PE.

>>> import struct
>>> (pe_header_offset,) = struct.unpack("B", dll[0x3C:0x3C+1])
>>> hex(pe_header_offset)
'0x80'

À l'adresse que l'on vient de lire se trouvent 4 octets magiques permettant de s'assurer que l'on est bien en présence d'un fichier PE :

>>> data = dll[pe_header_offset:pe_header_offset+4]
>>> (pe_magic_dword,) = struct.unpack("4s", data)
>>> pe_magic_dword
b'PE\x00\x00'

Ensuite se trouve le header PE, mesurant 20 octets, et qui ne contient pas énormément de choses qui nous intéressent ici. Il est suivi d'un header « optionnel » (qui est présent dans 99.9% des fichiers PE vu qu'il est obligatoire sous Windows), dont la taille est variable en fonction de son type. Sa taille est indiquée dans un des champs du header PE : SizeOfOptionalHeader, de 2 octets, à l'offset 16.

>>> data = dll[pe_header_offset+4+16:pe_header_offset+4+18]
>>> (opt_hdr_size,) = struct.unpack("H", data)

Après le header PE et le header optionnel se trouve la table des sections. Il va falloir la lire intégralement puis la parcourir afin de trouver la section .rsrc, contenant les ressources inclues dans un fichier PE.

>>> def read_section(idx):
...     offset = pe_header_offset + 4 + 20 + opt_hdr_size
...     sect_offset = idx * 40
...     real_off = offset + sect_offset
...     data = dll[real_off:real_off+40]
...     tup = struct.unpack('8sIIII', data[:24])
...     return SectionHeader(*tup)
...
>>> read_section(0)
SectionHeader(name=b'.text\x00\x00\x00', virtual_size=2820,
              virtual_address=4096, size_of_data=3072,
              pointer_to_data=1024)
>>> read_section(1)
SectionHeader(name=b'.data\x00\x00\x00', virtual_size=8,
              virtual_address=8192, size_of_data=512,
              pointer_to_data=4096)
>>> read_section(2)
SectionHeader(name=b'.rdata\x00\x00', virtual_size=272,
              virtual_address=12288, size_of_data=512,
              pointer_to_data=4608)
>>> read_section(3)
SectionHeader(name=b'.bss\x00\x00\x00\x00', virtual_size=104,
              virtual_address=16384, size_of_data=0,
              pointer_to_data=0)

Le nombre total de sections est inclus dans le header PE, à l'offset 2, et mesure deux octets :

>>> data = dll[pe_header_offset+4+2:pe_header_offset+4+4]
>>> (num_of_sections,) = struct.unpack('H', data)

Lisons donc toutes les sections, ou plutôt tous les headers de sections :

>>> sect_headers = [read_section(i) for i in range(num_of_sections)]

On peut donc maintenant facilement trouver la section que l'on souhaite : .rsrc :

>>> for sh in sect_headers:
...     if sh.name.startswith(b'.rsrc'):
...             rsrc = sh
...
>>> rsrc
SectionHeader(name=b'.rsrc\x00\x00\x00', virtual_size=140,
              virtual_address=36864, size_of_data=512,
              pointer_to_data=7680)

Et on va maintenant lire tout son contenu, qui va de la position pointer_to_data à pointer_to_data + size_of_data.

>>> start_addr = rsrc.pointer_to_data
>>> end_addr = rsrc.pointer_to_data + rsrc.size_of_data
>>> rsrc_data = dll[start_addr:end_addr]

À l'intérieur de la section

La section .rsrc contient principalement une structure hiérarchique (un arbre quoi) qui permet d'y trouver facilement ce que l'on y cherche. Pour citer la documentation (en traduisant), « Le design général supporte 2**31 niveaux de hiérarchie. En pratique, Windows en utilise 3. ». Ces trois niveaux sont le type de ressource, son ID et la langue de la ressource. Pour les chaines contenues dans une string table, le type est la constante RT_STRING (RT = Resource Type), définie dans winuser.h comme valant 6. L'ID (deuxième niveau) est l'ID de la chaine recherchée, divisée par 16, plus un (string_id / 16 + 1). Enfin, la langue par défaut est Anglais US, dont l'ID peut être trouvé dans cette liste : 1033.

En feuille de l'arbre se trouve un bloc de 16 chaines précédées de leur taille sur 2 octets. Les chaines ne contiennent pas de terminateur nul (\0), on a de toute façon leur taille. Maintenant, parsing time ! On commence par définir les quelques structures qui vont nous servir :

>>> TreeNode = namedtuple(
...     'TreeNode',
...     'characteristics timestamp ver_major ver_minor '
...     'name_entries id_entries'
... )
>>> TreeEntry = namedtuple(
...     'TreeEntry',
...     'label address'
... )
>>> TreeLeaf = namedtuple(
...     'TreeLeaf',
...     'address size'
... )

Quelques fonctions pour parser ces noeuds :

>>> def read_node(offset):
...     data = rsrc_data[offset:offset+16]
...     return TreeNode(*struct.unpack('IIHHHH', data))
...
>>> def read_entry(offset):
...     data = rsrc_data[offset:offset + 8]
...     return TreeEntry(*struct.unpack('II', data))
... 
>>> def read_leaf(offset):
...     data = rsrc_data[offset:offset+8]
...     return TreeLeaf(*struct.unpack('II', data))
... 

Et enfin, des fonctions de plus haut niveau, qui lisent respectivement une feuille de l'arbre et son arborescence :

>>> def read_data(addr):
...     leaf = read_leaf(addr)
...     data_off = leaf.address - rsrc.virtual_address
...     return rsrc_data[data_off:data_off+leaf.size]
... 
>>> def read_tree(offset):
...     node = read_node(offset)
...     child_off = offset + 16
...     
...     # Skip the named childs
...     child_off += 8 * node.name_entries
...     
...     childs = {}
...     for i in range(node.id_entries):
...         entry = read_entry(child_off)
...         child_off += 8
...         
...         # If the MSB is 1, it is an internal node
...         if entry.address & 0x80000000:
...             child = read_tree(entry.address & ~0x80000000)
...         else:
...             child = read_data(entry.address)
...         childs[entry.label] = child
...     return childs

On confirme le bon fonctionnement sur notre DLL de test :

>>> read_tree(0)
{6: {1: {1033: b'...'}}}

Il reste donc les données en feuille de l'arbre à parser. C'est en fait très simple, comme dit plus haut : 16 chaines UTF-16 préfixées par leur taille.

>>> def read_table_chunk(chunk):
...     li = []
...     while chunk:
...         (size,) = struct.unpack('H', chunk[:2])
...         chunk = chunk[2:]
...         string = chunk[:2*size]
...         li.append(string.decode('utf-16'))
...         chunk = chunk[2*size:]
...     return li
... 
>>> read_table_chunk(read_tree(0)[6][1][1033])
['', 'Hello', 'World', '', '', '', '', '', '', '', '', '', '', '', '', '']

Avec tout ça c'est presque fini, plus qu'à faire une fonction qui récupère une string depuis son ID. Trivial !

>>> def resource_string(id):
...     tree = read_tree(0)
...     chunk_id = id // 16 + 1
...     if 6 not in tree:
...         raise ValueError("no string table found")
...     if chunk_id not in tree[6]:
...         raise IndexError("string chunk not found")
...     if 1033 not in tree[6][chunk_id]:
...         raise ValueError("wrong language for the string table")
...     chunk = read_table_chunk(tree[6][chunk_id][1033])
...     return chunk[id % 16]
... 
>>> resource_string(1)
'Hello'
>>> resource_string(2)
'World'

Et voilà ! Tout cela est bien entendu améliorable pour tenir en compte de pas mal de choses (les différentes langues par exemple), mais les bases sont là.

2010
10.25

Afin de compiler du C pour le faire tourner sur une Fonera+, il est nécessaire d’avoir tous les outils adéquats. Parmi ces outils, le compilateur C est le plus évident, mais il ne servira à rien sans un assembleur et un linker (qui rassemble plusieurs fichiers assemblés en un binaire final). Un debugger est aussi un luxe dont on aura du mal à se passer. Heureusement pour nous, le projet GNU a créé des logiciels libres qui font tout ça : les GNU binutils, GCC et GDB. Il se trouve que le support de la cross-compilation avec ces outils est très bon, ce qui va bien nous arranger.

Tout d’abord, il a fallu décider de l’architecture précise pour laquelle compiler. En effet, le MIPS 4KEc de la Fonera+ peut fonctionner en mode little endian ou en mode big endian, au choix, et il n’est pas pratique de mélanger les deux. J’ai décidé d’utiliser pour DMMS le mode big endian, mais j’essaierai de temps en temps de compiler en mode little endian et de tester ça sur ma Fonera+.

Ensuite, on peut commencer à build les outils dont on aura besoin. J’ai trouvé pas mal de documentation sur le wiki de Linux MIPS ainsi que dans les PKGBUILD des paquets Archlinux sur l’Archlinux User Repository. Je vais tout de même tout résumer ici.

Comme vous le savez surement, la compilation d’un projet GNU se fait en général en deux étapes : la configuration (via le script ./configure) et la compilation (make). Lors de la configuration, on peut spécifier de compiler pour une certaine architecture cible, via l’option --target. L’architecture est désignée par ce que l’on appelle le target triplet qui est souvent sous la forme architecture-os-format. Dans notre cas, l’architecture sera mips, l’OS unknown (car on ne compile pas pour un OS connu) et le format elf, ce qui donne un triplet mips-unknown-elf. Si on voulait compiler pour du MIPS little endian, ça serait mipsel-unknown-elf (le « el » signifiant « little endian » mais inversé, notez l’humour très fin).

Maintenant que tout est expliqué, commençons. On va exporter quelques variables d’environnement dont on va se servir par la suite, notamment le chemin où installer les binaires compilés et le type de système pour lequel on compile :
$ export TARGET=mips-unknown-elf
$ export PREFIX=/usr/cross/fonera

Ensuite, on se place dans un dossier où il y a de l’espace disque (il faut environ 1.5G), puis on télécharge et on désarchive la dernière version de tous les outils qu’ils nous faut :
$ wget http://ftp.gnu.org/gnu/binutils/binutils-2.20.1.tar.bz2
$ wget http://ftp.gnu.org/gnu/gcc/gcc-4.5.1/gcc-core-4.5.1.tar.bz2
$ wget http://ftp.gnu.org/gnu/gdb/gdb-7.2.tar.bz2
$ for f in *.tar.bz2; do tar xjvf $f; rm $f; done;

C’est parti pour la compilation des différents logiciels. Tout d’abord, pour les binutils, rien de spécial :
$ ./configure --target=$TARGET --prefix=$PREFIX
$ make -j3 # environ 2 minutes sur mon i7
$ sudo make install

Vu que je suis curieux et impatient, testons l’assemblage d’une toute petite fonction en assembleur MIPS, qui fait simplement une boucle infinie (on écrit une instruction qui saute vers elle-même) :

$ cat > test.s <<EOF
loop_forever:
    j loop_forever
EOF

On assemble le code en utilisant les binutils que l’on vient de compiler :
$ /usr/cross/fonera/bin/mips-unknown-elf-as test.s -o test.o

Puis pour tester et être sûr que tout est bon, on va désassembler avec objdump, qui fait lui aussi partie des binutils :

$ /usr/cross/fonera/bin/mips-unknown-elf-objdump -d test.o
test.o:     file format elf32-bigmips
Disassembly of section .text:
00000000 <loop_forever>:
   0:   08000000        j       0 <loop_forever>
   4:   00000000        nop

L’output d’objdump nous dit que notre fichier compilé est au format elf32-bigmips (donc du MIPS big endian, impeccable) et on retrouve le code que l’on a mis dans notre test.s. Tout semble bon, on peut continuer la compilation des outils.

Maintenant, la bête : GCC. Comme il n’aime pas trop qu’on le compile directement dans le dossier qui contient ses sources, on va simplement créer un dossier gcc-build et se mettre dedans pour compiler. Au niveau des options du configure, on désactive une tonne de choses qui ne nous serviront pas : le support des threads (on ne compile pas pour linux ou pour windows, il n’aurait de toute façon pas su faire), la libgcj utilisée pour le Java (je doute vraiment en avoir besoin pour le moment), les bibliothèques dynamiques (on ne supportera pas ça tout de suite), le support de la compilation pour du little endian et des variantes de MIPS qui ne nous intéressent pas (--disable-multilib), la bibliothèque de GCC qui s’occupe de détecter les stack corruption, et enfin l’utilisation des headers de la libc de l’architecture cible, vu qu’on n’a pas de libc pour le moment. On définit également le CPU pour lequel on compile par défaut (un MIPS 4Kec). Quel bordel.
$ mkdir gcc-build && cd gcc-build
$ ../gcc-4.5.1/configure --target=$TARGET --prefix=$PREFIX --disable-threads --disable-libgcj --disable-shared --disable-multilib --disable-libssp --without-headers --with-arch=4kec --with-tune=4kec
$ make -j 3 # environ 5 minutes
$ sudo make install

Testons ça de la même manière que les binutils : compilons un test.c basique qui fait une boucle infinie, et désassemblons le :

$ cat > test.c <<EOF
void infinite_loop(void)
{
    for (;;)
        ;
}
EOF
$ /usr/cross/fonera/bin/mips-unknown-elf-gcc -c test.c
$ /usr/cross/fonera/bin/mips-unknown-elf-objdump -d test.o
test.o:     file format elf32-bigmips
Disassembly of section .text:
00000000 <infinite_loop>:
   0:   27bdfff8        addiu   sp,sp,-8
   4:   afbe0004        sw      s8,4(sp)
   8:   03a0f021        move    s8,sp
   c:   08000003        j       c <infinite_loop+0xc>
  10:   00000000        nop

Ça semble bien fonctionner ! Reste donc à compiler GDB, le debugger GNU. C’est en gros la même chose que pour les binutils :
$ ./configure --target=$TARGET --prefix=$PREFIX
$ make -j3 # environ 2 minutes
$ sudo make install

Voilà, nous avons maintenant toute une flopée d’outils pour travailler avec une architecture MIPS : un assembleur, un désassembleur, un linker, un compilateur, un debugger, un stripper, etc. Et c’est donc fini pour cette étape 0.5, dont j’avais prévu de mettre le contenu avec d’autres choses mais ça s’est avéré un peu trop long. Peut-être qu’on va enfin pouvoir exécuter un truc sur ce routeur la prochaine fois ?

2010
10.23

Vouloir développer un noyau de système d’exploitation pour une machine dont on ne connait presque rien, c’est une chose. Le faire, c’en est une autre. Jusqu’à aujourd’hui je ne connaissais rien du tout de l’intérieur de la Fonera+, comment booter un système, et toutes ces choses nécessaires pour pouvoir commencer à effectivement coder un noyau. Fort heureusement, on trouve beaucoup de documentation sur le net, notamment sur les wikis des firmwares alternatifs de routeur. Du coup, j’ai pu glaner un grand nombre d’informations que je vais résumer dans cet article.

Commençons par le matériel qui se trouve dans la Fonera+. La page de wiki d’OpenWRT parle en détail du matériel de la Fonera normale, et d’après une comparaison sur Tom’s Hardware la seule différence entre la Fonera et la Fonera+ est dans le contrôleur Ethernet. Le CPU est un MIPS 4KEc, donc un CPU MIPS 32 bits qui fonctionne aussi bien en little endian qu’en big endian. D’ailleurs, comme l’indique le wiki officiel de Fon, leur firmware officiel tourne en big endian alors qu’OpenWRT tourne normalement en little endian. Le routeur est doté de 16 Mio de RAM et 8 Mio de Flash. Le reste nous importe peu pour le moment.

Pour exécuter notre noyau sur la Fonera+, il faut trouver un moyen de le transférer sur le routeur puis de lui faire exécuter le code. Sur un PC classique, le BIOS exécute par défaut le premier secteur du premier disque dur pour booter l’OS. En général, il y a un intermédiaire nommé le bootloader (dont GRUB, NTLDR, LILO et quelques autres sont des exemples) qui se charge de booter un noyau qui est sur une partition du disque et pas dans le premier secteur (car c’est plus pratique pour tout le monde que le noyau soit un fichier comme les autres). La Fonera+ a elle aussi un bootloader, nommé RedBoot. Il boote par défaut sur un fichier de la ROM, mais il peut faire plein d’autres choses. Voyons tout d’abord comment on peut y accèder et le contrôler.

On peut rentrer dans la console série de RedBoot de deux manières : via un port série que l’on bricole et que l’on branche sur des bornes de la carte de la Fonera+ (vraiment pas pratique et je suis pas très bricolo), ou simplement en ethernet ! Par défaut, RedBoot sur la Fonera+ est accessible via telnet sur le port 9000 et l’IP 192.168.1.1 pendant 2s avant de démarrer l’OS par défaut de la Fonera+. En s’y connectant pendant les 2s on peut interrompre le chargement de l’OS (via un simple ^C) et rentrer dans la console de RedBoot qui permet de faire tout plein de choses. Sous Linux, en supposant que le routeur est branché directement via un cable ethernet à l’interface eth0 de la machine, la marche à suivre exacte est la suivante :

  • Configurer le réseau sur la machine pour faire transiter tout 192.168.1.0 vers le port ethernet qui nous intéresse : sudo ip route add 192.168.1.0/24 via eth0
  • Débrancher l’alimentation de la Fonera+ pour lui faire subir un reboot complet
  • Créer un fichier ~/.telnetrc via les commandes suivantes afin de configurer telnet en mode bufferisé par ligne (nécessaire pour que le ^C fonctionne !) : echo -e "192.168.1.1\n\tmode line" >> ~/.telnetrc
  • Préparer la machine à se connecter à la Fonera+ dès qu’elle sera accessible : sudo arping -f 192.168.1.1; telnet 192.168.1.1 9000
  • Brancher l’alimentation de la Fonera+ et se préparer à faire ^C dès que la connexion telnet sera établie.

Si tout se passe bien, il devrait s’afficher quelque chose ressemblant à ceci sur la machine locale : ARPING 192.168.1.1 from 192.168.2.202 eth0
Unicast reply from 192.168.1.1 [00:18:84:A0:BF:D0] 1.048ms
Sent 8 probes (8 broadcast(s))
Received 1 response(s)
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
== Executing boot script in 1.030 seconds - enter ^C to abort
^C
RedBoot>

Nous sommes donc dans la console de RedBoot. À partir de là, on peut faire plein de choses utiles dont je ne parlerai pas forcèment (pour ça il y a la commande help). Parmi ce qui nous intéresse, on peut afficher la configuration de RedBoot via la commande fconfig -l -n. Sur ma Fonera+, ça renvoie ceci : RedBoot> fconfig -l -n
boot_script: true
boot_script_data:
.. fis load -l vmlinux.bin.l7
.. exec
boot_script_timeout: 2
bootp: false
bootp_my_gateway_ip: 0.0.0.0
bootp_my_ip: 192.168.1.1
bootp_my_ip_mask: 255.255.255.0
bootp_server_ip: 192.168.1.254
console_baud_rate: 9600
gdb_port: 9000
info_console_force: false
net_debug: false

On peut remarquer là dedans les choses suivantes :

  • On peut redéfinir le script RedBoot lancé au démarrage du routeur. Quand on aura un truc qui fonctionne bien, on pourra le faire se lancer automatiquement avec ça.
  • On peut définir un serveur BOOTP. BOOTP, c’est l’ancêtre de DHCP, et le principe est bien expliqué sur la page Wikipédia du protocole. C’est très bon, ça veut dire qu’on peut potentiellement faire des tests de boot sans avoir à mettre notre noyau sur la ROM à chaque modification. RedBoot ira simplement demander le noyau sur le réseau et on lui renverra le fichier qui va bien.
  • RedBoot se comporte comme un remote target gdb. Cela signifie qu’on peut utiliser le debugger GDB pour debugger notre noyau pendant qu’il est en fonctionnement sur le routeur. Encore une fois, ça sera très pratique quand on devra programmer notre noyau et qu’on aura des bugs louches.

L’autre commande qui va surement être intéressante, c’est load. D’après sa documentation, elle permet de télécharger un binaire dans la RAM pour ensuite l’exécuter. Pour ça, RedBoot peut utiliser soit le protocole TFTP, soit le protocole HTTP. Apparemment HTTP plante dans la version de RedBoot installée sur la Fonera+, tant pis.

Vu que je suis curieux de voir comment ça marche, j’ai mis en place un serveur TFTP sur ma machine pour faire télécharger un fichier à RedBoot. Pour cela, j’ai créé un fichier rempli de zéros avec dd if=/dev/zero of=dmms.bin count=42, puis je l’ai placé dans le dossier /var/tftpboot. J’ai configuré l’interface eth0 de ma machine en point to point avec la commande sudo ifconfig eth0 192.168.1.2 pointopoint 192.168.1.1 puis j’ai lancé /etc/rc.d/tftpd start pour servir le fichier. Ensuite, sur la Fonera+, j’ai lancé load -h 192.168.1.2 dmms.bin, ce qui a effectivement téléchargé le fichier, mais le format n’est pas reconnu (normal, que des zéros) : RedBoot> load -h 192.168.1.2 dmms.bin
Using default protocol (TFTP)
Unrecognized image type: 0x0

On a donc maintenant une méthode permettant d’exécuter du code sur la bête. La prochaine étape sera d’exécuter quelque chose d’un peu plus utile. Peut-être un hello world ? 🙂

Voir aussi :

2010
10.22

Il y a 3 ans, Mickaël Thomas (mickael9) a gagné une Fonera+ lors d’un concours sur Net-Actuality. Ne sachant pas quoi en faire, il a donc décidé de me la donner (merci à lui !). Il se trouve que je n’avais pas non plus d’utilisation pour ce routeur à l’époque, et il est donc resté dans un carton jusqu’au début de ce mois d’Octobre.

Pour ceux qui ne connaitraient pas la Fonera+, c’est un routeur vendu par Fon qui est sensé fournir un access point privé chiffré (classique), mais également un access point public pour tous les utilisateurs d’une Fonera. Ainsi, si je vais en vacances quelque part et que je partage ma Fonera, je peux utiliser la connexion d’autres gens partageant également leur Fonera. C’est un peu le principe du FreeWifi, mais c’est bien plus vieux ! Sur un plan technique de base, la Fonera+ (aussi connue sous le nom de FON2201) est un routeur muni d’une antenne wifi et de deux ports ethernet, un utilisé pour l’upstream et un downstream. Le microgiciel tournant sur ce routeur est une variante verrouillée d’OpenWRT, c’est donc du Linux derrière.

Pour parler un peu de moi, j’utilise dans mon appart un routeur Linksys WRT54GC. Très petit, assez configurable, wifi, 5 ports ethernet, que du bonheur. Le seul problème c’est que le WRT54GC n’a pas assez de RAM ni de ROM (« disque dur » du routeur, permettant de stocker l’OS et la configuration) pour faire tourner un firmware alternatif comme OpenWRT ou DD-WRT. Or je suis actuellement dans ma période où je m’amuse beaucoup avec le réseau, et il y a 2 semaines j’ai eu envie de connecter mon routeur à mon VPN en lançant OpenVPN dessus. Sauf que le WRT54GC ne supporte pas ça. Je me suis donc souvenu que j’avais une Fonera+ dans un carton, qui elle permet de faire tourner OpenWRT. Du coup, j’ai flashé ça avec un outil que j’ai trouvé sur le net, et j’ai un OpenWRT tout propre tout neuf dessus. Mais en fait, la connexion de ma résidence est trop crappy pour avoir un VPN qui a moins de 400ms de ping, donc j’ai abandonné cette idée.

Par contre, une Fonera+, c’est un système que je ne connais pas du tout. Le hardware m’est totalement inconnu, et je n’ai jamais vraiment programmé sur d’autres choses que des architectures Intel classiques (x86 ou x86_64). Du coup, cette Fonera+ est une bonne occasion de m’amuser à programmer pour un système « exotique » (pas tant que ça, mais pour moi si). Et quoi de mieux pour maitriser une plateforme que de coder un noyau de système d’exploitation pour la machine en question ?

Maintenant que j’ai raconté ma vie, je vais donc présenter mon projet : DMMS. Cet acronyme veut simplement dire delroth’s Minimalist MIPS System et sera le nom par lequel je me référerai à ce noyau de système d’exploitation pour ma Fonera+. À noter le « MIPS » dans l’acronyme : la Fonera+ est basée sur un CPU d’architecture MIPS 4KEc. Ça tombe bien, j’ai eu à bosser un peu avec du MIPS l’an dernier quand je programmais un recompilateur de binaire PlayStation (projet de l’échec, mais ça n’est pas le sujet de ce billet). Je n’ai pas vraiment d’objectifs avec ce noyau. Je coderai ce qui me passe par la tête et à un rythme plus ou moins régulier. Disons que j’aurai réalisé un premier objectif quand j’aurai un serveur telnet qui tournera sur ma Fonera+. Mais avant ça il faudra programmer un driver réseau, une pile IP et énormément d’autres choses.

À chaque fois que je jugerai intéressant de le faire, j’écrirai un article sur l’avancement du projet. Je tiendrai compte des commentaires sur les billets et je tâcherai d’y répondre, j’espère donc que cette série ne sera pas que du write-only pour moi 🙂 .

Sur ce, à bientôt pour un article sur l’étape 0 de l’écriture de DMMS : mes expérimentations avec RedBoot, le bootloader de la Fonera+.