Que contient un fichier .wasm? Présentation de Wasm-Decompile

Nous avons de nombreux compilateurs et autres outils à notre disposition pour créer et travailler avec des fichiers .wasm. Le nombre de ces outils est en constante augmentation. Parfois, vous devez regarder dans le fichier .wasm et comprendre ce qu'il contient. Peut-être êtes-vous le développeur de l'un des outils Wasm, ou peut-être êtes-vous un programmeur qui écrit du code conçu pour être converti en Wasm et qui s'intéresse à la façon dont il regarde dans quoi son code se transformera. Un tel intérêt peut être déclenché, par exemple, par des considérations de performance.



Le problème est que les fichiers .wasm contiennent un code de bas niveau qui ressemble beaucoup à du vrai code assembleur. En particulier, contrairement, par exemple, à la JVM, toutes les structures de données sont compilées en ensembles d'opérations de chargement / stockage, et non en quelque chose qui a des noms de classe et de champ clairs. Les compilateurs, comme LLVM, peuvent changer le code d'entrée de telle manière que ce qu'ils obtiennent ne lui ressemble pas. 

Et celui qui veut, en prenant un fichier .wasm, savoir ce qui s'y passe?

Démontage ou ... décompilation?


Pour convertir des fichiers .wasm en fichiers .wat contenant une représentation textuelle standard du code Wasm, vous pouvez utiliser des outils comme wasm2wat (cela fait partie de la boîte à outils WABT ). Les résultats de cette conversion sont très précis, mais la lecture du code résultant n'est pas particulièrement pratique.

Voici, par exemple, une fonction simple écrite en C:

typedef struct { float x, y, z; } vec3;

float dot(const vec3 *a, const vec3 *b) {
    return a->x * b->x +
           a->y * b->y +
           a->z * b->z;
}

Le code est stocké dans un fichier dot.c.

Nous utilisons la commande suivante:

clang dot.c -c -target wasm32 -O2

Ensuite, pour convertir ce qui est arrivé à un fichier .wat, nous appliquons la commande suivante:

wasm2wat -f dot.o

Voici ce que cela nous donnera:

(func $dot (type 0) (param i32 i32) (result f32)
  (f32.add
    (f32.add
      (f32.mul
        (f32.load
          (local.get 0))
        (f32.load
          (local.get 1)))
      (f32.mul
        (f32.load offset=4
          (local.get 0))
        (f32.load offset=4
          (local.get 1))))
    (f32.mul
      (f32.load offset=8
        (local.get 0))
      (f32.load offset=8
        (local.get 1))))))

Le code est petit, mais pour de nombreuses raisons, il est extrêmement difficile à lire. Outre le fait que les expressions ne sont pas utilisées ici, et le fait que, dans l'ensemble, il semble assez verbeux, il n'est pas facile de comprendre les structures de données présentées sous la forme de commandes pour charger des données à partir de la mémoire. Imaginez maintenant que vous devez analyser un tel code d'une taille beaucoup plus grande. Une telle analyse sera une tâche très difficile.

Essayons, au lieu d'utiliser wasm2wat, exécutons la commande suivante:

wasm-decompile dot.o

Voici ce qu'elle nous donnera:

function dot(a:{ a:float, b:float, c:float },
             b:{ a:float, b:float, c:float }):float {
  return a.a * b.a + a.b * b.b + a.c * b.c
}

Ça a déjà l'air beaucoup mieux. En plus d'utiliser des expressions rappelant un langage de programmation que vous connaissez déjà, le décompilateur analyse les commandes visant à travailler avec la mémoire et tente de recréer les structures de données représentées par ces commandes. Le système annote ensuite chaque variable, qui est utilisée comme pointeur avec une déclaration de structure «intégrée». Le décompilateur ne crée pas de déclaration de structure nommée, car il ne sait pas s'il y a quelque chose en commun entre les structures qui utilisent chacune 3 valeurs flottantes.

Comme vous pouvez le voir, les résultats de la décompilation se sont avérés plus compréhensibles que les résultats du démontage.

Dans quelle langue le code écrit par le décompilateur est-il écrit?


L'outil wasm-decompile génère le code, essayant de faire ressembler ce code à une sorte de langage de programmation «moyen». Dans le même temps, cet outil essaie de ne pas aller trop loin de Wasm.

Le premier objectif de wasm-decompiler était de créer du code lisible. C'est-à-dire un tel code qui permettra à son lecteur de comprendre facilement ce qui se passe dans le fichier .wasm décompilé. Le deuxième objectif de cet outil est de fournir la représentation la plus précise du fichier .wasm, en générant du code qui représente pleinement ce qui se passe dans le fichier source. De toute évidence, ces objectifs sont loin d'être toujours en bon accord les uns avec les autres.

Les sorties de wasm-decompiler n'étaient pas conçues à l'origine comme du code représentant un vrai langage de programmation. Il n'existe actuellement aucun moyen de compiler ce code dans Wasm.

Commandes de chargement et de sauvegarde des données


Comme indiqué ci-dessus, wasm-decompile recherche les commandes de chargement et d'enregistrement associées à un pointeur particulier. Si ces commandes forment une séquence continue, le décompilateur affiche l'une des déclarations de structure de données «intégrées».

Si tous les «champs» n'ont pas été accédés, le décompilateur ne peut pas distinguer de manière fiable la structure d'une certaine séquence d'opérations avec l'utilisation de la mémoire. Dans ce cas, wasm-decompile utilise l'option de secours, en utilisant des types plus simples comme float_ptr(si les types sont identiques), ou, dans le pire des cas, génère du code qui illustre comment travailler avec un tableau, comme o[2]:int. Un tel code nous indique qu'il opointe vers des éléments du type intet nous nous tournons vers le troisième élément de ce type.

Cette dernière situation se produit beaucoup plus souvent que vous ne le pensez, car les fonctions Wasm locales sont plus axées sur l'utilisation de registres plutôt que de variables. Par conséquent, dans le code optimisé, le même pointeur peut être utilisé pour travailler avec des objets complètement indépendants.

Le décompilateur cherche une approche intelligente de l'indexation et est capable d'identifier des modèles comme (base + (index << 2))[0]:int. La source de ces modèles est les opérations d'indexation habituelles pour C, telles base[index]que les basepointages vers un type à 4 octets. Ceci est très courant dans le code, car Wasm, dans les commandes de chargement et d'enregistrement de données, ne prend en charge que les décalages définis comme constantes. Dans le code généré par wasm-decompile, ces constructions sont converties en type base[index]:int.

De plus, le décompilateur sait quand les adresses absolues pointent vers une section de données.

Contrôle du déroulement du programme


Si nous parlons de constructions de contrôle, la plus célèbre d'entre elles est la construction si-alors Wasm, qui se transforme en if (cond) { A } else { B }, avec l'ajout du fait qu'une telle construction dans Wasm peut renvoyer une valeur, de sorte qu'elle peut également représenter un opérateur ternaire, comme cond ? A : B, ce qui est dans certains langues.

D'autres structures de contrôle de Wasm sont basées sur des blocs blocket loop, ainsi que des transitions br, br_ifet br_table. Le décompilateur essaie de rester le plus près possible de ces structures. Il ne cherche pas à recréer des constructions while / for / switch qui pourraient leur servir de base. Le fait est que cette approche se montre mieux lors du traitement de code optimisé. Par exemple, une conception conventionnelleloop pourrait ressembler au code renvoyé par wasm-decompile comme ceci:

loop A {
  //    .
  if (cond) continue A;
}

Voici Aune étiquette qui vous permet de construire des structures imbriquées les unes dans les autres loop. Le fait qu'il existe des commandes ifet continueutilisées pour contrôler le cycle peut sembler quelque peu étranger aux boucles while, mais elles correspondent à la construction Wasm br_if.

Les blocs sont dressés de manière similaire, mais ici les conditions sont au début et non à la fin:

block {
  if (cond) break;
  //    .
}

Le résultat de la décompilation de la construction if-then est affiché ici. Dans les futures versions du décompilateur, probablement, au lieu d'un tel code, si possible, une construction if-then plus familière sera formée.

L'outil Wasm le plus inhabituel utilisé pour contrôler le déroulement d'un programme est br_table. Cet outil est une sorte d'instruction switch, sauf qu'il utilise des blocs en ligne. Tout cela complique la lecture du code. Le décompilateur simplifie la structure de ces structures, s'efforçant de rendre leur perception un peu plus facile:

br_table[A, B, C, ..D](a);
label A:
return 0;
label B:
return 1;
label C:
return 2;
label D:

Cela rappelle l'utilisation switchpour l'analyse alorsque l'option par défaut est D.

Autres fonctionnalités intéressantes


Voici quelques autres fonctionnalités de wasm-decompile:

  • , . C++-.
  • , , , . . , .
  • .
  • Wasm-, . , wasm-decompile , , , .
  • , ( , C- ).

 


La décompilation du code Wasm est une tâche beaucoup plus compliquée que, par exemple, la décompilation du code d'octet JVM.

Le bytecode n'est pas optimisé, c'est-à-dire qu'il reproduit la structure du code source de façon assez précise. Dans le même temps, malgré le fait qu'un tel code peut ne pas contenir les noms d'origine, le bytecode utilise des références à des classes uniques et non à des zones de mémoire.

Contrairement au bytecode JVM, le code qui pénètre dans les fichiers .wasm est hautement optimisé par LLVM. En conséquence, un tel code perd souvent la majeure partie de sa structure d'origine. Le code de sortie est très différent de ce que le programmeur écrirait. Cela complique grandement la tâche de décompilation du code Wasm avec la sortie de résultats qui peuvent apporter de réels avantages aux programmeurs. Cependant, cela ne signifie pas que nous ne devons pas nous efforcer de résoudre ce problème!

Sommaire


Si vous êtes intéressé par le sujet de la décompilation du code Wasm, alors la meilleure façon de comprendre ce sujet est de prendre et de décompiler votre propre projet .wasm! De plus, ici vous trouverez des indications plus détaillées sur wasm-décompiler. Le code du décompilateur se trouve dans les fichiers de ce référentiel, dont les noms commencent par decompile(si vous le souhaitez, rejoignez le travail sur le décompilateur). Ici vous pouvez trouver des tests montrant des exemples supplémentaires de différences entre les fichiers .wat et les résultats de la décompilation.

Et avec quels outils recherchez-vous les fichiers .wasm?

, , iPhone. , .


All Articles