Já fiz isso muitas vezes e continuo fazendo. Neste caso, onde seu objetivo principal é ler e não escrever assembler, acho que isso se aplica.
Escreva seu próprio desmontador. Não com o propósito de fazer o próximo grande desmontador, este é estritamente para você. O objetivo é aprender o conjunto de instruções. Esteja eu aprendendo assembler em uma nova plataforma, lembrando-me de assembler para uma plataforma que conheci. Comece com apenas algumas linhas de código, adicionando registradores, por exemplo, e fazendo pingue-pongue entre desmontar a saída binária e adicionar mais e mais instruções complicadas no lado da entrada:
1) aprender o conjunto de instruções para o processador específico
2) aprender as nuances de como escrever código em montagem para o referido processador de modo que você possa mexer cada bit de opcode em cada instrução
3) você aprende o conjunto de instruções melhor do que a maioria dos engenheiros que usam esse conjunto de instruções para ganhar a vida
No seu caso, há alguns problemas, eu normalmente recomendo o conjunto de instruções ARM para começar, há mais produtos baseados em ARM vendidos hoje do que qualquer outro (computadores x86 incluídos). Mas a probabilidade de você estar usando o ARM agora e não conhecer o montador suficiente para escrever código de inicialização ou outras rotinas sabendo o ARM pode ou não ajudar no que você está tentando fazer. A segunda e mais importante razão para o ARM primeiro é porque os comprimentos das instruções são de tamanho fixo e alinhados. Desmontar instruções de comprimento variável como x86 pode ser um pesadelo como seu primeiro projeto, e o objetivo aqui é aprender o conjunto de instruções para não criar um projeto de pesquisa. O terceiro ARM é um conjunto de instruções bem executado, os registros são criados iguais e não possuem nuances especiais individuais.
Portanto, você terá que descobrir com qual processador deseja começar. Eu sugiro o msp430 ou ARM primeiro, depois o ARM primeiro ou segundo e então o caos do x86. Não importa a plataforma, qualquer plataforma que valha a pena usar tem planilhas de dados ou manuais de referência de programadores gratuitos do fornecedor que incluem o conjunto de instruções, bem como a codificação dos opcodes (os bits e bytes da linguagem de máquina). Com o propósito de aprender o que o compilador faz e como escrever código com o qual o compilador não precisa se esforçar, é bom conhecer alguns conjuntos de instruções e ver como o mesmo código de alto nível é implementado em cada conjunto de instruções com cada compilador com cada otimização configuração. Você não quer otimizar seu código apenas para descobrir que o tornou melhor para um compilador / plataforma, mas muito pior para todos os outros.
Oh, para desmontar conjuntos de instruções de comprimento variável, em vez de simplesmente começar do início e desmontar cada palavra de quatro bytes linearmente através da memória como faria com o ARM ou a cada dois bytes como o msp430 (O msp430 tem instruções de comprimento variável, mas você ainda pode sobreviver passando linearmente pela memória se você começar nos pontos de entrada da tabela de vetores de interrupção). Para comprimento variável, você deseja encontrar um ponto de entrada com base em uma tabela de vetores ou conhecimento sobre como o processador inicializa e seguir o código na ordem de execução. Você tem que decodificar cada instrução completamente para saber quantos bytes são usados, então se a instrução não for um desvio incondicional, assuma que o próximo byte após essa instrução é outra instrução. Você também deve armazenar todos os endereços de ramificação possíveis e assumir que esses são os endereços de byte iniciais para obter mais instruções. A única vez que tive sucesso, fiz várias passagens pelo binário. Começando no ponto de entrada, marquei aquele byte como o início de uma instrução e então decodifiquei linearmente através da memória até atingir um desvio incondicional. Todos os alvos de ramificação foram marcados como endereços iniciais de uma instrução. Fiz várias passagens pelo binário até não encontrar nenhum novo destino de ramificação. Se a qualquer momento você encontrar, digamos, uma instrução de 3 bytes, mas por alguma razão você marcou o segundo byte como o início de uma instrução, você tem um problema. Se o código foi gerado por um compilador de alto nível, isso não deve acontecer a menos que o compilador esteja fazendo algo mal, se o código tiver um assembler escrito à mão (como, digamos, um antigo jogo de arcade), é bem possível que haja desvios condicionais que nunca podem acontecer como r0 = 0 seguido por um salto, senão zero. Você pode ter que editar manualmente aqueles fora do binário para continuar. Para seus objetivos imediatos, que suponho que serão em x86, não acho que você terá problemas.
Eu recomendo as ferramentas gcc, mingw32 é uma maneira fácil de usar as ferramentas gcc no Windows se x86 for seu destino. Senão, o mingw32 plus msys é uma plataforma excelente para gerar um compilador cruzado a partir de fontes binutils e gcc (geralmente muito fácil). mingw32 tem algumas vantagens sobre o cygwin, como programas significativamente mais rápidos e você evita o inferno do cygwin dll. gcc e binutils permitirão que você escreva em C ou assembler e desmonte seu código e há mais páginas da web do que você pode ler, mostrando como fazer qualquer um ou todos os três. Se você vai fazer isso com um conjunto de instruções de comprimento variável, eu recomendo fortemente que você use um conjunto de ferramentas que inclui um desmontador. Um desmontador de terceiros para x86, por exemplo, será um desafio de usar, pois você nunca sabe realmente se ele foi desmontado corretamente. Parte disso também depende do sistema operacional; o objetivo é compilar os módulos em um formato binário que contenha instruções de marcação de informações dos dados para que o desmontador possa fazer um trabalho mais preciso. Sua outra escolha para esse objetivo principal é ter uma ferramenta que possa compilar diretamente no assembler para sua inspeção e, em seguida, esperar que, ao compilar para um formato binário, crie as mesmas instruções.
A resposta curta (ok, ligeiramente mais curta) para sua pergunta. Escreva um desmontador para aprender um conjunto de instruções. Eu começaria com algo RISCy e fácil de aprender como o ARM. Uma vez que você conhece um conjunto de instruções, outros se tornam muito mais fáceis de entender, geralmente em poucas horas, no terceiro conjunto de instruções, você pode começar a escrever código quase imediatamente usando a folha de dados / manual de referência para a sintaxe. Todos os processadores que valem a pena usar têm uma folha de dados ou manual de referência que descreve as instruções até os bits e bytes dos opcodes. Aprenda um processador RISC como ARM e um CISC como x86 o suficiente para sentir as diferenças, coisas como ter que passar por registros para tudo ou ser capaz de executar operações diretamente na memória com menos ou nenhum registro. Instruções de três operandos versus dois, etc. Conforme você ajusta seu código de alto nível, compilar para mais de um processador e comparar a saída. A coisa mais importante que você aprenderá é que não importa quão bom o código de alto nível seja escrito, a qualidade do compilador e as escolhas de otimização feitas fazem uma grande diferença nas instruções reais. Eu recomendo llvm e gcc (com binutils), nenhum produtoótimo código, mas eles são multiplataformas e alvos múltiplos e ambos têm otimizadores. E ambos são gratuitos e você pode construir compiladores cruzados facilmente a partir de fontes para vários processadores de destino.