Phase 13: Tools & Protocols

Au-delà du code natif: Python, JVM, .NET et Bytecode WebAssembly

Un fichier de code octal est un programme plus les notes du compilateur à ce sujet, et cette leçon construit les lecteurs qui montrent quelles notes chaque conteneur conserve.

Type: Build

Languages: Python, Java, C#, WebAssembly text

Prerequisites: Phase 13 · 32 (ELF from scratch), 33 (disassembly and control flow)

Time: ~150 minutes

Objectifs d'apprentissage

  • Lisez le 16 octets .pycen-tête de la PEP 552 et démarcher l'objet code sans le marshalmodule, pour les fichiers écrits par quatre versions interprètes.
  • Parser un fichier de classe JVM à partir du JVMS Chapitre 4: pool constant, champs, méthodes, le Codeattribut, et les tables qui javac -g- Il y a aussi.
  • Démontrez les codes opcodes de la JVM en Ledger.classet prouver la liste contre javap -csur quatre constructions de la même source.
  • Lisez une section par section de module WebAssembly, décodez chaque corps de fonction et prouvez le résultat en fonction de wasm-objdump -d- Je suis désolé .
  • Passez les métadonnées ECMA-335 d' un ensemble .NET du répertoire de données PE à la MethodDefles lignes et le CIL de chaque méthode, vérifiés par rapport à la bibliothèque de lecteur utilisée par ILSpy.
  • Mesurer, pour une fonction dans cinq conteneurs, les noms, types et structures de contrôle que chaque format conserve et quels il perd.

Le problème

Vous avez construit .ledger.pyet trouvéledger.cpython-314.pycdans __pycache__Vous avez demandé à un agent ce qu'il y avait dans le dossier, l'agent a mis en place un décompiler.Unsupported opcodeet une ligne qui disait:# WARNING: Decompyle incompleteL'agent a conclu que le dossier était obscurci.

Le fichier n'a pas été obscurci.2b 0eC'est le nombre que CPython 3.14 écrit, et les tables de code op de décompiler s'arrêtent à 3.13.

La leçon 33 montre ce que le code natif perd: les noms des paramètres, les types et lesswitchqui est devenu une table de saut. Les conteneurs de code octogène perdent moins, et chacun perd quelque chose de différent. Un pyc conserve tous les noms de variables et aucun type. Un fichier de classe ne conserve les types et les noms locaux que lorsque vous demandez. Un module de Wasm conserve la structure de contrôle et dépose les noms à moins qu'une section personnalisée ne les porte. Un assemblage .NET conserve les noms de paramètres dans une table et laisse les noms locaux dans un fichier séparé. Vous ne pouvez pas juger la sortie d'un décompilateur sur aucun d'entre eux jusqu'à ce que vous sachiez avec quelles notes le conteneur a dû commencer.

Cette leçon construit quatre lecteurs dans la bibliothèque standard Python. Chacun lit le conteneur à partir de son premier octet, énumère une fonction et est vérifié par rapport à l'outil du format sur les accessoires que vous avez construits.gradede ledgercôte à côte dans les cinq conteneurs.

Le pyc Header est quatre mots

PEP 552 définit l'en-tête comme quatre mots de 32 bits. Le premier est le nombre magique. Le second est un champ de bit.check_sourceL'interprète hashes la source à l'importation et compare. Avec bit 0 clair, les mots trois et quatre sont le source mtime et la taille de la source. Avec bit 0 ensemble, ils contiennent un SipHash de 64 bits de la source.

Le nombre magique vit dansInclude/internal/pycore_magic_number.hLa règle de Python 3.11 est que Python 3.n commence à 2900 + 50n, et chaque modification de code octal au sein d'un cycle de libération en ajoute un.\r\nPython 3.15.0 a été publié le 2026-10-09 et n'est pas installé sur la machine qui a construit ces fixtures, donc son numéro provient du fichier d'en-tête et non d'un fichier ici.

Les appareils contiennent deux fichiers pyc par interprète. compile_pyc.pyJe les ai écrites avec py_compile.compile(..., dfile="ledger.py")afin que le nom du fichier stocké soit ledger.pyLes 16 premiers octets des fichiers 3.14:

output
checked-hash   2b 0e 0d 0a  03 00 00 00  c7 16 c7 fc c5 1d 81 01
timestamp      2b 0e 0d 0a  00 00 00 00  8d 60 cf 6a  f1 01 00 00

Lisez l'en-tête de l'étemplaire mot par mot. 03 00 00 00ou 00 00 00 00C'est le mot de drapeau.8d 60 cf 6aest 0x6acf608d = 1791975565, le mtime de ledger.pyquand le dossier a été construit. f1 01 00 00Le fichier de hash vérifié contient des drapeaux 3 et huit bytes hash.

InterpreterMagic bytesWordFile bytesgrade.co_code bytesgrade instructions
3.11.14a7 0d 0d 0a349510548031
3.12.12cb 0d 0d 0a35319376226
3.13.12f3 0d 0d 0a35719437026
3.14.62b 0e 0d 0a36279659635

read_pyc_header()dans code/main.pyrenvoie ces champs et nomme la version du mot. importlib.util.MAGIC_NUMBERdonne les mêmes quatre octets pour l'interprète en cours d'exécution. python3 code/main.py comparecompile la source avec chaque python3.11à python3.15trouvé sur PATHPour chacun, il imprime l'en-tête que le lecteur voit à côté des faits que l'interprète rapporte sur lui-même.

Le maréchal écrit le code Objet champ par champ

Le 16 octets de chaque fichier est e3- C' est ça .0x80 | ord('c'): le code de type marshal pour un objet de code avec FLAG_REFl'ensemble, qui dit au lecteur de se souvenir de l'objet pour plus tard rLes documents du marshal indiquent que le format est délibérément indocumenté et que les objets de code ne sont pas compatibles entre les versions.Python/marshal.crépertorie les codes de type: NAucun.iune int de 32 bits, sen octets, zet Zcourts chaînes ASCII, (et )- Les deux autres.cun objet de code, rLes conteneurs réservent leur place de référence devant leurs enfants, de sorte que le lecteur fait de même.

w_complex_object()Il est écrit un objet de code dans un ordre fixe.argcount- Je suis là .posonlyargcount- Je suis là .kwonlyargcount- Je suis là .stacksize- Je suis là .flagscomme 32 bits de l'ins, alors code- Je suis là .consts- Je suis là .names- Je suis là .localsplusnames- Je suis là .localspluskinds- Je suis là .filename- Je suis là .name- Je suis là .qualnameen tant qu'objets, alors firstlinenoAlors ...linetableet exceptiontable. Décoder manuellement l'objet du code du module du fichier 3.14 à partir de l' octet 17:

output
00 00 00 00   argcount 0
00 00 00 00   posonlyargcount 0
00 00 00 00   kwonlyargcount 0
01 00 00 00   stacksize 1
00 00 00 00   flags 0
f3 22 00 00 00   type 's' with FLAG_REF, 34 bytes of co_code follow
80 00  52 06  74 00  52 01  17 00  74 01 ...

Les octets de code sont deux par instruction: 80 00est RESUME 0- Je suis là .52 06est LOAD_CONST 6- Je suis là .74 00est STORE_NAME 0, et 17 00est MAKE_FUNCTIONLa liste 3.14 des fixations imprime les mêmes cinq déclarations au niveau du module.codeviennent les constantes, qui contiennent les quatre objets de code nichés, et après eux namesLe numéro de la liste:('LABELS', 'percent_change', 'grade', 'total', 'grade_label')- Je suis désolé .

Unmarshallerdans code/main.pyIl est écrit dans cette disposition et dans la version ancienne que 3.9 et 3.10 ont utilisée, où nlocals- Je suis là .varnames- Je suis là .freevars, et cellvarsLes champs sont séparés.CodeObjectavec les mêmes noms d'attributs que l'objet réel. varnamesfiltres localsplusnamespar le CO_FAST_LOCALUn peu delocalspluskindsPour ...gradedans chacun des quatre dossiers qu' il renvoie ('score', 'bucket')- Je suis là .firstlineno10 et une non-voie linetable- Le test .test_unmarshaller_agrees_with_the_running_interpretercomparer chaque champ avec marshal.loads()Les trois autres fichiers sont lisibles par notre code et illisibles par notre code.marshalsous cet interprète.

Quatre interprètes, quatre code de code pour la note

La dé-documentation contient un avertissement sur chaque version: le code octal est un détail d'implémentation de CPython et aucune garantie n'est faite qu'il reste le même.python3.1N -m dis ledger.pyIl a été capturé pour chaque interprète.

Dans 3.11 gradecommence comme ceci:

output
 11           2 LOAD_FAST                0 (score)
              4 LOAD_CONST               1 (10)
              6 BINARY_OP                2 (//)
             10 STORE_FAST               1 (bucket)
 12          12 LOAD_FAST                1 (bucket)
             14 LOAD_CONST               2 (9)
             16 COMPARE_OP               5 (>=)
             22 POP_JUMP_FORWARD_IF_FALSE     2 (to 28)

BINARY_OPLes deux octets entre eux sont une entrée cache en ligne de PEP 659, l'interprète adaptatif spécialisé. COMPARE_OPLe 16 est suivi par le 22, donc il possède deux entrées cachées.show_caches=True. 3.12 remplace LOAD_CONSTplus RETURN_VALUEavec RETURN_CONST, renommé le saut à POP_JUMP_IF_FALSE, et les psychiatriques .gradede 80 à 62 octets. 3,13 imprimés étiquettes L1à L4au lieu de compenser et donne l'indice de valeur-Cpour les caches et -Opour les compensations. 3.14 supprime RETURN_CONST- encore une fois, ajoute-t-il.LOAD_SMALL_INTet LOAD_FAST_BORROW, et les inserts NOT_TAKENaprès chaque saut conditionnel:

output
 12         20       LOAD_FAST_BORROW         1 (bucket)
            22       LOAD_SMALL_INT           9
            24       COMPARE_OP             188 (bool(>=))
            26       CACHE                    0 (counter: 0)
            28       POP_JUMP_IF_FALSE        3 (to L1)
            30       CACHE                    0 (counter: 0)
            32       NOT_TAKEN

Le -C -Ola liste montre 13 CACHELes entrées à l'intérieur grade: cinq après BINARY_OP, un après l' autre .COMPARE_OP, un après l' autre .POP_JUMP_IF_FALSELe dispositif d'adaptation montre à quoi servent les caches.dis.dis(grade, adaptive=True)des empreintesRESUME_CHECKet COMPARE_OP_INToù la liste simple avait RESUMEet COMPARE_OP, et totalLes spectaclesFOR_ITER_LIST- Je suis là .BINARY_OP_ADD_INT, et JUMP_BACKWARD_NO_JITLes formes spécialisées vivent dans la mêmeco_codeLes pyc ne les contiennent jamais, car la spécialisation se fait dans la mémoire.

C'est pourquoi les décompilateurs ont des plafonds de version. uncompyle6 3.9.3 indique qu'il accepte le code par octets de Python 1.0 à 3.8. décompyle3 3.9.3 lit 3.7 et 3.8. xdis 6.3.0, le démontant de version croisée derrière les deux, lit jusqu'à 3.13. pycdc a des tables de code optique jusqu'à python_3_13.cppdans son fichier de construction, et sur une imprimante de code op inconnu Unsupported opcodePyLingual revendique chaque version publiée depuis 3.6 et utilise des interprètes de 3.8 à 3.15 pour recompiler et vérifier sa propre sortie. Pour un fichier 3.14 les trois premiers rejettent ou échouent, et seuls les deux derniers s'appliquent.

Le dossier de classe garde un dictionnaire

Le JVMS Java SE 25, section 4.1, donne la mise en page en tant qu'une seule structure: magic- Je suis là .minor_version- Je suis là .major_version- Je suis là .constant_pool_count, la piscine constante,access_flags- Je suis là .this_class- Je suis là .super_classLes interfaces, les champs, les méthodes, les attributs.0xCAFEBABEUne machine virtuelle Java SE 25 accepte les versions majeures 45 à 69, et une version mineure de 65535 marque un fichier de fonctionnalité d'aperçu.Ledger-g.classsontca fe ba be 00 00 00 41 00 5e: majeur 65 est Java 21, et 0x5e= 94 est le nombre constant de poissons. Ledger-jdk25.class, construit par javac 25.0.2 à partir de la même source, commence ca fe ba be 00 00 00 45Le majeur 69.

La base constante est l'endroit où le fichier de classe garde son propre dictionnaire.#13dans le -gconstruire est un Methodrefqui pointe à la classe #8et nom et type #14. Suivant les indices donne Ledger.grade:(I)I, ce qui est exactement ce que javap -vdes empreintes après //- Le descripteur .(I)Iest unintparamètre et un intLe résultat.([I)Isurtotaldit que le paramètre est un intChaque méthode, champ et classe que le code touche a une telle entrée, et le vérificateur de la section 4.10 refuse un fichier dont les instructions ne sont pas d'accord avec eux.ClassFileclasse en code/main.pylisent les 17 étiquettes constantes du tableau 4.4 et traitent un Longou DoubleIl s'élève sur une balise inconnue et sur des octets après le dernier attribut.

Les attributs dépendent de la classe, de chaque champ, de chaque méthode et de la CodeJVMS dit que la machine virtuelle doit ignorer les attributs qu'elle ne connaît pas, c'est pourquoi les tables de débogage peuvent être ajoutées ou supprimées sans changer un corps de méthode.-ggénère toutes les informations de débogage, y compris les variables locales, que le numéro de ligne et le fichier source sont générés par défaut seulement, et que -g:noneLes trois construisent une mesure qui:

BuildBytesConstant pool countLineNumberTableLocalVariableTableSourceFilegrade code bytes
javac -g1832947 methods6 methodsyes50
javac1581827 methodsnoneyes50
javac -g:none140479nonenoneno50

Les 50 octets de code de gradeLes trois dossiers sont identiques, et le test le vérifie.-gajout pour gradeest unLocalVariableTableligne: début 0, longueur 50, espace 0, nom score, décritrice ISans elle , un décompilateur voit la fente de type 0 .intIl doit inventer un nom.StackMapTablereste dans chaque construction parce que le vérificateur en a besoin, et le BootstrapMethodsl' attribut reste parce que mainconcatenant les chaînes à travers invokedynamic- Je suis désolé .

Décodage des tables à la main

Le chapitre 6 du JVMS définit chaque instruction comme un octet opcode et les opérands qui nomment opcode. Les branches offsets sont des valeurs signées de 16 bits par rapport à l'adresse de l'opcode de branche, qui diffère de x86-64, où le déplacement est relatif à l'instruction suivante. tableswitchet lookupswitchcommencez par 0 à 3 octets de remplissage de sorte que leurs opérands à 32 bits commencent à une adresse qui est un multiple de 4.

Les octets de gradeà partir de l' ordinateur 4 en Ledger-g.class- Le numéro de la liste:

output
aa                 tableswitch at pc 4
00 00 00           padding to pc 8
00 00 00 2c        default: 4 + 44 = 48
00 00 00 06        low 6
00 00 00 0a        high 10
00 00 00 2a        case 6:  4 + 42 = 46
00 00 00 28        case 7:  4 + 40 = 44
00 00 00 26        case 8:  4 + 38 = 42
00 00 00 24        case 9:  4 + 36 = 40
00 00 00 24        case 10: 4 + 36 = 40

36 octets, une instruction, et tout.switch (score / 10)La construction native de la même switchDans la leçon 33, il fallait vérifier les limites,lea, une movslq, uneaddLes cibles sont dans l'instruction, et le fait qu'elles appartiennent ensemble.

disassemble_jvm()couvre tous les 202 opcodes définis avec leurs largeurs d'opérand, suit la règle de rembourrage et rend les opérands la façon javapIl le fait.compare_with_javap()Il analyse les prisonniers .javap -c -ptexte, y compris le texte multilinéaire tableswitchLes quatre constructions sont d'accord sur 108 des 108 instructions.test_branch_offsets_are_relative_to_the_branch_instructionles chèques ifneà l'atterrissage sur le pc 1 le 6, if_icmpgeà 13 et à 33gotoÀ 30h, retour sur 10.

Radar2 6.2.4, construit à partir de la source pour ces leçons, lit le même fichier. rabin2 -Irapports arch java- Je suis là .lang java 21- Je suis là .linenum true, et lsyms true, qui est la lecture de la version et des deux tables de débogage. pd 14 @ sym.Ledger.gradeimprime le tableswitchIl imprime aussi des traces.Unable to parse code attribute: 2Il est donc nécessaire de mettre en place des procédures de certification et de vérification des données.javapcomme l'oracle. Les décompilateurs pour ce format sont matures par rapport à Python. CFR 0.152 est sous licence MIT. Procyon 0.6.0 et Vineflower 1.12.0 sont Apache-2.0, et Vineflower est un fourchette Fernflower avec support Java 21. Fernflower lui-même vit dans le référentiel de la communauté IntelliJ. JADX 1.5.6 couvre le format Android DEX, et JD-GUI a été publié pour la dernière fois en 2019. Harrand, Soto-Valero, Monperrus et Baudry en ont mesuré huit en 2020. Le meilleur produit syntaxiquement correct pour 84% des classes et sémantiquement équivalent pour 78%. Leur méta-décompilateur a récupéré 37,6% des classes qu'aucun outil ne gérait. Les types, les noms et les piles vérifiées rendent le format décompilable. Ils ne rendent pas un seul décompilateur complet.

Un module de la chaux est une liste de sections

La spécification de base définit un module comme le préambule 00 61 73 6d 01 00 00 00, la corde \0asmet la version 1, suivie de sections.u32Les ids 1 à 13 sont les sections connues, chacune au plus une fois et dans l'ordre. id 0 est une section personnalisée avec un nom, autorisé partout. tous les entiers sont LEB128, donce4 00est de 100 et 80 08C'est le 1024.wasm-objdump -hsurledger.wasmlistes de sept sections, et WasmModuletrouve les mêmes sept avec les mêmes compensations de charge utile:

SectionPayloadCountWhat it holds for ledger
Type10 to 263(i32) -> nil, (i32, i32) -> i32, (i32) -> i32
Import28 to 391env.log with type 0
Function41 to 464type index for each defined function
Memory48 to 511one page, no maximum
Export53 to 1105memory and the four function names
Code113 to 2714bodies of 23, 63, 44, and 23 bytes
Data273 to 2901F\0D\0C\0B\0A\0 at offset 1024

La division entre les sections Fonction et Code existe afin qu'un navigateur puisse commencer à compiler avant l'arrivée de l'ensemble du fichier.gradeLe vecteur local est 01 7f: un groupe d' un i32- C' est ça .bucket, avec son type et sans son nom.

Le flux de contrôle est structuré par construction.block- Je suis là .loopou ifLes étiquettes sont indexées par profondeur de nidification. br_tableL'opérand est une branche indirecte qui indique une liste d'étiquettes.gradeà l' échange de fichiers 0xa3- Le numéro de la liste:

output
0e                  br_table
0b                  11 labels follow
04 04 04 04 04 04   buckets 0 to 5: label 4, the outermost block, returns 0
03 02 01 00         buckets 6, 7, 8, 9: labels 3, 2, 1, 0
00                  bucket 10: label 0, shared with 9
04                  default label 4

Chaque étiquette est une profondeur.block, et le code après son endest i32.const 4suivie de br 4Le résultat est le bloc.blockLes instructions devant la table, les onze étiquettes et les cinq brles instructions après l' ensemble switch, et un validateur rejette toute branche qui quitte la structure. disassemble_wasm()La profondeur est suivie, donc les tirages de liste comme wasm-objdump, couvre chaque code optique de un seul octet de unreachableà i64.extend32_s, et correspond aux captifs wasm-objdump -dsur 77 des 77 instructions pour les deux constructions.

Les noms sont la seule chose qu'un module simple laisse tomber.wasm-objdumpDes empreintes<grade>Parce que la section Export carte que la chaîne à fonctionner l'index 2, et le lecteur fait la même chose.wat2wasm --debug-namesajoute une section personnalisée nommée nameLe fichier est de 425 octets. Le lecteur trouve {0: 'score', 1: 'bucket'}pour la fonction 2 et {0: 'values', 1: 'count', 2: 'sum', 3: 'i'}pour la fonction 3. wasm-stripLe référentiel des conventions d'outils décrit deux autres porteurs..debug_*Les sections personnalisées.sourceMappingURLla section personnalisée peut indiquer une carte source dont les numéros de colonne sont des dérivés en octets dans la liste binaire.

L'outillage a été déplacé en 2026.wasm-decompile, et la construction Homebrew sur cette machine a exactement les onze outils restants.print- Je suis là .objdump, et demangle. La version Binaryen_133 fournit wasm-disLe radar 6.2.4 lit aussi le module.rabin2 -Idit arch wasmC' est le symbole de lagradecommence à0x8e, où le vecteur local commence, alors que wasm-objdumpétiquette la fonction à 0x8bLa liste utilise les mnemoniques plus anciens.get_local- Je suis là .set_local, et brtable 11 4 4 ...avec le nombre d'étiquettes imprimé. Les octets sont les mêmes dans les trois outils et les orthographes diffèrent, donc une comparaison doit nommer l'outil avec lequel il a été capturé. La recherche pour cette leçon n'a trouvé aucun module de processeur WebAssembly dans le répertoire de processeurs de Ghidra, et cette liste n'a pas été lue en entier. Le plugin ghidra-wasm v2.4.0 de tiers cible Ghidra 12.0. La spécification elle-même est un document vivant. La page du W3C pour wasm-core-2est un projet de recommandation de candidat du 13 octobre 2026 dont le résumé décrit la version 3.0, et le projet de rédaction porte la même date.

Les métadonnées ECMA-335 sont la base de données du programme

Un assemblage .NET est un fichier PE. LedgerNet.dllcommence par MZ, a trois sections nommées .text- Je suis là ..rsrc, et .reloc, et déclare machine 0x14cLe répertoire de données 14 de l'en-tête optionnel, l'en-tête de temps d'exécution CLR, pointe vers RVA 0x2008. L'ECMA-335 II.25.3.3 définit les 72 octets qui y sont:

output
48 00 00 00   Cb 72
02 00 05 00   MajorRuntimeVersion 2, MinorRuntimeVersion 5
78 21 00 00   MetaData RVA 0x2178
84 07 00 00   MetaData size 1924
01 00 00 00   Flags: COMIMAGE_FLAGS_ILONLY
05 00 00 06   EntryPointToken 0x06000005, MethodDef row 5, which is Main

La racine de métadonnées à RVA 0x2178commence par la signature 42 53 4a 42, qui dit:BSJB, puis la version 1.1 et la chaîne v4.0.30319Cinq en-têtes de flux suivent:#~à l' équilibre 0x6cAlors ...#Strings- Je suis là .#US- Je suis là .#GUID, et #BlobIl est également possible de détecter les effets de la pollution de l'air.#~Le flux commence par un HeapSizesoctet, zéro ici, donc chaque chaîne, GUID, et index de la tache est de deux octets de large, et un 64 bits Validmasque qui donne le nom des tables présentes.

TableIdRowsRow sizeWhat the rows say
Module0x00110the module name and its GUID
TypeRef0x01216System.Object, System.Console, 19 more
TypeDef0x02414<Module>, Ledger, and two compiler helpers
Field0x0426Labels and a static array initialiser
MethodDef0x06614the five methods and .cctor
Param0x0856before, after, score, values, score
MemberRef0x0a16612 attribute constructors and the 4 methods that Main calls
CustomAttribute0x0c126attributes the compiler and the SDK add
StandAloneSig0x1132the three local variable signatures
Assembly, AssemblyRef, ClassLayout, FieldRva, NestedClass1, 2, 1, 1, 1identity, references, layout

Une taille de rangée n'est pas stockée nulle part. II.22 donne à chaque colonne un type, et II.24.2.6 donne la règle de taille.HeapSizes. Un index de table simple est de deux octets alors que cette table comporte moins de 65536 lignes. Un index codé pouvant indiquer n tables avec b tag bits est de deux octets alors que chacune de ces tables comporte moins de 2^(16-b) lignes. CliAssemblyLe système.Reflection.Dépôt de métadonnées dans les appareils rapporte les mêmes tailles et les mêmes tableaux de compensation, ce qui est la façon dont le test prouve l'arithmétique.

III.1.9 définit le jeton: le byte haut est la table, les trois bytes bas sont la ligne basée sur 1. 0x06000002est MethodDefla ligne 2. Cette ligne contient RVA 0x2060, nom Grade, et une tache de signature .00 01 08 08: convention d'appel par défaut, un paramètre, retour int32, paramètre int32- C' est ...ParamListpoints à la ligne 3 de Param, dont le nom est scoreLa table Param est la raison pour laquelle ILSpy peut imprimerGrade(int score)- Il a été démonté.0x2060le corps de la méthode commence par une tête de graisse:

output
13 30         flags 0x13: fat, init locals; size 3 dwords
02 00         MaxStack 2
2d 00 00 00   CodeSize 45
01 00 00 11   LocalVarSigTok 0x11000001

StandAloneSigla ligne 1 est retenue 07 01 08Le numéro de la liste:LOCAL_SIG, un local, int32. Le local a un type et aucun nom. ECMA-335 conserve les noms locaux dans le format de débogage d'interchange de Partition V, et le .NET SDK les écrit à un PDB. Ce dispositif a été construit avec DebugTypeAucune, donc aucune table et aucun dossier ne porte le nom.bucket- Je suis là .PercentChangea une petite en-tête à la place: son premier octet 3aest 0x0e << 2 | 2, un corps de 14 bytes sans habitants et une pile par défaut de 8.

Les 45 octets de code de Gradedécodez avec la table des codes optiques de la partie III. switchest opcode 0x45suivie d'une u32Compte et beaucoup d' autres .int32objectifs relatifs à l'instruction après le tableau:

output
IL_0000: 02           ldarg.0
IL_0001: 1f 0a        ldc.i4.s 10
IL_0003: 5b           div
IL_0004: 0a           stloc.0
IL_0005: 06           ldloc.0
IL_0006: 1c           ldc.i4.6
IL_0007: 59           sub
IL_0008: 45 05 00 00 00 08 00 00 00 06 00 00 00 04 00 00 00 02 00 00 00 02 00 00 00
                      switch (IL_0029, IL_0027, IL_0025, IL_0023, IL_0023)
IL_0021: 2b 08        br.s IL_002b
IL_0023: 1a 2a        ldc.i4.4, ret

Le compilateur a soustrait 6 avant le switch, donc le cas 6 est la première cible.disassemble_cil()rend les jetons à travers les tables: ldsfld Labels- Je suis là .call Grade- Je suis là .call System.Console::WriteLine, et ldstr ' 'de la #USL'oracle de tout ça estMetaDump, un court programme C# dans les appareils construits sur System.Reflection.MetadataIl imprime le nombre de lignes de chaque table, la taille et l'offset, le RVA de chaque méthode, la signature, les noms des paramètres et les octets IL, et les 226 champs de System.Reflection.Emit.OpCodes- Je suis là .compare_with_metadump()Le tableau de l'heure d'exécution comporte 218 codes plus huitprefixLa table de cours comporte 219 entrées car elle inclut:no.de III.2.2, que le tableau des émissions n'a jamais eu.

Les outils pour le format: ILSpy 11.0 (MIT, publié 2026-08-12) fonctionne sur macOS via sa mise en page de bureau Avalonia. ilspycmd11.1.0.9782 imprimées IL avec -ilet sélectionne une méthode par token avec -m 0x06000002. dnSpyEx 6.6.0 continue dnSpy sur Windows seulement.ildasmLes données métadonnées sont également lues par radar2 6.2.4: rabin2 -Idit arch cilet class PE32, et isListe Ledger.Grade(int:score):intIl n'a pas de démonteur CIL dans cette construction. Ce dispositif a été construit avec .NET SDK 8.0.205 parce que c'est le SDK installé sur la machine de construction. .NET 10 est en cours depuis 2025-11-11 et écrit les mêmes structures ECMA-335 .

Ce que chaque conteneur conserve

Je vous ai misgradeChaque rangée provient depython3 code/main.py keeps, qui fait passer les lecteurs sur les appareils:

ContainerBody bytesFunction nameParameter namesParameter and return typesLocal namesLine numbersControl flow
ELF object, clang -O0118grade in .symtabnonenonenonenone, no .debug_* sections5 jumps and 1 indirect jump through a relocation table
CPython 3.14 pyc96co_qualnamescorenonebucketco_linetable, first line 104 compare and POP_JUMP_IF_FALSE pairs
JVM class, javac -g50constant poolscore in LocalVariableTable(I)Inone beyond the parameterLineNumberTable, 6 rows1 tableswitch
WebAssembly63Export sectiononly with a name section(i32) -> i32only with a name sectionnone6 nested blocks, 1 br_table, 4 br
.NET assembly45MethodDefscore in Paramstatic int32(int32)none, types only in StandAloneSignone without a PDB1 switch, 1 br.s

Le tableau explique les résultats de décompilation que vous verrez.int total(const int *, int)avec le int total(long param_1, uint param_2)Le code natif ne conserve aucun de ces colonnes. Un pyc conserve tous les noms et aucun type, et son flux de contrôle est des sauts dont la forme change à chaque sortie, de sorte que les décompilateurs Python se brisent par version et récupèrent les noms gratuitement.-gest en marche et exprime leswitchUn module de Wasm maintient la structure par construction, donc wasm2watUn assemblage .NET conserve la description la plus complète, car les tables de métadonnées sont normatives, et nécessite toujours un PDB pour le nom d'un local.

La table indique avec quoi le décompilateur a dû travailler, et quelles de ses sorties copient un fait stocké et qui en déduit une. Un nom de paramètre dans la sortie ILSpy est une copie. Un nom local dans la même sortie est une inférence à moins qu'un PDB ne soit présent.

Faites-le

Exécutez la démo:

bash
cd phases/13-tools-and-protocols/43-bytecode-and-managed-formats
python3 code/main.py

La sortie est un rapport JSON. Pour chaque pyc il donne les deux en-têtes, le fichier et gradeLes données de base sont les données de chaque classe, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base, les données de base et les données de base.javapPour chaque construction de Wasm, il donne des sections, des types, des fonctions, des contenus de la section nominale et leswasm-objdumpIl donne pour l'assemblage l'en-tête CLI, les flux, le nombre de lignes de table, les méthodes et l'accord System.Reflection.Metadata. Il se termine par la table de conservation et la liste des interprètes trouvés sur PATH- Je suis désolé .

Lire code/main.pydans cet ordre:

  1. Readerdétient le curseur et les largeurs entières. uleb()et sleb()sont les lecteurs LEB128 dont Wasm et le .NET ont besoin.
  2. read_pyc_header()- Je suis là .Unmarshaller, et PycFileJe lis un pyc.parse_dis_text()Il lit les prisonniers .disles listes afin que la comparaison entre versions ne nécessite pas quatre interprètes au moment de l'essai.
  3. ClassFilelisent le fichier de classe et disassemble_jvm()décode son code. parse_javap_code()et compare_with_javap()sont les vérifications oracle.
  4. WasmModuleIl lit les sections et disassemble_wasm()décode une expression avec sa profondeur. compare_with_wasm_objdump()C'est le contrôle de l'oracle.
  5. CliAssemblymarche PE, en-tête CLI, racine de métadonnées, flux et tables. TABLE_SCHEMASet CODED_INDEXESsont II.22 et II.24.2.6 comme données. disassemble_cil()décode un corps et compare_with_metadump()Il le vérifie.
  6. NativeObjectLire suffisamment ELF pour énumérer les symboles de fonction et prendre la leçon 33 comme texte. what_each_keeps()et figure_data()construire la comparaison.

Il y a cinq commandes de plus.python3 code/main.py pyc FILE- Je suis là .class FILE- Je suis là .wasm FILE, et dll FILEimprimer la structure et une liste de chaque fonction dans un fichier que vous leur donnez. python3 code/main.py comparecompilations ledger.pyavec chaque interprète sur .PATHet imprime ce que chacun a écrit.

Utilisez-le

Utilisez les lecteurs dans cet ordre sur un conteneur que vous avez construit.

  1. Lisez les premiers octets et nommez le format.0d 0aest un pyc, ca fe ba beun dossier de classe, 00 61 73 6dun module, 4d 5aavec l'annuaire de données 14 un ensemble de configuration.
  2. Lisez l'en-tête et écrivez la version de producteur qu'il nomme. Pour un pyc qui est le mot magique, pour un fichier de classe la version principale, pour un assemblage les chaînes de version de temps d'exécution et de métadonnées.
  3. Vérifiez chaque décompiler que vous prévoyez d'exécuter contre cette version avant de l'exécuter.
  4. Exécutez le lecteur et l'outil du format sur le même fichier et comparez-les rangée par rangée. Un déséquilibre est un bug dans le lecteur ou un changement dans l'outil, et l'un ou l'autre doit être compris avant que la liste ne soit une preuve.
  5. Remplissez la table de conservation pour la fonction qui vous intéresse, marquez chaque fait comme conservé ou perdu et nommez la table, l'attribut ou la section d'où il vient.
  6. Il suffit de lire la sortie du décompilateur, de marquer chaque nom et de le taper comme une copie d'un fait conservé ou comme une inférence.

Gardez les fichiers texte oracle avec les conteneurs. Lorsque l'interprète, JDK, SDK ou wabt version change, reconstruire avec build_fixtures.shet de recapture, parce qu'une liste modifiée est un fichier modifié.

Laboratoire interactif

Laboratoire A: lire l'en-tête, puis l'objet du code

De la coderépertoire, ouverture Python:

bash
cd phases/13-tools-and-protocols/43-bytecode-and-managed-formats/code
python3 -q

Je vais courir .

python
from main import *
pyc = load_pyc("ledger.cpython-311.pyc")
pyc.header.to_dict()
pyc.module.names
pyc.functions()["grade"].varnames
len(pyc.functions()["grade"].code)

Écrivez le mot magique avant le premier appel.ledger.cpython-314.pycet comparer les deux codeAlors essayez.import marshal; marshal.loads(pyc.data[16:])sur le fichier 3.11 sous votre interprète et lire l'erreur.

Laboratoire B: la chaîne de piscine constante

python
cls = load_class("Ledger-g.class")
cls.constant(13)
cls.constant(14)
cls.describe_constant(13)
[row["text"] for row in jvm_listing(cls, "grade")]

Suivez#13à #8et #14, puis à la Utf8Les inscriptions, à la main avant d'appeler describe_constant- Alors chargez .Ledger-gnone.classet appelercls.method("grade").attribute_names()Les deux.CodeL'ensemble est le même, les attributs ne le sont pas.

Laboratoire C: profondeur et étiquettes

python
module = load_wasm("ledger.wasm")
[(row["depth"], row["text"]) for row in wasm_listing(module, "grade")]
module.function_name(2)
load_wasm("ledger-names.wasm").names["locals"][2]

Trouvez le br_tableRésumez la profondeur de la ligne.endles rangées après elle et correspondent chacune à un numéro d'étiquette dans le tableau.ledger.watet trouver le block $sixCette étiquette 3 noms.

Laboratoire D: des jetons aux octets

python
asm = load_assembly("LedgerNet.dll")
asm.method("Grade")
body = asm.method_body(asm.method("Grade")["RVA"])
body.fat, body.max_stack, body.code_size, hex(body.local_signature_token)
asm.token_text(0x11000001)
[row["text"] for row in cil_listing(asm, "GradeLabel")]

Compute le décalage de fichier de RVA 0x2060de la .texten-tête de section à la main et comparer avec asm.rva_to_offset(0x2060)Alors regardez en haut .asm.row(0x08, 3)et trouver où le nom scoreIl vit.

Laboratoire de pratique

Ajoutez une cinquième langue source à la comparaison, ou un sixième fait au tableau.

Les exigences:

  • Écrivez une fonction avec une boucle et une variable locale dans une langue dont vous avez le compilateur, par exemple Kotlin à un fichier de classe ou Rust à un module Wasm. Commencez la sortie et la liste oracle avec la commande exacte en build_fixtures.sh- Je suis désolé .
  • Faites en sorte que le lecteur correspondant soit d'accord avec l'oracle sur chaque instruction.
  • Extension what_each_keeps()avec le nouveau conteneur, et avec un autre fait pour chaque conteneur: si le corps porte une table d'exception ou tryrégion.
  • Ajoutez le nouveau conteneur à figure_data()et régénérer site/figures-re-43.js- Je suis désolé .
  • Écrivez un test qui échoue lorsqu'un lecteur rapporte un fait tel qu'il est conservé sans nommer la table, l'attribut ou la section qui le contient.

Le succès signifiepython3 code/main.py keepsmontre la nouvelle rangée avec des preuves pour chaque cellule, et chaque comparaison oracle est toujours d'accord.

Leçon d'artisanat

Cette leçon comprend outputs/skill-bytecode-and-managed-formats.md. Utilisez-le pour transformer un conteneur de code octal en rapport d'en-tête, une liste vérifiée d'une fonction et une table de faits qui marque chaque nom, type et structure comme conservé ou perdu. Il nécessite un outil du format comme oracle, une vérification de version avant tout décompilateur et une déclaration de l'endroit où chaque fait perdu aurait vécu.

Vérifiez

Exécutez la démo et la suite déterministe:

bash
cd phases/13-tools-and-protocols/43-bytecode-and-managed-formats
python3 code/main.py
python3 -m unittest discover -s code/tests -v

La vérification doit prouver:

  • Les quatre en-têtes pyc portent les mots magiques 3495, 3531, 3571 et 3627, les drapeaux 3, et un hash de 8 octets, et les fichiers timestamp portent des drapeaux 0, mtime 1791975565, et la taille 497
  • chaque mot magique pour 3.11 et plus tard se trouve dans la gamme de la 2900 + 50nla règle donne sa libération
  • le démarcheur renvoie les mêmes noms, constantes, octets de code, noms de variables et table de lignes que marshal.loads()sur le fichier que l'interprète en cours d'exécution a écrit
  • Les quatre fichiers conservent les noms des modules,('score', 'bucket')pour grade, première ligne 10, et ledger.pycomme nom de fichier
  • grade.co_codeest 80, 62, 70 et 96 octets dans les quatre versions et les listes capturées ont 31, 26, 26 et 35 instructions
  • La liste 3.11 a POP_JUMP_FORWARD_IF_FALSE, 3.12 et 3.13 ont étéRETURN_CONST, et 3.14 a été .LOAD_SMALL_INT- Je suis là .LOAD_FAST_BORROW, et NOT_TAKEN
  • le 3.14 -C -Ola liste montre 13 entrées de cache dans grade, cinq d' eux après BINARY_OP, et les émissions de l' annonce adaptative COMPARE_OP_INT
  • une tête sans 0d 0a, une tête tronquée, et un corps qui n'est pas un objet de code lever FormatError
  • une liste d'instructions n' existe que pour le pyc écrit par l' interprète en cours d' exécution et correspond à celui capturé disdégagement
  • les fichiers de classe ont des majors 65 ou 69, des tailles 1581, 1832, 1404 et 1581, et les nombres de la pile constante 82, 94, 79 et 82
  • constantes #13résolue à Method Ledger.grade:(I)Iet #47à String F
  • le démonteur de la VVM est d' accord avec javap -c -psur 108 des 108 instructions dans les quatre constructions
  • tableswitchsur pc 4 est de 36 octets avec faible 6, haut 10, 48 par défaut et cibles 46, 44, 42, 40, 40
  • Les trois niveaux de débogage changent le nombre d' attributs et laissent les 50 octets de code de gradeidentique
  • ifne- Je suis là .if_icmpge, et gotoles objectifs sont relatifs au code optique de la branche
  • une mauvaise magie, des octets en retard et une augmentation indéfinie du code optique.FormatError
  • Les sept sections de Wasm ont des compensations de charge utile wasm-objdump -hLes empreintes, et les noms construire ajoute un namesection douanière à 293 à 425
  • Les types, les env.logLes données de l'importation, les cinq exportations et le segment des données au décode 1024 comme écrit dans ledger.wat
  • Le désassembleur de Wasm est d' accord avec wasm-objdump -dsur 77 des 77 instructions dans les deux constructions
  • gradea 6 blocs en nichage, un br_tableavec des étiquettes 4 4 4 4 4 4 3 2 1 0 0et par défaut 4 à la profondeur 6, et un i32locaux
  • Les noms de fonctionnalités proviennent des exportations sans section nom et des noms locaux uniquement avec elle
  • LEB128 e4 00est de 100, 7fest -1, 80 08est 1024, et les types de blocs se rendent comme wasm-objdump
  • une préamble erronée, version 2, et une augmentation inconnue du code optique FormatError
  • l' assemblage est PE32 avec machine 0x14c, en-tête CLI à RVA 0x2008, temps de fonctionnement 2,5, ILONLY, et point d'entrée 0x06000005
  • la version des métadonnées est v4.0.30319Il y a cinq courants, et les 14 tableaux ont le nombre de rangées et les tailles de rangées que l'oracle rapporte
  • le lecteur .NET est d' accord avec System.Reflection.Metadata sur 246 des 246 contrôles
  • Gradea un paramètre score, signature static int32(int32), une grosse tête avec MaxStack2 et 45 octets de code, et un int32locaux sans nom
  • CIL switchcibles, br.s- Je suis là .ldsfld- Je suis là .call, et ldstrLes opérands se résolvent à travers les tables et les amas
  • Le tableau des codes optiques CIL couvre tous les codes optiques en cours d' exécution et ne ajoute que no.
  • le tableau de garde rapporte le nom du paramètre dans quatre conteneurs et dans aucun pour le code natif, et les types dans trois
  • chaque ligne oracle dans la démo est n de n
  • la figure interactive intègre les mêmes données figure_data()produit

Mode de défaillance de la production

FailureWhat the weak tool reportsWhat the analysis must prove
Decompiler older than the interpreterUnsupported opcode, output marked incomplete, or a claim of obfuscationThe magic word, the release it names, and the tool's documented ceiling
pyc read with the wrong interpretermarshal.loads raises or returns garbageOur unmarshaller reads names, constants, and sizes from any 3.9 to 3.15 file, and instructions come from the matching dis
Specialised opcodes mistaken for the file's contentCOMPARE_OP_INT in a listingA pyc holds the unspecialised form, specialisation happens in memory after warm-up
Local names reported for a default javac buildvar1, var2 presented as source namesLocalVariableTable is present only with -g, the 50 code bytes are identical either way
Branch target off by the instruction lengthA JVM listing whose jumps land mid-instructionOffsets relative to the branch opcode, checked against javap
Switch padding skippedA tableswitch with a wrong default and shifted casesZero to three padding bytes to a 4-aligned pc, 36 bytes for five cases
Wasm names assumed stripped"no symbols"Exports name every exported function, locals need the name section, wasm-strip removes only that section
Three tools, three spellingsget_local versus local.get, a function at 0x8b versus 0x8eSame bytes, and the listing that the comparison was captured with
Table row size guessedEvery row after the first table misreadRow sizes from HeapSizes, row counts, and coded index widths, equal to the oracle's
Local names expected in a release assemblyV_0 presented as the source nameLocals have types in StandAloneSig and names only in a PDB that DebugType none did not write
Reader guesses an unknown opcode or type codeA plausible mnemonicFormatError with the offset, and the oracle comparison stops there
Oracle captured from another buildTests pass on stale textbuild_fixtures.sh names every tool version and writes versions.txt and sha256.txt

Règlement d'exploitation

Lisez d'abord l'en-tête et nommez le producteur à qui il pointe. Lisez le contenant avec un lecteur qui refuse ce qu'il ne sait pas, et vérifiez sa liste par rapport à l'outil du format dans le même fichier. Puis dites, pour chaque nom et type dans la sortie d'un décompiler, que le contenant l'ait stocké ou que l'outil l'ait déduit.

Pour en savoir plus

This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.

Browse the complete course catalog or open this lesson on GitHub.