Como Descompiladores Funcionam
Imagine uma ferramenta mágica que recebe um executável como entrada e te devolve o código fonte original do projeto… Bom, esta ferramenta não existe. Mas a proposta de um descompilador (ou decompiler) é te devolver um código fonte que seja equivalente ao código do executável.
Ficou confuso? Esquenta não, a proposta do artigo é justamente explicar o que diabos descompiladores fazem.
Compiladores parecem complexos mas na verdade eles seguem um princípio muito simples: Dado um determinado código de entrada, é produzido um código de saída que seja semanticamente equivalente.
O que significa que o compilador nada mais faz que ler um código de entrada e produzir um outro código de saída que faz a mesma coisa, embora não necessariamente faça da mesma forma.
Imagine o seguinte cenário onde você precisa traduzir a frase “Eu estou bem” para o inglês, então você a traduz para “I am fine”.
As frases são diferentes, usam palavras diferentes, porém têm a mesma semântica em suas respectivas línguas. Ou seja, elas têm o mesmo significado.
O compilador é basicamente um “tradutor” de código convertendo código de uma linguagem X de entrada, para outra linguagem Y alvo. Mas sem alterar a semântica original, o código na linguagem alvo irá fazer exatamente a mesma coisa que o código da linguagem de origem.
Calma, calma! Sei que não parece mas ainda estamos falando de descompiladores… Você vai entender, segue lendo aí. 😤
Assembly e Linguagem Intermediária
Compiladores normalmente não convertem o código fonte diretamente em código de máquina. Existe o conceito de linguagem intermediária que é basicamente uma linguagem que representa o código de uma máquina virtual.
Pense assim: Assembly é a linguagem que representa o código da máquina real, já o código intermediário representa o código de uma máquina virtual hipotética. É o “Assembly” de uma máquina virtual. 😄
Muitos compiladores (não todos) convertem o código fonte para, primeiro, uma linguagem intermediária. O objetivo disto é obter um código em uma linguagem simplificada que pode ser mais facilmente otimizada e portada para várias arquiteturas de processadores.
Imagine, como você escreveria um otimizador de código para otimizar um código em C? Difícil, né? Do ponto de vista do computador C é uma linguagem extremamente complexa, é extremamente difícil escrever um algoritmo para otimizar o código de uma linguagem de alto nível.
Então o que o compilador realmente otimiza é o código em linguagem intermediária.
Depois desta primeira “etapa”, o compilador então compila o código de IL (Intermediate Language) para o Assembly da arquitetura alvo. Como o IL é uma linguagem bem simplista assim como Assembly, a compilação de IL para Assembly não é tão complicado (exceto é claro pela parte de otimização do código, que é bem complexo).
Por isto não é missão impossível escrever compiladores que compilem para diversas arquiteturas diferentes. O GCC e o Clang tem esta capacidade, por exemplo.
A propósito, notou que são basicamente dois processos de compilação aí? De C para IL, e de IL para Assembly.
A gente chama esta divisão de “frontend” e “backend” do compilador. Onde o frontend compila o código fonte para linguagem intermediária, e o backend faz o trabalho pesado de otimizar e compilar para Assembly.
Ah, mas ainda falta o código de máquina né? Bom, a tradução de Assembly para código de máquina é feita pelo assembler. Então só o backend do compilador e o assembler que de fato variam de acordo com a arquitetura. O frontend só precisa se preocupar em gerar código em IL.
Falando em Clang, você já ouviu falar em LLVM? Este projeto é basicamente uma infraestrutura para compiladores e coleção de ferramentas relacionadas, tipo depurador e linker por exemplo. Ele provém tudo o que é necessário do backend “para baixo”, então tudo o que sobra é implementar frontends para esta infraestrutura e pimba! Você tem um compilador novo em folha. O Clang é um exemplo de frontend para o LLVM.
Existe o LLVM IR que é o nome da linguagem intermediária do LLVM, e a partir dela que a infraestrutura faz seu trabalho. Otimização, compilação para Assembly, tradução para código de máquina, linkedição e por fim gerando o programa executável final.
Get FreeDev’s stories in your inbox
Join Medium for free to get updates from this writer.
Puxa, compiladores até que são complexos né? Mas pera, a gente não ia falar de descompiladores? Está até no título… Bom, estamos falando de descompiladores! 😃
Descompiladores
Plot twist: Descompiladores são compiladores! Você não leu tudo isto a toa, eu estava explicando o que descompiladores fazem desde o começo.
Como eu disse no começo do artigo, o objetivo de um descompilador é ler um código de entrada X e traduzir para outra linguagem Y… Não é isto que compiladores fazem? Pois é. 😃
Só que a gente dá o nome de “descompilador” devido ao seu propósito ser o exato oposto de um compilador. Ou seja, ele faz a mesma coisa que eu descrevi acima falando de compiladores só que na ordem inversa.
Compiladores convertem o código fonte para IL, de IL para Assembly e de Assembly para código de máquina.
O descompilador converte código de máquina para Assembly*, de Assembly para IL e de IL para o código fonte da linguagem alvo.
É a mesma coisa só que na ordem inversa, por isto descompilador. Ele desfaz o que o compilador fez. 👍
*Nota: O processo de converter código de máquina para Assembly a gente chama de disassembly. Acho que você já ouviu falar em disassembler, né? Que é o nome dado as ferramentas que fazem isto.
Como em código de máquina a gente perde muita “informação” do código, é difícil obter um código fonte resultante que seja igual ao código original. Então o que o descompilador cospe é um código fonte que é equivalente ao código de máquina do executável, é o máximo que ele consegue.
Lembre-se do processo todo que eu descrevi. Para o compilador fazer o trabalho dele é mais simples porque ele tem muita informação, o código de “entrada” tem muita semântica.
E durante cada etapa de compilação essa semântica vai se perdendo, principalmente na otimização do código.
Já o descompilador tem pouca informação a partir de um código de máquina otimizado, ele tira leite de pedra para conseguir gerar algo legível.
E isto explica porque o código que o descompilador cospe costuma ser muito bagunçado e nem sempre totalmente funcional.
Mas existem algumas linguagens cuja descompilação de código gera um código fonte bastante legível e bem aproximado do código fonte original. Java e C# são exemplos.
O motivo disto é que o código destas linguagens não são, normalmente, compilados para código nativo do processador. Eles são compilados para código de uma máquina virtual e “param” por aí mesmo.
Com certeza tu já deve ter ouvido falar na JVM, que é a máquina virtual do Java. Assim como no caso do LLVM IR que é o código intermediário de uma máquina virtual hipotética, no caso do Java a máquina virtual não é apenas hipotética.
A linguagem Smali é o “Assembly” da JVM, que seria equivalente ao LLVM IR.
E ao invés do Smali ser compilado para Assembly, ele é compilado para o bytecode que a JVM interpreta. Onde “bytecode” é tipo o código de máquina de uma máquina virtual.
Como estamos falando de uma máquina virtual, o bytecode não precisa ser tão simplista assim. E de fato não é, o bytecode costuma ter mais informações e por isto é mais fácil descompilar bytecode e conseguir um código fonte mais próximo do código original.
Eba! Acabou! 😜
Para não te deixar à deriva, vou te indicar alguns projetos que você pode dar uma olhada e, se estiver interessado, aprender a usar:
- RetDec — Descompilador feito pela Avast e advinha? Baseado em LLVM. Tu consegue ter uma noção de como funciona, né? Hihi, pois é.
- Ghidra — Embora não seja um descompilador, a ferramenta tem um descompilador integrado. :)
- apktool — Faz o disasembly de bytecode da JVM, gerando código Smali, e outras paradas assim. É para fazer engenharia reversa em aplicativos APK.






