Qué es descompilar y qué herramientas sirven en cada lenguaje: lo probamos con siete

Descompilar es intentar recuperar el código fuente de un programa a partir del programa ya compilado: el proceso inverso al que hace un compilador. Sirve para entender cómo funciona un programa del que no tienes el código, para analizar malware o para recuperar algo que se perdió. Pero lo que sale nunca es el original, y lo que se parece depende muchísimo del lenguaje. Para comprobarlo, hemos escrito el mismo programa en siete lenguajes, lo hemos compilado y lo hemos descompilado con las herramientas actuales. Esta guía cuenta qué pasó y qué herramienta usar en cada caso.

Compilar, desensamblar y descompilar

Un compilador traduce el código que escribe una persona a algo que puede ejecutar una máquina: instrucciones del procesador (código máquina) o instrucciones para una máquina virtual (bytecode), como la de Java o la de .NET. Desde ahí se puede volver atrás en dos niveles:

  • Desensamblar es traducir esos bytes a instrucciones legibles, una por una: «mueve este valor aquí», «resta 48», «salta si es menor». Es exacto, porque cada byte corresponde a una instrucción, pero leerlo cuesta.
  • Descompilar va un paso más allá: agrupa esas instrucciones en bucles, condiciones, variables y funciones, y escribe un código en un lenguaje de alto nivel. Ese paso ya no es exacto: es una reconstrucción.

Código fuente suma += digito; if (digito > 9) … compilar Programa compilado 0f be 02 83 e8 30 … bytes que ejecuta la CPU desensamblar Desensamblado movsx eax, byte [rdx] sub eax, 0x30 descompilar Descompilado iVar2 = *pcVar5 + -0x30; if (9 < iVar2) … Compilar es fácil y exacto. Volver atrás es adivinar: los nombres y los comentarios ya no están en el programa compilado.

Las líneas del esquema son reales: salen del programa en C del experimento, desensamblado con objdump y descompilado con Ghidra 12.2. Esquema: decodigo.com.

Qué se pierde al compilar

Un descompilador no puede recuperar lo que el compilador tiró. Según el lenguaje y las opciones de compilación, se pierden:

  • Los comentarios, siempre. No llegan nunca al programa compilado.
  • Los nombres de variables locales, y en código máquina sin símbolos, también los de las funciones: es_valido pasa a ser FUN_001011c0.
  • Los tipos: en código máquina, un número, un puntero o un carácter son bytes; el descompilador tiene que deducir qué eran.
  • La estructura: el compilador optimiza, y a veces mucho. Puede desenrollar un bucle, meter una función dentro de otra o cambiar una división por una multiplicación, y el descompilador ve el resultado, no tu código.

Por eso el resultado depende sobre todo del destino de la compilación. Cuanto más información conserva el formato compilado, más se parece lo descompilado al original:

Qué tan fiel sale lo descompilado Bytecode con metadatos Java, Kotlin, Android, C#, .NET Resultado: casi como el original Bytecode de Python .pyc Resultado: depende mucho de la versión JavaScript minificado webs y aplicaciones Electron Resultado: legible, pero sin nombres WebAssembly .wasm Resultado: instrucciones de bajo nivel Código máquina C, C++, Go, Rust, Swift Resultado: una aproximación en c De arriba abajo, cada vez se pierde más.

Resumen de lo que vimos en el experimento. Esquema: decodigo.com.

El experimento: el mismo programa en siete lenguajes

El programa comprueba un número con el algoritmo de Luhn, el mismo de nuestro validador de tarjetas: recorre las cifras de derecha a izquierda, duplica una sí y otra no y mira si la suma es múltiplo de 10. Esta es la versión en C:

#include <stdio.h>
#include <string.h>

/* Comprueba un número con el algoritmo de Luhn (tarjetas bancarias, IMEI). */
__attribute__((noinline)) int es_valido(const char *numero) {
    int suma = 0, dobla = 0;
    for (int i = (int)strlen(numero) - 1; i >= 0; i--) {
        int digito = numero[i] - '0';
        if (dobla) {
            digito *= 2;
            if (digito > 9) digito -= 9;
        }
        suma += digito;
        dobla = !dobla;
    }
    return suma % 10 == 0;
}

int main(int argc, char **argv) {
    const char *numero = argc > 1 ? argv[1] : "4242424242424242";
    printf("%s: %s\n", numero, es_valido(numero) ? "válido" : "no válido");
    return 0;
}

Lo escribimos igual en Java, C#, Python, Go y Rust, y una versión sin biblioteca estándar para WebAssembly. Todo se compiló y descompiló en contenedores desechables, con las versiones que se indican en cada apartado.

Java: casi el original

Un archivo .class guarda los nombres de las clases, de los métodos y de los campos, y unas instrucciones de máquina virtual muy cercanas al código. Lo compilamos con javac 25 y lo descompilamos con Vineflower 1.12.0:

static boolean esValido(String var0) {
   int var1 = 0;
   boolean var2 = false;

   for (int var3 = var0.length() - 1; var3 >= 0; var3--) {
      int var4 = var0.charAt(var3) - '0';
      if (var2) {
         var4 *= 2;
         if (var4 > 9) {
            var4 -= 9;
         }
      }

      var1 += var4;
      var2 = !var2;
   }

   return var1 % 10 == 0;
}

Es el mismo método, con una sola diferencia: las variables locales se llaman var1, var2… porque javac no guarda sus nombres salvo que se compile con -g. CFR 0.152, otro descompilador veterano, da un resultado igual de correcto, pero lo reescribe a su manera: junta las dos condiciones en una y pone el carácter '0' como el número 48:

static boolean esValido(String string) {
    int n = 0;
    boolean bl = false;
    for (int i = string.length() - 1; i >= 0; --i) {
        int n2 = string.charAt(i) - 48;
        if (bl && (n2 *= 2) > 9) {
            n2 -= 9;
        }
        n += n2;
        bl = !bl;
    }
    return n % 10 == 0;
}

C#: también casi el original

.NET funciona como Java: el compilador produce un lenguaje intermedio (IL) con muchos metadatos. Lo compilamos con el SDK de .NET 10 en modo Release y lo descompilamos con ilspycmd 11.1, la versión de línea de órdenes de ILSpy:

public static bool EsValido(string numero)
{
    int num = 0;
    bool flag = false;
    for (int num2 = numero.Length - 1; num2 >= 0; num2--)
    {
        int num3 = numero[num2] - 48;
        if (flag)
        {
            num3 *= 2;
            if (num3 > 9)
            {
                num3 -= 9;
            }
        }
        num += num3;
        flag = !flag;
    }
    return num % 10 == 0;
}

Aquí incluso se conservan los nombres de los parámetros (numero), porque forman parte de los metadatos públicos del método. Solo se pierden los de las variables locales.

Python: depende de la versión

Python también compila, a archivos .pyc, pero su bytecode cambia en cada versión del lenguaje, y los descompiladores van por detrás. Compilamos el programa con Python 3.8, 3.13 y 3.14 y lo pasamos por pycdc (Decompyle++), en su versión de abril de 2026. Con Python 3.8, el resultado es perfecto, hasta el comentario de documentación:

pycdc, Python 3.8import sys

def es_valido(numero):
    '''Comprueba un número con el algoritmo de Luhn (tarjetas bancarias, IMEI).'''
    (suma, dobla) = (0, False)
    for caracter in reversed(numero):
        digito = int(caracter)
        if dobla:
            digito *= 2
            if digito > 9:
                digito -= 9
        suma += digito
        dobla = not dobla
    return suma % 10 == 0

Con Python 3.13, se rinde a la segunda línea, y con 3.14 ni siquiera reconoce el archivo:

pycdc, Python 3.13 y 3.14Unsupported opcode: MAKE_FUNCTION (122)
# Source Generated with Decompyle++
# File: valida-313.pyc (Python 3.13)

import sys
# WARNING: Decompyle incomplete

Bad MAGIC!
Could not load file /w/bin/pyc/valida-314.pyc

Probamos también decompyle3 3.9.3, que solo admite Python 3.7 y 3.8 (con 3.13 responde «Unsupported Python version»). Con 3.8 terminó diciendo «okay decompiling», pero su código tiene un fallo: metió las dos últimas líneas del bucle y el return un nivel más adentro.

import sys
def es_valido(numero):
    """Comprueba un número con el algoritmo de Luhn (tarjetas bancarias, IMEI)."""
    suma, dobla = (0, False)
    for caracter in reversed(numero):
        digito = int(caracter)
        if dobla:
            digito *= 2
            if digito > 9:
                digito -= 9
            suma += digito
            dobla = not dobla
        return suma % 10 == 0

Parece correcto, y para el número de ejemplo da la respuesta buena por casualidad. Con otro número, se ve el error:

el original y lo descompilado, ejecutadosoriginal:     4242424242424242: válido
descompilado: 4242424242424242: válido
original:     4242424242424241: no válido
descompilado: 4242424242424241: válido

Es la lección más importante de esta guía: el código descompilado puede tener errores aunque la herramienta no avise. Si lo vas a usar para algo, comprueba que se comporta igual que el programa original.

C: una aproximación muy buena, y sin nombres si los quitan

Con C ya no hay máquina virtual: el compilador (gcc 14.2, con -O2) produce instrucciones del procesador. Lo descompilamos con Ghidra 12.2, la herramienta gratuita de la NSA, en su versión publicada el 22 de octubre de 2026:

bool es_valido(char *param_1)

{
  bool bVar1;
  int iVar2;
  size_t sVar3;
  uint uVar4;
  char *pcVar5;
  int iVar6;
  
  sVar3 = strlen(param_1);
  uVar4 = (int)sVar3 - 1;
  if ((int)uVar4 < 0) {
    return true;
  }
  iVar6 = 0;
  pcVar5 = param_1 + (int)uVar4;
  bVar1 = false;
  do {
    iVar2 = *pcVar5 + -0x30;
    if (bVar1) {
      iVar2 = (*pcVar5 + -0x30) * 2;
      if (9 < iVar2) {
        iVar2 = iVar2 + -9;
      }
    }
    pcVar5 = pcVar5 + -1;
    iVar6 = iVar6 + iVar2;
    bVar1 = (bool)(bVar1 ^ 1);
  } while (param_1 + (((long)(int)sVar3 + -2) - (ulong)uVar4) != pcVar5);
  uVar4 = iVar6 * -0x33333333 + 0x19999998;
  return (uVar4 >> 1 | (uint)((uVar4 & 1) != 0) << 0x1f) < 0x19999999;
}

La lógica está entera: el bucle que recorre la cadena hacia atrás, el resto de 48 (0x30, el código del carácter «0»), el doble y el «menos 9». Pero mira la última línea: no aparece ningún % 10. El compilador cambió la división, que es lenta, por una multiplicación por un número mágico (-0x33333333) y una comparación, un truco clásico de optimización, y Ghidra muestra el truco, no la intención. Y si al programa le quitan los símbolos con strip, como suele pasar con lo que se distribuye, la función pierde hasta el nombre:

bool FUN_001011c0(char *param_1)

{

Go y Rust: los más difíciles

Go y Rust también compilan a código máquina, pero con convenciones propias que los descompiladores pensados para C entienden peor. Con Go 1.27, Ghidra encontró la función por su nombre, main.esValido, porque Go guarda una tabla con los nombres de las funciones incluso en los programas de producción, pero se perdió el cuerpo del bucle:

void main_esValido(string numero,bool ~r0)

{
  int in_RBX;
  
  do {
                    /* Unresolved local var: int i@[???] */
    in_RBX = in_RBX + -1;
  } while (-1 < in_RBX);
  return;
}

Con Rust 1.99, compilado con optimizaciones, pasó lo contrario: el compilador convirtió el bucle en instrucciones vectoriales, que procesan muchas cifras a la vez, y Ghidra reconstruyó una función de 452 líneas casi ilegible:

/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
/* valida[b87ace54fbb2847b]::es_valido */

bool __rustcall valida[b87ace54fbb2847b]::es_valido(long param_1,ulong param_2)

{
  char *pcVar1;
  undefined4 uVar2;
  undefined4 uVar3;
  short sVar4;
  short sVar5;
  short sVar6;
  short sVar7;
  short sVar8;
  …
  (y así hasta 452 líneas)

No es que Ghidra sea malo: es que en estos casos la descompilación automática solo es el punto de partida, y el trabajo real lo hace una persona renombrando variables, corrigiendo tipos y reconociendo patrones. Para Go existen ayudas específicas como GoReSym, que recupera nombres y tipos de su tabla interna.

WebAssembly: de vuelta a texto

WebAssembly es el formato con el que se ejecuta código de C, C++ o Rust dentro del navegador. Compilamos la función con clang 19 y la pasamos por wasm2wat, de wabt 1.0.42, que la convierte a su formato de texto. Es más un desensamblado que una descompilación:

(module $luhn.wasm
  (type (;0;) (func (param i32 i32) (result i32)))
  (func $es_valido (type 0) (param i32 i32) (result i32)
    (local i32 i32 i32 i32)
    i32.const 1
    local.set 2
    block  ;; label = @1
      local.get 1
      i32.const 1
      i32.lt_s
      br_if 0 (;@1;)
      local.get 1
      i32.const 1
      i32.and
      local.set 3
      block  ;; label = @2
        block  ;; label = @3
          local.get 1
          i32.const 1
          i32.ne
          br_if 0 (;@3;)
          i32.const 0
          local.set 2
          br 1 (;@2;)
    …

wabt tenía una herramienta que iba más allá, wasm-decompile, pero la quitó en junio de 2026 porque nadie la mantenía. Para algo más legible quedan wasm2c, que lo traduce a C, o el complemento de WebAssembly para Ghidra.

Qué herramienta usar en cada lenguaje

Versiones y licencias comprobadas el 22 de octubre de 2026. Las marcadas como gratuitas lo son para cualquier uso salvo que se indique otra cosa.

Código máquina: C, C++, Go, Rust, Swift

  • Ghidra (NSA, Apache 2.0, gratuito): la versión 12.2 salió el 22 de octubre de 2026. Descompila a C para decenas de arquitecturas y tiene analizadores específicos para Go y Swift. Tenemos una guía práctica de Ghidra.
  • IDA (Hex-Rays): el estándar comercial; la versión más reciente es la 9.4. IDA Free es gratuita solo para uso no comercial, y su descompilador funciona en la nube y solo para x86 y x86-64.
  • Binary Ninja (Vector 35): versión 6.0. La edición Free, no comercial, descompila x86, x86-64 y ARM; la Personal cuesta $199 USD y la Commercial, $1,799 USD.
  • radare2 (6.2.4) y su derivado Rizin (0.9.1), con la interfaz gráfica Cutter (2.5.0): libres, para la línea de órdenes, y con el descompilador de Ghidra integrado mediante r2ghidra y rz-ghidra.
  • Para Go, GoReSym (Mandiant, MIT, versión 3.4.1) recupera los nombres de funciones, tipos y archivos de un programa de Go, aunque lo hayan compilado sin símbolos.

Java, Kotlin y la JVM

  • Vineflower (Apache 2.0, versión 1.12.0): el sucesor moderno de Fernflower, el descompilador que lleva IntelliJ IDEA. Entiende las novedades recientes de Java.
  • CFR (MIT): muy usado y fiable, aunque su última versión, la 0.152, es de diciembre de 2021. Aun así, descompiló bien nuestra clase de Java 25.
  • Recaf (MIT): un editor de bytecode con varios descompiladores dentro; su versión 4 sigue en alfa.
  • JD-GUI y Procyon: los veteranos. Sus últimas versiones son de 2019 y 2022, así que para Java moderno conviene usar los anteriores.
  • Kotlin compila al mismo bytecode, así que estas herramientas lo descompilan, pero a Java, no a Kotlin.

Android

  • JADX (Apache 2.0, versión 1.5.6): convierte el código de una app (.apk o .dex) directamente a Java, con interfaz gráfica o desde la línea de órdenes. Hay incluso un servidor MCP para que lo use un asistente de IA: lo explicamos en jadx-headless-mcp y en la guía de REA.
  • Apktool (Apache 2.0, versión 3.0.3): no descompila a Java, sino a smali, el ensamblador de Android, y recupera los recursos y el manifiesto. Permite modificar la app y volver a empaquetarla.

C# y .NET

  • ILSpy (MIT, versión 11.1): el de referencia, con interfaz para Windows, la herramienta de línea de órdenes ilspycmd que usamos y una extensión para Visual Studio Code.
  • dnSpyEx (GPL 3.0, versión 6.6.0): la continuación comunitaria de dnSpy, que además de descompilar permite depurar y editar ensamblados.
  • dotPeek (JetBrains): gratuito, descompila a C# o a IL.

Python

  • pycdc (Decompyle++) (GPL 3.0): escrito en C++, admite muchas versiones, pero con las más recientes se queda a medias, como vimos con 3.13 y 3.14. No publica versiones: se compila desde su repositorio.
  • decompyle3 y uncompyle6 (GPL 3.0, versión 3.9.3 de ambos): decompyle3 cubre Python 3.7 y 3.8, y uncompyle6, desde versiones muy antiguas hasta la 3.8. Para Python moderno no sirven.
  • Lo más eficaz con un .pyc moderno suele ser desensamblarlo con el módulo dis del propio Python y reconstruir el código a mano, o con ayuda de un asistente de IA, comprobando el resultado.

WebAssembly y JavaScript

  • wabt (Apache 2.0, versión 1.0.42): wasm2wat lo pasa a texto y wasm2c, a C. Para Ghidra existe el complemento ghidra-wasm-plugin (GPL 3.0, versión 2.4.0).
  • JavaScript no se compila: se distribuye como texto, pero minificado (sin espacios, con nombres de una letra) y a veces ofuscado. Lo primero es buscar los mapas de fuente (archivos .map), que si están publicados permiten ver el código original en las herramientas de desarrollo del navegador. Si no, un formateador como Prettier lo hace legible, y webcrack (MIT, versión 2.16.0) deshace las ofuscaciones más comunes y separa los módulos de un paquete de webpack.

Para qué se usa, y qué dice la ley

Descompilar es una técnica habitual y legítima en muchos casos:

  • Interoperabilidad: conseguir que tu programa funcione con otro del que no tienes documentación.
  • Seguridad: analizar malware, buscar vulnerabilidades o auditar un programa antes de confiar en él.
  • Recuperar lo propio: el código de un programa tuyo cuyo fuente se perdió.
  • Aprender y competir: entender cómo funciona algo y resolver retos de seguridad (CTF).

La ley, en cambio, la limita, y no es igual en todas partes. Esto es un resumen, no un consejo legal:

  • En la Unión Europea, la Directiva 2009/24/CE sobre programas de ordenador permite descompilar sin permiso del autor cuando sea «indispensable para obtener la información necesaria para la interoperabilidad de un programa de ordenador creado de forma independiente», siempre que lo haga quien tiene derecho a usar el programa, que esa información no esté ya disponible y que se limite a las partes necesarias (artículo 6). La información obtenida no se puede usar para otra cosa ni para crear un programa «sustancialmente similar». Y una cláusula de contrato que lo prohíba «se considerará nula» (artículo 8).
  • En Estados Unidos, la ley de derechos de autor (17 U.S.C. §1201(f)) permite saltarse las protecciones técnicas de un programa obtenido legalmente con el único fin de identificar y analizar lo necesario para la interoperabilidad. Fuera de esa excepción, mandan sobre todo las licencias, y muchas prohíben la ingeniería inversa.
  • En la práctica, lee la licencia del programa, no distribuyas el código descompilado de otros y, si es para tu trabajo, consúltalo con quien lleve los temas legales.

Consejos para descompilar con cabeza

  • Busca antes el código: muchos programas tienen el fuente publicado, mapas de fuente o documentación, y eso siempre es mejor que descompilar.
  • Usa la herramienta de la familia correcta: para Java, Android o .NET, las específicas dan resultados casi perfectos; para código máquina, Ghidra, IDA o Binary Ninja.
  • Comprueba las versiones: un descompilador de Python o de Java puede no entender versiones más nuevas que él.
  • Verifica el resultado: si vas a reutilizar o modificar el código, ejecútalo y compáralo con el original, como hicimos con decompyle3.
  • Apóyate en la IA, sin fiarte a ciegas: los asistentes de IA son muy buenos renombrando variables y explicando funciones descompiladas, y con MCP pueden manejar Ghidra o JADX directamente, como cuenta la guía de REA. Pero pueden equivocarse igual que un descompilador.

Si quieres seguir, empieza por la guía de Ghidra y por lo que trae su versión 12.2, la que hemos usado aquí.

Fuentes: repositorios y páginas oficiales de cada herramienta (versiones de la API de GitHub, 22 de octubre de 2026); sobre IDA Free, Hex-Rays; sobre Binary Ninja, sus páginas Free y de compra; la retirada de wasm-decompile, en la PR #2769 de wabt; la Directiva 2009/24/CE (texto en español en WIPO Lex) y el 17 U.S.C. §1201. El experimento se hizo el 22 de octubre de 2026 en contenedores desechables con gcc 14.2.0, clang 19.1.7, Go 1.27.3, Rust 1.99.1, javac 25, .NET SDK 10.0.402 y Python 3.8.20, 3.13.16 y 3.14.8; las sumas de Ghidra, wabt, Vineflower y CFR se comprobaron con las publicadas.