P : Por que há necessidade de outro descompilador, especialmente um descompilado?
R : Uma triste verdade é que a maioria dos descompiladores por aí são deficientes. Muitos não são capazes de descompilar construções triviais, outros não conseguem descompilar mais avançados, aqueles que aparentemente conseguem lidar com eles são prejudicados por suportar apenas arquiteturas e sistemas operacionais enfadonhos. E quase todos são escritos de tal forma que ajustá-los ou adicionar uma nova arquitetura é complicado. Um descompilador é uma ferramenta para engenharia reversa, mas ironicamente, se você quiser usar um descompilador típico de forma produtiva ou adequá-lo às suas necessidades, primeiro você precisará fazer a engenharia reversa do próprio descompilador, e isso pode facilmente levar meses (ou anos). .
A parte central de um descompilador (e de qualquer estrutura de transformação de programa) é a Representação Intermediária (IR). Um descompilador deve funcionar em IR e deve tomá-lo como entrada, e a conversão do assembler de uma arquitetura específica para este IR deve ser bem dissociada de um descompilador, caso contrário, será necessário um esforço extraordinário para adicionar suporte para outra arquitetura (o que por sua vez limita base de usuários de um descompilador).
A descompilação é uma tarefa complexa, portanto deve haver uma visão fácil do processo de descompilação. Isso significa que o IR usado por um descompilador deve ser amigável, por exemplo, usar uma sintaxe familiar aos programadores, mapear o mais diretamente possível para um montador de máquina típico, etc.
Os requisitos acima devem ser bastante óbvios por si só. Caso contrário, eles podem ser aprendidos nos livros sobre o assunto, por exemplo:
"O escritor do compilador também precisa de mecanismos que permitam aos humanos examinar o programa de RI de maneira fácil e direta. O interesse próprio deve garantir que os escritores do compilador prestem atenção a este último ponto."
(Keith Cooper, Linda Torczon, "Engenharia de um Compilador")
No entanto, projetos de descompiladores, incluindo os de código aberto, violam rotineiramente esses requisitos: eles estão fortemente acoplados a arquiteturas de máquinas específicas, não permitem a alimentação de IR e, muitas vezes, nem o expõem nem documentam ao usuário.
ScratchABlock é uma tentativa de dizer “não” a tais práticas e desenvolver uma estrutura de descompilação baseada nos requisitos acima. Observe que ScratchABlock pode ser considerado um projeto de aprendizagem/pesquisa e, além de boas intenções e críticas a outros projetos, pode não oferecer muito a um usuário casual - atualmente, ou possivelmente não. Certamente também pode ser criticado em muitos aspectos.
ScratchABlock é lançado sob os termos da Licença Pública Geral GNU v3 (GPLv3).
ScratchABlock é escrito na linguagem Python3 e testado com a versão 3.3 e superior, embora também possa funcionar com 3.2 ou inferior (não funcionará com versões legadas do Python2). Existem algumas dependências:
No Debian/Ubuntu Linux, eles podem ser instalados com sudo apt-get install python3-yaml python3-nose
. Alternativamente, você pode instalá-los através do próprio gerenciador de pacotes pip
do Python (deve funcionar para qualquer sistema operacional): pip3 install -r requirements.txt
.
ScratchABlock usa o assembler PseudoC como IR. É uma linguagem assembly expressa tanto quanto possível usando a sintaxe familiar da linguagem C. A ideia é que qualquer programador C o entenda intuitivamente (exemplo), mas há um esforço contínuo para documentar o PseudoC de forma mais formal.
Observe que com base nos requisitos descritos na seção anterior do documento, e seguindo o conhecido "paradigma Unix", ScratchABlock faz "uma coisa" - análises e transformações em programas PseudoC, e explicitamente não se preocupa em converter instruções de máquina de arquiteturas específicas em PseudoC (pelo menos por enquanto). Isso significa que ScratchABlock não força você a usar nenhum conversor/elevador específico - você pode usar o que quiser. Advertência: você precisaria ter um para usá-lo. Veja o final do documento para algumas dicas a esse respeito.
O código-fonte e os scripts de interface estão na raiz do repositório. Os scripts mais importantes são:
apply_xform.py
- Um driver central, permite aplicar uma sequência de transformações (ou em geral, scripts de análise/transformação de alto nível) a um único arquivo ou diretório de arquivos ("diretório do projeto").
inter_dataflow.py
- Driver de análise de fluxo de dados (WIP) interprocedural (global).
script_*.py
- Scripts de análise/transformação de alto nível para apply_xform.py
( --script
switch).
script_i_*.py
- Scripts de análise para inter_dataflow.py
.
run_tests
- O executor do conjunto de testes de regressão. A maior parte do testsuite é de alto nível, consistindo na execução de apply_xform.py com diferentes passagens no(s) arquivo(s) e na verificação dos resultados esperados.
Outros subdiretórios do repositório:
tests_unit
– Testes de unidade clássicos para módulos Python, escritos em Python.
tests
- O conjunto de testes principal. Embora de natureza integrativa, ele geralmente testa uma passagem em um arquivo simples, portanto segue a filosofia de teste de unidade. Os testes são representados como arquivos de entrada PseudoC, enquanto os resultados esperados - como PseudoC com anotação de blocos básicos e (quando aplicável) CFG em formato .dot. Observar esses casos de teste, tentar modificá-los e ver o resultado é a melhor maneira de aprender como o ScratchABlock funciona.
docs
- Uma coleção crescente de documentação. Por exemplo, há uma especificação da linguagem assembly PseudoC servindo como representação intermediária (IR) para ScratchABlock e uma pesquisa sobre por que outro IR existente não foi selecionado.
A abordagem atual do ScratchABlock é aumentar uma coleção de algoritmos relativamente pouco acoplados ("passagens") para análise e transformação de programas, cobri-los com testes e permitir fácil acesso do usuário a eles. A magia da descompilação consiste em aplicar esses algoritmos na ordem certa e no número certo de vezes. Então, para melhorar o desempenho da descompilação, essas passagens geralmente requerem um acoplamento mais rígido. Explorar essas direções é a próxima prioridade após a implementação do inventário de passes descrito acima.
Algoritmos e transformações implementadas pelo ScratchABlock:
Algoritmos gráficos:
Formulário de atribuição única estática (SSA):
Análise de fluxo de dados:
Propagação:
Eliminação de código morto (DCE)
Reescrevendo:
Estruturação do fluxo de controle:
Formatos de saída:
A ferramenta parceira do ScratchABlock é o ScratchABit, que é um desmontador interativo destinado a executar as tarefas de nível mais baixo do processo de descompilação, como separação de código de dados e identificação de limites de funções. ScratchABit geralmente funciona com uma sintaxe assembler de arquitetura nativa, mas para algumas arquiteturas (geralmente, RISCs fiéis), se um plugin adequado estiver disponível, ele pode gerar uma sintaxe PseudoC, que pode servir como entrada para ScratchABlock.