Saltar a contenido

Uniones (Unions) en C++: Reutilización de Memoria

Índice


Introducción

Las uniones (unions) son un tipo especial de estructura en C++ que permite almacenar diferentes tipos de datos en la misma posición de memoria, aunque evidentemente no simultáneamente.

Diferencia clave con estructuras:

  • Estructura: Cada campo tiene su propio espacio de memoria.
  • Unión: Todos los campos comparten el mismo espacio de memoria.

Aplicaciones en reversing:

  • Detectar reutilización de variables para diferentes tipos.
  • Entender acceso a datos de múltiples formas (p.ej., bytes individuales vs entero completo).
  • Reconocer almacenamiento temporal de valores.

Parte 1: Concepto de Unión

Definición

Una unión reserva espacio para el campo más grande, y todos los campos se superponen en la misma dirección de memoria.

Sintaxis:

union nombre_union {
    tipo1 campo1;
    tipo2 campo2;
    tipo3 campo3;
};

Ejemplo Conceptual

union Ejemplo {
    int a;      // 4 bytes
    char b;     // 1 byte
    double c;   // 8 bytes
};

Tamaño de la unión: sizeof(Ejemplo) = 8 (el mayor campo es double).

Representación en memoria:

Offset:   0   1   2   3   4   5   6   7
        +---+---+---+---+---+---+---+---+
   a:   | int (4 bytes) |   (sin usar) |
   b:   |char|         (sin usar)      |
   c:   |      double (8 bytes)        |
        +---+---+---+---+---+---+---+---+

Todos los campos comparten la misma dirección inicial.

Comportamiento

union Ejemplo {
    int a;
    char b;
    double c;
} ejemplo;

ejemplo.a = 100;       // Escribe 100 en los primeros 4 bytes
ejemplo.b = 'A';       // Escribe 0x41 en el PRIMER byte (sobreescribe parte de a)
ejemplo.c = 10.32;     // Escribe 10.32 en los 8 bytes (sobreescribe todo)

Importante: Escribir en un campo modifica los otros campos, porque comparten memoria.


Parte 2: Unión vs Estructura (Memoria)

Comparación de Tamaño

Estructura (campos separados)

struct EstructuraEjemplo {
    int a;      // Offset 0-3 (4 bytes)
    char b;     // Offset 4 (1 byte)
    double c;   // Offset 8-15 (8 bytes, alineado)
};

sizeof(EstructuraEjemplo) = 16 bytes (con padding)

Memoria:

Offset:  0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15
       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
       | int (a)       |chr| padding   | double (c)                    |
       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

Unión (campos superpuestos)

union UnionEjemplo {
    int a;      // Offset 0-3 (4 bytes)
    char b;     // Offset 0 (1 byte)
    double c;   // Offset 0-7 (8 bytes)
};

sizeof(UnionEjemplo) = 8 bytes (solo el mayor)

Memoria:

Offset:  0   1   2   3   4   5   6   7
       +---+---+---+---+---+---+---+---+
   a:  | int           | (sin usar)    |
   b:  |chr| (sin usar)                |
   c:  | double                        |
       +---+---+---+---+---+---+---+---+

Tabla Comparativa

Aspecto Estructura Unión
Tamaño Suma de todos los campos (+ padding) Tamaño del campo más grande
Posición de campos Cada campo en offset diferente Todos en offset 0
Independencia Modificar un campo NO afecta otros Modificar un campo AFECTA otros
Uso Datos permanentes, diferentes campos coexisten Datos temporales, un solo campo activo a la vez

Parte 3: Ejemplo Básico con Unión

Código Completo

#include <iostream>
using namespace std;

union Ejemplo {
    int a;
    char b;
    double c;
} ejemplo;

int main() {
    // Asignar 100 a 'a'
    ejemplo.a = 100;
    cout << "a = " << ejemplo.a << endl;

    // Asignar 'A' a 'b' (modifica 'a')
    ejemplo.b = 'A';
    cout << "b = " << ejemplo.b << endl;

    // Asignar 10.32 a 'c' (modifica todo)
    ejemplo.c = 10.32;
    cout << "c = " << ejemplo.c << endl;

    // Imprimir direcciones (todas iguales)
    cout << "Dirección de a: " << (void*)&ejemplo.a << endl;
    cout << "Dirección de b: " << (void*)&ejemplo.b << endl;
    cout << "Dirección de c: " << (void*)&ejemplo.c << endl;

    // Imprimir tamaños
    cout << "sizeof(ejemplo) = " << sizeof(ejemplo) << endl;
    cout << "sizeof(a) = " << sizeof(ejemplo.a) << endl;
    cout << "sizeof(b) = " << sizeof(ejemplo.b) << endl;
    cout << "sizeof(c) = " << sizeof(ejemplo.c) << endl;

    return 0;
}

Salida Esperada

a = 100
b = A
c = 10.32
Dirección de a: 0x00007FF705050
Dirección de b: 0x00007FF705050
Dirección de c: 0x00007FF705050
sizeof(ejemplo) = 8
sizeof(a) = 4
sizeof(b) = 1
sizeof(c) = 8

Observaciones:

  • Todas las direcciones son iguales (misma posición de memoria).
  • El tamaño de la unión es 8 (el campo double c).
  • Los tamaños individuales de los campos son sus tamaños normales.

Análisis Paso a Paso en Memoria

ejemplo.a = 100;  // 0x00000064

Memoria:

Offset:  0   1   2   3   4   5   6   7
       +---+---+---+---+---+---+---+---+
       |64 |00 |00 |00 |?? |?? |?? |?? |
       +---+---+---+---+---+---+---+---+
ejemplo.b = 'A';  // 0x41

Memoria:

Offset:  0   1   2   3   4   5   6   7
       +---+---+---+---+---+---+---+---+
       |41 |00 |00 |00 |?? |?? |?? |?? |
       +---+---+---+---+---+---+---+---+

Nota: ejemplo.a ahora vale 0x00000041 (65), no 100. El byte bajo fue sobreescrito.

ejemplo.c = 10.32;

Memoria:

Offset:  0   1   2   3   4   5   6   7
       +---+---+---+---+---+---+---+---+
       |A4 |70 |A4 |71 |3D |40 |24 |40 |  (representación IEEE 754)
       +---+---+---+---+---+---+---+---+

Todos los bytes fueron sobreescritos.


Parte 4: Análisis en Ensamblador

Código Original

union Ejemplo {
    int a;
    char b;
    double c;
} ejemplo;

int main() {
    ejemplo.a = 100;
    ejemplo.b = 'A';
    ejemplo.c = 10.32;
    return 0;
}

Ensamblador (x64)

; ejemplo está en sección .data (variable global)
.data
ejemplo dq ?    ; 8 bytes reservados

.text
main proc
    ; ejemplo.a = 100
    mov     dword ptr [ejemplo], 100  ; Escribir 0x64 en los primeros 4 bytes

    ; ejemplo.b = 'A'
    mov     byte ptr [ejemplo], 41h   ; Escribir 0x41 en el PRIMER byte

    ; ejemplo.c = 10.32
    movsd   xmm0, qword ptr [_real_10_32]  ; Cargar 10.32 en registro XMM0
    movsd   qword ptr [ejemplo], xmm0      ; Escribir 10.32 en los 8 bytes

    xor     eax, eax
    ret
main endp

Análisis Detallado

1. Asignación a ejemplo.a = 100

mov     dword ptr [ejemplo], 100
  • dword ptr: El compilador sabe que a es un int (4 bytes).
  • Escribe 0x64 en [ejemplo+0] a [ejemplo+3].

Memoria después:

ejemplo: 64 00 00 00 ?? ?? ?? ??

2. Asignación a ejemplo.b = 'A'

mov     byte ptr [ejemplo], 41h
  • byte ptr: El compilador sabe que b es un char (1 byte).
  • Escribe 0x41 en [ejemplo+0] (sobreescribe el byte bajo de a).

Memoria después:

ejemplo: 41 00 00 00 ?? ?? ?? ??

3. Asignación a ejemplo.c = 10.32

movsd   xmm0, qword ptr [_real_10_32]
movsd   qword ptr [ejemplo], xmm0
  • movsd: Mueve un double (8 bytes) usando registros SSE.
  • Escribe toda la representación de 10.32 en [ejemplo+0] a [ejemplo+7].

Memoria después:

ejemplo: A4 70 A4 71 3D 40 24 40

Reconocimiento en IDA

En IDA, con símbolos:

mov     dword ptr ds:ejemplo, 64h
mov     byte ptr ds:ejemplo, 41h
movsd   qword ptr ds:ejemplo, xmm0

Sin símbolos:

  • Verás múltiples escrituras a la misma dirección con diferentes tamaños.
  • Esto es un indicador fuerte de unión.
mov     dword ptr ds:140005050h, 64h
mov     byte ptr ds:140005050h, 41h
movsd   qword ptr ds:140005050h, xmm0

Clave: Misma dirección base, diferentes tamaños de operando (dword, byte, qword).


Parte 5: Caso Avanzado - Estructura dentro de Unión

Concepto

Una unión puede contener una estructura y un array, permitiendo acceder a los mismos datos de dos formas diferentes.

Código Completo

#include <iostream>
using namespace std;

struct stCoor3D {
    int X, Y, Z;
};

union unCoor3D {
    struct stCoor3D N;  // Estructura con campos X, Y, Z
    int Coor[3];        // Array de 3 enteros
} Punto;

int main() {
    // Asignar usando campos de estructura
    Punto.N.X = 10;
    Punto.N.Y = 20;

    // Asignar usando array
    Punto.Coor[2] = 30;

    // Imprimir usando estructura
    cout << "Punto.N.X = " << Punto.N.X << endl;
    // Imprimir usando array (mismo valor)
    cout << "Punto.Coor[0] = " << Punto.Coor[0] << endl;

    cout << "Punto.N.Y = " << Punto.N.Y << endl;
    cout << "Punto.Coor[1] = " << Punto.Coor[1] << endl;

    cout << "Punto.N.Z = " << Punto.N.Z << endl;
    cout << "Punto.Coor[2] = " << Punto.Coor[2] << endl;

    return 0;
}

Salida

Punto.N.X = 10
Punto.Coor[0] = 10
Punto.N.Y = 20
Punto.Coor[1] = 20
Punto.N.Z = 30
Punto.Coor[2] = 30

Análisis de Memoria

Tamaño de stCoor3D:

sizeof(stCoor3D) = 3 * sizeof(int) = 3 * 4 = 12 bytes

Tamaño de int Coor[3]:

sizeof(Coor) = 3 * sizeof(int) = 3 * 4 = 12 bytes

Tamaño de la unión:

sizeof(unCoor3D) = max(12, 12) = 12 bytes

Disposición en memoria:

Offset:   0   1   2   3   4   5   6   7   8   9  10  11
        +---+---+---+---+---+---+---+---+---+---+---+---+
  N.X:  | int (X)       |
  N.Y:  |               | int (Y)       |
  N.Z:  |               |               | int (Z)       |
        +---+---+---+---+---+---+---+---+---+---+---+---+
Coor[0]:|int (Coor[0])  |
Coor[1]:|               | int (Coor[1]) |
Coor[2]:|               |               | int (Coor[2]) |
        +---+---+---+---+---+---+---+---+---+---+---+---+

Equivalencias:

  • Punto.N.XPunto.Coor[0] (offset 0)
  • Punto.N.YPunto.Coor[1] (offset 4)
  • Punto.N.ZPunto.Coor[2] (offset 8)

Ensamblador Real (MSVC x64)

; Datos en .data
unCoor3D Punto DB 0cH DUP (?)           ; 12 bytes (0x0C) para la unión

main PROC
        sub     rsp, 40                 ; Reservar stack frame (0x28)

; === Asignaciones ===

; Punto.N.X = 10 (acceso directo por offset)
        mov     DWORD PTR unCoor3D Punto, 10

; Punto.N.Y = 20 (acceso directo por offset)
        mov     DWORD PTR unCoor3D Punto+4, 20

; Punto.Coor[2] = 30 (acceso por índice calculado)
        mov     eax, 4
        imul    rax, rax, 2             ; 4 * 2 = 8 (offset para Coor[2])
        lea     rcx, OFFSET FLAT:unCoor3D Punto
        mov     DWORD PTR [rcx+rax], 30 ; Escribir en Punto + 8

; === Imprimir Punto.N.X (acceso por offset directo) ===
        mov     edx, DWORD PTR unCoor3D Punto    ; Cargar Punto.N.X (offset 0)
        mov     rcx, QWORD PTR __imp_std::cout
        call    QWORD PTR __imp_std::ostream::operator<<(int)

        lea     rdx, OFFSET FLAT:std::endl
        mov     rcx, rax
        call    QWORD PTR __imp_std::ostream::operator<<

; === Imprimir Punto.Coor[0] (acceso por índice calculado) ===
        mov     eax, 4
        imul    rax, rax, 0             ; 4 * 0 = 0
        lea     rcx, OFFSET FLAT:unCoor3D Punto
        mov     edx, DWORD PTR [rcx+rax]        ; Cargar Punto.Coor[0]
        mov     rcx, QWORD PTR __imp_std::cout
        call    QWORD PTR __imp_std::ostream::operator<<(int)

        lea     rdx, OFFSET FLAT:std::endl
        mov     rcx, rax
        call    QWORD PTR __imp_std::ostream::operator<<

; === Imprimir Punto.N.Y (acceso por offset directo) ===
        mov     edx, DWORD PTR unCoor3D Punto+4 ; Cargar Punto.N.Y (offset 4)
        mov     rcx, QWORD PTR __imp_std::cout
        call    QWORD PTR __imp_std::ostream::operator<<(int)

        lea     rdx, OFFSET FLAT:std::endl
        mov     rcx, rax
        call    QWORD PTR __imp_std::ostream::operator<<

; === Imprimir Punto.Coor[1] (acceso por índice calculado) ===
        mov     eax, 4
        imul    rax, rax, 1             ; 4 * 1 = 4
        lea     rcx, OFFSET FLAT:unCoor3D Punto
        mov     edx, DWORD PTR [rcx+rax]        ; Cargar Punto.Coor[1]
        mov     rcx, QWORD PTR __imp_std::cout
        call    QWORD PTR __imp_std::ostream::operator<<(int)

        lea     rdx, OFFSET FLAT:std::endl
        mov     rcx, rax
        call    QWORD PTR __imp_std::ostream::operator<<

; === Imprimir Punto.N.Z (acceso por offset directo) ===
        mov     edx, DWORD PTR unCoor3D Punto+8 ; Cargar Punto.N.Z (offset 8)
        mov     rcx, QWORD PTR __imp_std::cout
        call    QWORD PTR __imp_std::ostream::operator<<(int)

        lea     rdx, OFFSET FLAT:std::endl
        mov     rcx, rax
        call    QWORD PTR __imp_std::ostream::operator<<

; === Imprimir Punto.Coor[2] (acceso por índice calculado) ===
        mov     eax, 4
        imul    rax, rax, 2             ; 4 * 2 = 8
        lea     rcx, OFFSET FLAT:unCoor3D Punto
        mov     edx, DWORD PTR [rcx+rax]        ; Cargar Punto.Coor[2]
        mov     rcx, QWORD PTR __imp_std::cout
        call    QWORD PTR __imp_std::ostream::operator<<(int)

        lea     rdx, OFFSET FLAT:std::endl
        mov     rcx, rax
        call    QWORD PTR __imp_std::ostream::operator<<

        xor     eax, eax                ; return 0
        add     rsp, 40                 ; 0x28
        ret     0
main ENDP

Notas sobre el código real:

  • Sin variables temporales: Esta versión sin strings literales es más directa; no guarda punteros base en el stack.
  • Patrón claro de offset vs índice:
  • Acceso a .N.X/Y/Zmov edx, DWORD PTR Punto+0/4/8 (offset fijo)
  • Acceso a .Coor[i]imul rax, 4 * i + lea rcx + mov edx, [rcx+rax] (índice calculado)
  • Stack frame pequeño (0x28 = 40 bytes): Solo shadow space para llamadas, sin locals complejas.

Patrones de Reconocimiento

Acceso como Estructura (Offset Fijo)

mov     edx, [Punto]      ; Punto.N.X
mov     edx, [Punto+4]    ; Punto.N.Y
mov     edx, [Punto+8]    ; Punto.N.Z

Clave: Offsets constantes (+0, +4, +8).

Acceso como Array (Índice Calculado)

imul    eax, 4            ; índice * sizeof(int)
lea     rcx, [Punto]      ; Base del array
mov     edx, [rcx+rax]    ; Carga con índice

Clave: Multiplicación por tamaño del elemento + lea base + acceso indexado.

Optimización del Compilador

En el código original, el compilador puede optimizar las primeras asignaciones:

; Optimización: Escribir X e Y juntos como qword
mov     qword ptr [Punto], 14000000Ah   ; 0x14 = 20, 0x0A = 10

Interpretación:

  • Byte 0-3: 0x0000000A (10)
  • Byte 4-7: 0x00000014 (20)

Esto es equivalente a:

mov     dword ptr [Punto], 10
mov     dword ptr [Punto+4], 20

Parte 6: Uniones Anónimas (Struct inline + Array)

Idea

Puedes declarar una estructura anónima dentro de la unión y un array que se superponen. Como la struct no tiene nombre de tipo ni nombre de objeto, accedes directo a los campos (X, Y, Z) como si fueran variables sueltas.

Código (anónimo)

union Punto3D {
    struct { int X, Y, Z; }; // struct anónima sin nombre de objeto
    int Coor[3];             // array superpuesto
} Punto;

int main() {
    Punto.X = 1; Punto.Y = 2; Punto.Z = 3;
    cout << Punto.X << " " << Punto.Y << " " << Punto.Z << "\n";
    cout << Punto.Coor[0] << " " << Punto.Coor[1] << " " << Punto.Coor[2] << "\n";

    Punto.Coor[0] = 4; Punto.Coor[1] = 5; Punto.Coor[2] = 6;
    cout << Punto.X << " " << Punto.Y << " " << Punto.Z << "\n";
    cout << Punto.Coor[0] << " " << Punto.Coor[1] << " " << Punto.Coor[2] << "\n";
}

Salida:

1 2 3
1 2 3
4 5 6
4 5 6

Claves: - X/Y/Z y Coor[0..2] son el mismo bloque de 12 bytes (offsets 0,4,8). - Si intentas darle nombre al objeto (N) aun sin tipo, ya no es anónima y debes acceder vía Punto.N.X.

Parte 7: Inicialización de Uniones (primer miembro)

Regla

Una unión solo se inicializa por su primer miembro en la declaración. Si pasas un literal que no cabe o es de otro tipo, el compilador lo convertirá al tipo del primer campo (con posibles truncamientos).

Ejemplo (truncamiento)

union EjemploInit {
    int a;      // primer miembro
    char b;
    double c;
};

double valor = 3.1416;
EjemploInit x = { valor }; // Se inicializa `a`, no `c`

Lo que sucede: - valor (double, 8 bytes) se convierte a inta = 3 (trunca la parte decimal). - b y c quedan sin inicializar; si los lees, verás basura. - MSVC emite warning de pérdida de datos si los warnings están activos.

Patrón en ensamblador

Verás una conversión a entero antes de escribir en la unión:

; valor (double) en xmm?
cvttsd2si eax, xmm0    ; convertir double a int (truncar)
mov     DWORD PTR x, eax ; escribir en el primer campo (int)

Parte 8: Discriminadores (uniones etiquetadas)

Problema

En una unión grande (libro/revista/película), sin un campo adicional no sabes qué variante está activa al leer.

Solución: Tagged union

enum Tipo { Libro = 0, Revista = 1, Pelicula = 2 };

struct TipoLibro   { int codigo; char autor[80];   char titulo[80];   char editorial[32]; int anno; };
struct TipoRevista { int codigo; char nombre[32];  int mes; int anno; };
struct TipoPelicula{ int codigo; char titulo[80];  char director[80]; char productora[32]; int anno; };

struct Ejemplar {
    Tipo tipo;           // discriminador (no se solapa con la unión)
    union {
        TipoLibro   l;
        TipoRevista r;
        TipoPelicula p;
    } datos;             // la unión real
};

Ejemplar tabla[100];

tabla[0].tipo = Libro;
tabla[0].datos.l.codigo = 1; // resto de campos libro...

tabla[1].tipo = Revista;
tabla[1].datos.r.codigo = 2; // resto de campos revista...

Patrón de memoria

  • tipo vive fuera de la unión → no se pisa con los campos de datos.
  • El tamaño de Ejemplar = sizeof(discriminador) + sizeof(variant más grande).

Reconocimiento en reversing

  • Verás un campo pequeño (byte/int) cercano al inicio del objeto que no se solapa con la región de datos grande.
  • Accesos condicionados: primero se lee el discriminador, luego se escoge qué offsets usar.
  • La región grande muestra reusos/offsets diferentes pero misma base → típico de unión.

Parte 9: Reconocimiento en Reversing

Indicadores de Unión en Ensamblador

1. Múltiples Escrituras a la Misma Dirección con Diferentes Tamaños

mov     dword ptr [var], 100
mov     byte ptr [var], 41h
movsd   qword ptr [var], xmm0

Conclusión: Probablemente una unión con int, char, y double.

2. Reutilización de Variable con Diferentes Tipos

; Primero se usa como entero
mov     dword ptr [var], 1234h
add     eax, [var]

; Luego se usa como float
movss   xmm0, dword ptr [var]
addss   xmm0, xmm1

Conclusión: Variable reutilizada; puede ser unión o simplemente reuso temporal.

3. Acceso a Mismos Datos de Dos Formas

; Acceso como estructura
mov     eax, [var]        ; Campo X
mov     ebx, [var+4]      ; Campo Y

; Acceso como array
imul    ecx, 4
mov     edx, [var+rcx]    ; Array[índice]

Conclusión: Unión con estructura + array.

Checklist de Reconocimiento

  • ¿Ves escrituras a la misma dirección base con diferentes tamaños (byte, dword, qword)? → Probable unión.
  • ¿Una variable cambia de "tipo" (entero → float → puntero) durante la ejecución? → Posible unión o reuso temporal.
  • ¿Acceso a datos mediante offsets fijos Y cálculos de índice sobre la misma base? → Unión con estructura + array.
  • ¿El tamaño de la "estructura" en IDA es menor de lo esperado si fueran campos independientes? → Probable unión.

Ejemplo de IDA sin Símbolos

Código observado:

mov     dword ptr ds:140005050h, 64h
mov     byte ptr ds:140005050h, 41h
movsd   qword ptr ds:140005050h, xmm0

Interpretación:

  1. Misma dirección base (140005050h).
  2. Tres tamaños diferentes (dword, byte, qword).
  3. Conclusión: Unión con 3 campos.

Reconstrucción en IDA:

union var_140005050 {
    int field_0;
    char field_0_byte;
    double field_0_double;
};

Resumen y Casos de Uso

Tabla Comparativa Final

Aspecto Estructura Unión
Memoria Suma de campos (+ padding) Campo más grande
Independencia Campos independientes Campos superpuestos
Coexistencia Todos los campos válidos simultáneamente Solo un campo válido a la vez
Uso típico Datos permanentes y relacionados Datos temporales, alternativas
Ejemplo struct Persona { char nombre[50]; int edad; } union Dato { int entero; float flotante; }

Casos de Uso de Uniones

1. Acceso a Bytes Individuales de un Entero

union IntBytes {
    int valor;
    unsigned char bytes[4];
};

IntBytes dato;
dato.valor = 0x12345678;

cout << hex << (int)dato.bytes[0] << endl;  // 0x78 (little-endian)
cout << hex << (int)dato.bytes[1] << endl;  // 0x56
cout << hex << (int)dato.bytes[2] << endl;  // 0x34
cout << hex << (int)dato.bytes[3] << endl;  // 0x12

Uso: Manipular bits individuales sin máscaras.

2. Interpretación de Datos Binarios

union Paquete {
    unsigned int raw;
    struct {
        unsigned char tipo;
        unsigned char tamaño;
        unsigned short checksum;
    } campos;
};

Uso: Leer un entero de 4 bytes como campos estructurados.

3. Ahorro de Memoria en Estructuras Grandes

struct Evento {
    int tipo;
    union {
        int dato_entero;
        float dato_float;
        char* dato_string;
    } datos;
};

Uso: Solo un tipo de dato activo a la vez; ahorra memoria.

4. Dos Formas de Acceso (Estructura + Array)

union Coordenadas {
    struct { float x, y, z; };
    float coord[3];
};

Uso: Acceso por nombre (x, y, z) o por índice (coord[i]).

Advertencias al Usar Uniones

1. Comportamiento Indefinido si Lees Campo Incorrecto

union U {
    int i;
    float f;
} u;

u.i = 100;
cout << u.f;  // ⚠️ Comportamiento indefinido

2. Datos Temporales, No Permanentes

Si escribes en un campo, los otros quedan con "basura" o valores sobrescritos.

3. Cuidado con Punteros

union U {
    int* ptr;
    int valor;
};

Si escribes valor, el puntero queda inválido.

Consejo de Narvaja

Cuando veas en reversing que una variable se reutiliza para diferentes tipos, pregúntate: ¿es una unión formal, o el programador está siendo astuto para ahorrar memoria? En ambos casos, entiende que solo un tipo es válido en un momento dado.

Las uniones son menos comunes que las estructuras, pero cuando aparecen, suelen estar relacionadas con protocolos de red, formatos binarios, o optimización de memoria. Detectarlas es clave para entender el flujo de datos.


Ejemplos de Reconocimiento en IDA

Ejemplo 1: Detectar Unión Simple

Código en IDA:

mov     dword ptr [rsp+20h], 100
mov     byte ptr [rsp+20h], 'A'

Conclusión:

  • Misma dirección ([rsp+20h]).
  • Diferentes tamaños (dword, byte).
  • Es una unión con al menos 2 campos.

Ejemplo 2: Detectar Unión con Estructura + Array

Código en IDA:

; Acceso como estructura
mov     eax, [rbx]
mov     ecx, [rbx+4]

; Acceso como array
imul    edx, 4
mov     esi, [rbx+rdx]

Conclusión:

  • Base rbx accedida con offsets fijos (+0, +4).
  • Base rbx accedida con índice calculado (+rdx).
  • Probable unión con estructura + array.

Reconstrucción:

union tipo_rbx {
    struct {
        int campo0;
        int campo1;
        int campo2;
    } s;
    int array[3];
};

Ejemplo 3: Detectar Reutilización Temporal

Código en IDA:

; Fase 1: uso como entero
mov     dword ptr [var], 12345
add     eax, [var]

; Fase 2: uso como float
movss   xmm0, dword ptr [var]

Conclusión:

  • Variable var usada primero como entero, luego como float.
  • No hay "limpieza" entre usos.
  • Probable unión o reuso intencional.

Notas Adicionales

Diferencias con C

En C, las uniones funcionan igual que en C++. No hay diferencias sintácticas.

Uniones Anónimas (C++11)

struct Evento {
    int tipo;
    union {  // Unión anónima
        int dato_entero;
        float dato_float;
    };
};

Evento e;
e.dato_entero = 10;  // Acceso directo, sin nombre de unión

Uniones y Optimizaciones

Con /O2, el compilador puede:

  • Eliminar uniones triviales y usar registros directamente.
  • Combinar escrituras (como vimos con el qword optimizado).

Para aprender, usa /Od sin optimizaciones.