EDIT : en espérant le rendre plus lisible.
Le matériel ne considère pas la mémoire comme une longue liste d'octets non organisés. Tous les processeurs, à longueur de mot fixe ou variable, ont une méthode de démarrage spécifique. Il s'agit généralement d'une adresse connue dans l'espace mémoire/adresse du processeur, avec soit une adresse de la première instruction du code d'amorçage, soit la première instruction elle-même. A partir de là et pour chaque instruction, l'adresse de l'instruction courante est l'endroit où commencer le décodage.
Pour un x86 par exemple, il doit regarder le premier octet. Selon le décodage de cet octet, il peut avoir besoin de lire d'autres octets d'opcode. Si l'instruction nécessite une adresse, un offset ou une autre forme de valeur immédiate, ces octets sont également présents. Très rapidement, le processeur sait exactement combien d'octets contient cette instruction. Si le décodage montre que l'instruction contient 5 octets et qu'elle a commencé à l'adresse 0x10, l'instruction suivante se trouve à 0x10+5 ou 0x15. Ce processus se poursuit indéfiniment. Les branchements inconditionnels, qui, selon le processeur, peuvent prendre différentes formes, ne supposent pas que les octets qui suivent l'instruction sont une autre instruction. Les branchements, conditionnels ou inconditionnels, vous donnent un indice de l'endroit où une autre instruction ou une série d'instructions commence dans la mémoire.
Notez que le X86 d'aujourd'hui ne récupère pas un octet à la fois lorsqu'il décode une instruction, des lectures de taille raisonnable sont effectuées, probablement 64 bits à la fois, et le processeur en extrait les octets selon les besoins. Lors de la lecture d'un seul octet à partir d'un processeur moderne, le bus mémoire effectue toujours une lecture complète et présente tous ces bits sur le bus où le contrôleur de mémoire ne tire que les bits qu'il recherchait, ou bien il peut aller jusqu'à conserver ces données. Dans certains processeurs, il peut y avoir deux instructions de lecture de 32 bits à des adresses consécutives, mais une seule lecture de 64 bits a lieu sur l'interface mémoire.
Je vous recommande vivement d'écrire un désassembleur et/ou un émulateur. Pour les instructions de longueur fixe, c'est assez facile, il suffit de commencer par le début et de décoder au fur et à mesure que l'on avance dans la mémoire. Un désassembleur de longueur de mot fixe peut aider à apprendre le décodage des instructions, qui fait partie de ce processus, mais il ne vous aidera pas à comprendre comment suivre des instructions de longueur de mot variable et comment les séparer sans se désaligner.
Le MSP430 est un bon choix comme premier désassembleur. Il existe des outils gnu asm et C, etc (et llvm d'ailleurs). Commencez par l'assembleur puis le C ou prenez des binaires pré-fabriqués. La clé est que vous devez parcourir le code comme le processeur, commencer par le vecteur de réinitialisation et parcourir tout le code. Lorsque vous décodez une instruction, vous connaissez sa longueur et savez où se trouve l'instruction suivante jusqu'à ce que vous trouviez un branchement inconditionnel. À moins que le programmeur n'ait intentionnellement laissé un piège pour tromper le désassembleur, supposez que toutes les branches conditionnelles ou inconditionnelles pointent vers des instructions valides. Une après-midi ou une soirée est tout ce qu'il faut pour faire le tour de la question ou au moins comprendre le concept. Il n'est pas nécessaire de décoder complètement l'instruction, ni d'en faire un désassembleur complet, il suffit d'en décoder suffisamment pour déterminer la longueur de l'instruction et déterminer s'il s'agit d'une branche et si oui, où. Comme il s'agit d'une instruction de 16 bits, vous pouvez, si vous le souhaitez, construire une table de toutes les combinaisons possibles de bits d'instruction et de leurs longueurs, ce qui peut vous faire gagner du temps. Vous devez toujours décoder votre chemin à travers les branches.
Certaines personnes pourraient utiliser la récursion, à la place j'utilise une carte mémoire qui me montre quels octets sont le début d'une instruction, quels octets/mots font partie d'une instruction mais pas le premier octet/mot et quels octets je n'ai pas encore décodés. Je commence par prendre les vecteurs d'interruption et de d'interruption et de réinitialisation et je les utilise pour marquer le point de départ des instructions. une boucle qui décode les instructions en cherchant d'autres points de départ. Si un passage se produit sans aucun autre point de départ, alors j'ai terminé cette phase. Si à un moment donné, je trouve un point de départ d'instruction qui se trouve au milieu d'une instruction, il y a un problème qui nécessitera une intervention humaine pour le résoudre. En désassemblant de vieilles roms de jeux vidéo par exemple, vous êtes susceptible de voir ceci, un assembleur écrit à la main. Les instructions générées par le compilateur ont tendance à être très propres et prévisibles. Si je passe par là avec une carte mémoire propre des instructions et de ce qui reste (supposons des données), je peux faire un passage en sachant où sont les instructions, les décoder et les imprimer. Ce qu'un désassembleur pour des jeux d'instructions à longueur de mot variable ne peut jamais faire, c'est trouver chaque instruction. Si le jeu d'instructions possède par exemple une table de saut ou une sorte d'adresse d'exécution calculée, vous ne les trouverez pas toutes sans exécuter réellement le code.
Il existe un certain nombre d'émulateurs et de désassembleurs, si vous voulez essayer de suivre le mouvement au lieu d'écrire le vôtre, j'en ai quelques-uns moi-même. http://github.com/dwelch67 .
Il y a des avantages et des inconvénients pour et contre la longueur variable et fixe des mots. La longueur fixe a des avantages, c'est sûr, facile à lire, facile à décoder, tout est beau et correct, mais pensez à la RAM, au cache en particulier, vous pouvez faire entrer beaucoup plus d'instructions x86 dans le même cache qu'un ARM. D'un autre côté, un ARM peut décoder beaucoup plus facilement, avec beaucoup moins de logique, d'énergie, etc. Historiquement, la mémoire était coûteuse, la logique était coûteuse et le système fonctionnait par octets. Un code d'opération d'un seul octet vous limitait à 256 instructions, ce qui faisait que certains codes d'opération nécessitaient plus d'octets, sans parler des immédiats et des adresses qui rendaient la longueur du mot variable. Gardez la compatibilité inverse pendant des décennies et vous vous retrouvez là où vous êtes maintenant.
Pour ajouter à toute cette confusion, ARM, par exemple, a maintenant un jeu d'instructions à longueur de mot variable. Thumb avait une seule instruction à mot variable, le branchement, mais vous pouvez facilement la décoder comme étant de longueur fixe. Mais ils ont créé thumb2 qui ressemble vraiment à un jeu d'instructions à longueur de mot variable. En outre, la plupart des processeurs qui prennent en charge les instructions ARM 32 bits prennent également en charge les instructions thumb 16 bits, de sorte que même avec un processeur ARM, vous ne pouvez pas simplement aligner les données par mots et décoder au fur et à mesure, vous devez utiliser une longueur de mot variable. Ce qui est pire, c'est que les transitions ARM vers/depuis la poucette sont décodées par exécution, vous ne pouvez normalement pas simplement désassembler et comprendre le bras de la poucette. Un branchement généré par le compilateur en mode mixte implique souvent le chargement d'un registre avec l'adresse vers laquelle le branchement doit être effectué, puis l'utilisation d'une instruction bx pour y accéder. Le désassembleur doit donc examiner la bx, remonter dans l'exécution pour trouver le registre utilisé dans le branchement, espérer y trouver un chargement et espérer qu'il s'agisse du segment .text à partir duquel il est chargé.