Saltar a contenido

Punteros en C++: Guía Completa de Reversing

Índice

Introducción

Los punteros son uno de los conceptos más importantes (y temidos) en C++. Sin embargo, son fundamentales para entender:

  • Cómo se pasan estructuras grandes a funciones sin copiarlas completamente
  • Cómo funcionan los arrays dinámicos
  • Cómo se manipulan objetos complejos en ensamblador
  • Cómo se realizan operaciones en memoria

Objetivo: Desmitificar los punteros y aprender a reconocerlos en ensamblador.


Parte 1: Concepto Fundamental de Punteros

¿Qué es un Puntero?

Un puntero es una variable que almacena la dirección de memoria de otra variable.

Conceptualmente:

Variable normal:
    Nombre: a
    Valor: 5
    Dirección: 0x1000

Puntero a a:
    Nombre: pa
    Valor: 0x1000  (dirección de a)
    Dirección: 0x2000

En el código:

int a = 5;           // Variable normal
int* pa = &a;        // Puntero a a (almacena dirección de a)

Diferencia: Variable vs Puntero

Característica Variable Normal Puntero
Almacena Valor Dirección de memoria
Tamaño Depende del tipo (4, 8 bytes, etc.) Siempre 8 bytes en x64
Declaración int a; int* pa;
Inicialización a = 5; pa = &a; (operador &)
Acceso al valor a *pa (operador *)

Operadores Clave

Operador & (Dirección de - Address-of)

int a = 5;
int* pa = &a;       // pa almacena la dirección de a

En ensamblador:

lea rax, [rsp+20]   ; Obtener dirección de a
mov [rsp+28], rax   ; Guardar en pa

Nota: lea (Load Effective Address) obtiene la dirección sin desreferenciar.

Operador * (Desreferenciación - Dereference)

int a = 5;
int* pa = &a;
int b = *pa;        // b = 5 (obtener valor de a a través del puntero)

En ensamblador:

mov rax, [rsp+28]   ; RAX = dirección de a (valor de pa)
mov ebx, [rax]      ; EBX = valor en esa dirección (5)

Nota: mov ebx, [rax] desreferencia el puntero.

Operador -> (Flecha - Arrow, para punteros a estructuras)

struct Persona {
    int edad;
};

Persona p;
Persona* pp = &p;

p.edad = 30;        // Acceso directo
pp->edad = 30;      // Acceso a través de puntero (equivalente)

Parte 2: Declaración y Tipado de Punteros

Sintaxis de Declaración

int* p_int;         // Puntero a int
char* p_char;       // Puntero a char
float* p_float;     // Puntero a float
Persona* p_persona; // Puntero a estructura Persona

Nota: El * está asociado al tipo, no a la variable:

// Estos son equivalentes:
int* p1;
int *p1;

// Pero estos NO son equivalentes:
int* p1, p2;        // p1 es puntero, p2 es int (confuso)
int *p1, *p2;       // Ambos son punteros (más claro)

Tipado de Punteros

Un puntero solo puede apuntar a su tipo específico:

int a = 5;
char c = 'A';

int* p_int = &a;     // Correcto
int* p_int = &c;     // Error de compilación
int* p_int = (int*)&c; // Correcto (con cast)

En reversing: Si ves un puntero que apunta a un tipo incorrecto, probablemente hay:

  • Un cast
  • Un error del compilador
  • Código intencional para eludir protecciones

Puntero Nulo

int* p = nullptr;   // Puntero sin dirección asignada
int* p = NULL;      // Equivalente (C++03)
int* p = 0;         // También equivalente

En ensamblador:

mov rax, 0          ; RAX = nullptr
mov [rsp+28], rax   ; p = nullptr

Acceder a un puntero nulo causa crash:

int* p = nullptr;
int x = *p;         // Crash (Segmentation fault)

Parte 3: Punteros en Ensamblador (x64 Windows)

Asignación: Obtener Dirección

int a = 5;
int* pa = &a;

En ensamblador:

lea rax, [rsp+20]   ; LEA = Load Effective Address
                    ; Obtiene la dirección de a sin desreferenciar
mov [rsp+28], rax   ; pa = dirección de a (8 bytes en x64)

Notas clave:

  • lea obtiene la dirección (sin acceder a memoria)
  • mov [dirección], valor almacena el valor en esa dirección

Desreferenciación: Acceder al Valor

int a = 5;
int* pa = &a;
int b = *pa;        // Leer valor de a a través de pa
*pa = 10;           // Escribir 10 en a a través de pa

ACLARACIÓN CRÍTICA: *pa vs pa

Esta es la confusión más común. Veamos qué contiene cada cosa:

Memoria (ejemplo):

Dirección    Contenido              Variable
0x1000       5                      ← a (el valor)
0x2000       0x1000                 ← pa (la DIRECCIÓN de a)
0x3000       5                      ← b (el valor que asignamos)

En el código:

  • pa contiene 0x1000 (la dirección de a)
  • *pa accede a lo que hay EN esa dirección, que es 5 (el valor de a)
  • b = *pa asigna el valor 5 a b, NO la dirección

Comparación:

int b = pa;    // INCORRECTO: b sería 0x1000 (tipo incorrecto)
int b = *pa;   // CORRECTO: b es 5 (el valor)

En ensamblador, verás la diferencia:

; Si fuera int b = pa (INCORRECTO):
mov rax, [rsp+28]           ; RAX = 0x1000 (la dirección, valor de pa)
mov [rsp+24], rax           ; b = 0x1000 (la dirección)
; Problema: estamos asignando dirección a un int

; Correcto: int b = *pa
mov rax, [rsp+28]           ; RAX = 0x1000 (la dirección)
mov edx, [rax]              ; EDX = [0x1000] = 5 (DESREFERENCIA)
mov [rsp+24], edx           ; b = 5 (el valor)

La clave del segundo mov:

En mov edx, [rax], los corchetes [] significan "accede a la dirección en RAX". Esto desreferencia el puntero.

Sin los corchetes sería diferente:

mov edx, rax                ; EDX = 0x1000 (la dirección)
mov edx, [rax]              ; EDX = 5 (el valor en esa dirección)

Resumen con símbolos:

Operación Significado Valor
pa El puntero mismo (contiene dirección) 0x1000
*pa Lo que apunta el puntero 5
&a La dirección de a 0x1000

Por eso *pa es equivalente a &a → ambos acceden al mismo lugar


Lectura:

mov rax, [rsp+28]   ; RAX = dirección de a (valor de pa)
mov ebx, [rax]      ; EBX = valor en [RAX] (5)

Escritura:

mov rax, [rsp+28]   ; RAX = dirección de a
mov dword ptr [rax], 10 ; Escribir 10 en la dirección de a

Operaciones Comunes

Lectura Simple

int a = 5;
int* pa = &a;
int b = *pa;        // b = 5
mov rax, [rsp+28]       ; RAX = pa (dirección de a)
mov ebx, dword ptr [rax]; EBX = *pa (valor 5)
mov [rsp+32], ebx       ; b = 5

Asignación a través de Puntero

int a = 5;
int* pa = &a;
*pa = 10;           // a ahora vale 10
mov rax, [rsp+28]           ; RAX = pa (dirección de a)
mov dword ptr [rax], 10     ; *pa = 10

Cadena de Lectura

int a = 5;
int* pa = &a;
int b = *pa;        // b = 5
int c = b + 1;      // c = 6
mov rax, [rsp+28]           ; RAX = pa
mov ebx, dword ptr [rax]    ; EBX = *pa = 5
mov ecx, ebx                ; ECX = 5
add ecx, 1                  ; ECX = 6
mov [rsp+36], ecx           ; c = 6

Parte 4: Ejemplo del Curso - Análisis Detallado

Código Fuente

#include <iostream>
using namespace std;

int main() {
    int a;              // Variable a (tipo int)
    int* pa = &a;       // Puntero pa que apunta a a

    *pa = 5;            // Asignar 5 a a a través del puntero
    int b = *pa;        // Leer valor de a a través del puntero
    *pa = 10;           // Asignar 10 a a a través del puntero

    cout << "a = " << a << ", b = " << b << endl;
    return 0;
}

Valores finales:

  • a = 10 (fue modificado a través de pa)
  • b = 5 (se asignó antes de que a cambiara a 10)

Ensamblador Anotado (x64 Windows, con símbolos)

00401000: push    rbp
00401001: mov     rbp, rsp
00401004: sub     rsp, 40h        ; Reservar 64 bytes en el stack
00401008: mov     rax, fs:[28h]   ; Security Cookie
0040100F: mov     [rsp+38h], rax  ; Guardar cookie

Stack layout:

RSP+0x00:  Shadow Space (32 bytes)
RSP+0x20:  a (int, 4 bytes)
RSP+0x24:  [padding, 4 bytes]
RSP+0x28:  pa (puntero, 8 bytes)
RSP+0x30:  [padding, 8 bytes]
RSP+0x38:  Security Cookie (8 bytes)
RSP+0x40:  RBP y return address

Asignación: pa = &a

00401018: lea     rax, [rsp+20h]  ; RAX = dirección de a
                                   ; [rsp+20h] es donde está a
                                   ; lea obtiene esta dirección
00401020: mov     [rsp+28h], rax   ; [rsp+28h] = pa
                                   ; pa ahora contiene dirección de a

Resultado:

pa = 0x...RSP+0x20  (dirección de a)
a  = ? (aún no inicializado)

Lectura Simple de pa

00401024: mov     rax, [rsp+28h]  ; RAX = pa (dirección de a)
                                   ; RAX contiene la dirección

Registro RAX después:

RAX = 0x...RSP+0x20 (la dirección de a)

Asignación a través del Puntero: *pa = 5

00401028: mov     dword ptr [rax], 5  ; Escribir 5 en [RAX]
                                       ; [RAX] = [dirección de a] = 5

Resultado en memoria:

[RSP+0x20] = 5  (a ahora vale 5)

Lectura a través del Puntero: *pa

00401030: mov     rax, [rsp+28h]      ; RAX = pa (dirección de a)
00401034: mov     edx, [rax]          ; EDX = *pa = 5 (valor de a)

Asignación a b = *pa

00401036: mov     [rsp+24h], edx      ; [rsp+24h] = b
                                       ; b ahora vale 5

Stack ahora:

[RSP+0x20] = 5      (a = 5)
[RSP+0x24] = 5      (b = 5)
[RSP+0x28] = dirección de a

Segunda Asignación: *pa = 10

0040103A: mov     rax, [rsp+28h]      ; RAX = pa (dirección de a)
00401040: mov     dword ptr [rax], 10 ; Escribir 10 en [RAX]
                                       ; a ahora vale 10

Resultado:

[RSP+0x20] = 10     (a = 10)
[RSP+0x24] = 5      (b = 5, no cambió)
00401044: lea     rcx, std::cout      ; RCX = &cout
00401050: lea     rdx, formato        ; RDX = "a = %d, b = %d"
00401057: call    std::operator<<     ; Imprimir

00401060: mov     ecx, 0              ; return 0
00401065: mov     rax, [rsp+38h]      ; Leer security cookie
0040106C: xor     rax, fs:[28h]       ; Verificar
00401074: call    __security_check_cookie
00401080: add     rsp, 40h            ; Liberar stack
00401087: pop     rbp
00401088: ret

Visualización en Depuración

Paso 1: Después de lea rax, [rsp+20h]

RAX = 0x1000000020  (dirección de a)
[RSP+0x28] = ???    (aún no asignado)

Paso 2: Después de mov [rsp+28h], rax

RAX = 0x1000000020
[RSP+0x28] = 0x1000000020  (pa ahora apunta a a)

Paso 3: Después de mov dword ptr [rax], 5

RAX = 0x1000000020  (dirección de a)
[RSP+0x20] = 5      (a ahora vale 5)

Paso 4: Después de mov edx, [rax]

RAX = 0x1000000020  (dirección de a)
EDX = 5             (se leyó el valor de a)

Paso 5: Después de mov dword ptr [rax], 10

RAX = 0x1000000020
[RSP+0x20] = 10     (a cambiado a 10)
[RSP+0x24] = 5      (b se mantuvo en 5)

Parte 5: Punteros a Estructuras

Declaración

struct Persona {
    char nombre[65];
    char direccion[65];
    int anio_nacimiento;
};

Persona p;
Persona* pp = &p;       // Puntero a estructura

Acceso a Campos

Método 1: Acceso Directo

Persona p;
p.nombre[0] = 'J';
p.anio_nacimiento = 1990;

Método 2: Acceso a través de Puntero

Persona p;
Persona* pp = &p;

pp->nombre[0] = 'J';           // Equivalente a p.nombre[0]
pp->anio_nacimiento = 1990;    // Equivalente a p.anio_nacimiento

En Ensamblador

; pp = &p
lea rax, [rsp+28]           ; RAX = dirección de p
mov [rsp+120], rax          ; pp = RAX

; pp->anio_nacimiento = 1990
mov rax, [rsp+120]          ; RAX = pp (dirección de p)
add rax, 130                ; RAX += offset de anio_nacimiento
mov dword ptr [rax], 1990   ; Escribir 1990

; int edad = pp->anio_nacimiento
mov rax, [rsp+120]          ; RAX = pp
add rax, 130                ; RAX += offset
mov edx, [rax]              ; EDX = *[RAX]
mov [rsp+64], edx           ; edad = EDX

Parte 6: Punteros y Funciones

Pasar Puntero a Función

void modificar(int* ptr) {
    *ptr = 20;              // Modifica el valor al que apunta
}

int main() {
    int a = 10;
    modificar(&a);          // Pasar puntero a a
    cout << a << endl;      // Imprime 20
}

Ventaja: No se copia el valor, solo se pasa la dirección (8 bytes).

En Ensamblador

Llamada a la función

lea rax, [rsp+20]           ; RAX = &a
mov rcx, rax                ; RCX = puntero (primer parámetro)
call modificar              ; Llamar

x64 FastCall: El primer parámetro va en RCX.

Dentro de modificar()

modificar proc near:
    mov rcx, [rbp+16]       ; RCX = puntero pasado
    mov dword ptr [rcx], 20 ; *ptr = 20
    ret

Caso de Uso: Estructuras Grandes

struct DatosGrandes {
    char datos[8000];       // 8 KB
};

void procesar(DatosGrandes* pdatos) {  // Pasar puntero (8 bytes)
    pdatos->datos[0] = 'A';
}

int main() {
    DatosGrandes d;
    procesar(&d);           // Pasar puntero, no copiar 8 KB
}

Sin punteros: Sería necesario copiar 8000 bytes en el stack.
Con punteros: Solo se copia una dirección (8 bytes).


Parte 7: Aritmética de Punteros

El Concepto Fundamental

Cuando incrementamos un puntero, no sumamos 1 byte, sino que sumamos el tamaño del tipo de dato al que apunta:

int vector[10];
int* puntero = vector;      // Apunta al primer elemento

puntero++;                  // A bajo nivel: suma 4 (sizeof(int))
puntero = puntero + 7;      // A bajo nivel: suma 28 (7 * sizeof(int))

Regla de oro: puntero + N equivale a puntero + (N * sizeof(tipo))

Suma y Resta de Punteros

int arr[10];
int* p = arr;   // p apunta al primer elemento

p++;            // p ahora apunta al segundo elemento
p += 5;         // p ahora apunta al sexto elemento
p--;            // p ahora apunta al quinto elemento

En memoria:

arr[0]  en 0x1000
arr[1]  en 0x1004  (0x1000 + 4)
arr[2]  en 0x1008  (0x1000 + 8)
arr[5]  en 0x1014  (0x1000 + 20)

p = 0x1000
p++     => p = 0x1004  (suma el tamaño del tipo: 4 bytes para int)
p += 5  => p = 0x1014  (suma 5 * 4 = 20 bytes)

En ensamblador:

lea rax, arr            ; RAX = dirección de arr[0]
mov rcx, 1
add rax, rcx            ; Esto suma 1 byte, INCORRECTO

; Correcto:
lea rax, arr
mov rcx, 1
imul rcx, 4             ; Multiplicar offset por tamaño de int
add rax, rcx            ; Ahora apunta a arr[1]

; O más directo:
lea rax, arr
add rax, 4              ; Sumar directamente 4 bytes = arr[1]

Diferencia de Punteros

int* p1 = &arr[2];
int* p2 = &arr[5];

int distancia = p2 - p1;    // distancia = 3

En ensamblador:

lea rax, [arr+8]            ; p1 = &arr[2]
lea rbx, [arr+20]           ; p2 = &arr[5]

mov rcx, rbx
sub rcx, rax                ; RCX = p2 - p1 = 12 bytes
sar rcx, 2                  ; Dividir por 4 (tamaño de int)
; RCX = 3 (elementos)

Aritmética de Punteros con Estructuras

Cuando el puntero apunta a una estructura, el incremento suma el tamaño de toda la estructura:

// Estructura de números complejos
struct st_complejo {
    float real;         // 4 bytes
    float imaginario;   // 4 bytes
};                      // Total: 8 bytes

st_complejo complejo[10];           // Array de 10 estructuras (global)
st_complejo* p_complejo = complejo; // Puntero al inicio

p_complejo++;   // A bajo nivel: suma 8 bytes (sizeof(st_complejo))

Layout en memoria:

Dirección       Contenido
0x7FF304        complejo[0].real
0x7FF308        complejo[0].imaginario
0x7FF30C        complejo[1].real         ← p_complejo después de ++
0x7FF310        complejo[1].imaginario
0x7FF314        complejo[2].real
...

En ensamblador:

; p_complejo = complejo
lea rax, complejo           ; RAX = dirección de la estructura
mov [rsp+28], rax           ; Guardar en p_complejo

; p_complejo++ (incrementar por sizeof(st_complejo) = 8)
mov rax, [rsp+28]           ; RAX = p_complejo
add rax, 8                  ; Sumar 8 (tamaño de la estructura)
mov [rsp+28], rax           ; Guardar de vuelta

En IDA Pro:

Cuando tenemos símbolos, IDA nos muestra en Local Types:

st_complejo size=8

Y en el stack frame vemos:

p_complejo: st_complejo*

Notaciones Equivalentes: Punteros y Arrays

En C/C++, estas dos notaciones son equivalentes:

int arr[10];
int* p = arr;

// Estas dos líneas hacen lo mismo:
*(p + 7) = 5;       // Forma con aritmética de punteros
p[7] = 5;           // Forma con notación de array

También funciona al revés:

arr[7] = 5;         // Notación de array
*(arr + 7) = 5;     // Notación de puntero (arr se convierte a puntero)

Importante: La diferencia es que el nombre del array (arr) no puede reasignarse, mientras que un puntero sí:

int* p = arr;
p++;            // ✅ Válido: p ahora apunta a arr[1]

arr++;          // ❌ ERROR: "lvalue required" - arr es constante

Inicialización de Punteros en Posición Arbitraria

Un puntero puede inicializarse en cualquier posición del array:

int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int* p = &arr[5];       // Iniciar en el campo 5

// Ahora los índices son relativos a esa posición:
p[0] = 100;             // Modifica arr[5]
p[2] = 200;             // Modifica arr[7]
p[-2] = 300;            // Modifica arr[3] (índices negativos!)

En ensamblador:

lea rax, [arr+20]       ; RAX = &arr[5] (5 * 4 = 20)
mov [rsp+28], rax       ; p = RAX

; p[-2] = 300 (equivale a arr[3])
mov rax, [rsp+28]       ; RAX = p
sub rax, 8              ; RAX = p - 2 (2 * 4 = 8)
mov dword ptr [rax], 300

Parte 7.5: Punteros Genéricos (void*)

Concepto

Un puntero genérico no tiene un tipo asociado. Puede apuntar a cualquier cosa:

void* b;                // Puntero genérico, sin tipo
char* cadena = "Hola";
b = (void*)cadena;      // Castear para asignar

Limitaciones

No se puede hacer aritmética sin castear:

void* p;
p++;        // ❌ ERROR: ¿Cuánto suma? No hay tipo.
p += 5;     // ❌ ERROR: No se puede calcular el offset.

Razón: El compilador necesita saber el tamaño del tipo para calcular el offset.

Uso Correcto

void* b;
int* enteros = (int*)malloc(10 * sizeof(int));
b = (void*)enteros;         // Guardar como genérico

// Para usar, siempre castear:
int x = *((int*)b);         // Castear a int* y desreferenciar
((int*)b)[5] = 100;         // Castear y usar como array

En ensamblador:

; int x = *((int*)b)
mov rax, [rsp+28]           ; RAX = b (dirección sin tipo)
mov edx, [rax]              ; EDX = contenido como dword (4 bytes)
mov [rsp+30], edx           ; x = EDX

Para reversing: El assembly es igual, pero en el código decompilado verás muchos casts.


Parte 8: Puntero a Puntero (double pointer)

Concepto

int a = 5;
int* pa = &a;       // Puntero a int
int** ppa = &pa;    // Puntero a puntero a int

Estructura de memoria:

a       en 0x1000, valor = 5
pa      en 0x2000, valor = 0x1000 (dirección de a)
ppa     en 0x3000, valor = 0x2000 (dirección de pa)

Desreferenciación

int x = **ppa;      // x = 5
                    // *ppa = pa (dirección de a)
                    // **ppa = *pa = a = 5

En ensamblador:

mov rax, [rsp+40]   ; RAX = ppa (dirección de pa)
mov rax, [rax]      ; RAX = pa (dirección de a)
mov eax, [rax]      ; EAX = a (valor 5)

Asignación

**ppa = 10;         // Asignar 10 a a a través de ppa

En ensamblador:

mov rax, [rsp+40]           ; RAX = ppa
mov rax, [rax]              ; RAX = pa
mov dword ptr [rax], 10     ; *pa = 10

Parte 9: Punteros y Arrays

Array como Puntero

int arr[10] = {1, 2, 3, 4, 5};
int* p = arr;       // p apunta al primer elemento

cout << p[0] << endl;   // 1
cout << p[1] << endl;   // 2
cout << *(p+2) << endl; // 3

En ensamblador:

lea rax, arr            ; RAX = dirección de arr[0]

mov edx, [rax]          ; p[0] = EDX = 1
mov edx, [rax+4]        ; p[1] = EDX = 2
mov edx, [rax+8]        ; p[2] = EDX = 3

Iteración con Punteros

int arr[5] = {10, 20, 30, 40, 50};

for (int* p = arr; p < arr + 5; p++) {
    cout << *p << endl;
}

En ensamblador:

lea rax, arr                ; p = arr
lea rbx, [arr+20]           ; rbx = arr + 5 (5 elementos * 4 bytes)

loop_start:
    cmp rax, rbx            ; if (p < arr+5)
    jge loop_end

    mov edx, [rax]          ; EDX = *p
    ; ... usar EDX ...

    add rax, 4              ; p++ (siguiente elemento)
    jmp loop_start

loop_end:

Parte 9.5: Array Bidimensional vs Array de Punteros

Esta es una diferencia crítica que confunde a muchos:

El Código de Ejemplo

#include <iostream>
using namespace std;

int main() {
    // Array BIDIMENSIONAL (todo el contenido en el stack)
    char Mes[][11] = {"Enero", "Febrero", "Marzo", "Abril",
       "Mayo", "Junio", "Julio", "Agosto",
       "Septiembre", "Octubre", "Noviembre", "Diciembre" };

    // Array de PUNTEROS (solo las direcciones en el stack)
    const char* Mes2[] = { "Enero", "Febrero", "Marzo", "Abril",
       "Mayo", "Junio", "Julio", "Agosto",
       "Septiembre", "Octubre", "Noviembre", "Diciembre" };

    // Acceso a caracteres
    cout << Mes[0][0] << endl;      // 'E' (Enero)
    cout << Mes[0][1] << endl;      // 'n' (Enero)
    cout << Mes[1][0] << endl;      // 'F' (Febrero)
    cout << Mes[1][1] << endl;      // 'e' (Febrero)

    // Array de punteros
    cout << Mes2 << endl;           // Dirección del array
    cout << Mes2[0] << endl;        // "Enero" (imprime el string)
    cout << Mes2[1] << endl;        // "Febrero"

    // Incrementar puntero dentro del array
    Mes2[0]++;                      // Ahora apunta a "nero"
    cout << Mes2[0] << endl;        // "nero"

    // Tamaños
    cout << sizeof(Mes) << endl;    // 12 * 11 = 132
    cout << sizeof(Mes2) << endl;   // 12 * 8 = 96 (punteros de 64 bits)

    return 0;
}

Diferencia Fundamental en Memoria

Array Bidimensional (Mes[][11])

Layout en el STACK:
┌────────────────────────────────────────┐
│ "Enero\0\0\0\0\0\0"     (11 bytes)     │ ← Mes[0]
├────────────────────────────────────────┤
│ "Febrero\0\0\0\0"       (11 bytes)     │ ← Mes[1]
├────────────────────────────────────────┤
│ "Marzo\0\0\0\0\0\0"     (11 bytes)     │ ← Mes[2]
├────────────────────────────────────────┤
│ ...                                    │
├────────────────────────────────────────┤
│ "Diciembre\0\0"         (11 bytes)     │ ← Mes[11]
└────────────────────────────────────────┘
Total: 12 strings × 11 bytes = 132 bytes en stack

Características: - Todo el contenido está contiguo en el stack - Cada string ocupa exactamente 11 bytes (se completa con \0) - El 11 es por el string más largo: "Septiembre" (10 chars + \0) - No se puede incrementar Mes (es una dirección fija)

Array de Punteros (const char* Mes2[])

Layout en el STACK (solo punteros):
┌─────────────────────────────────────────────────┐
│ 0x7FF...54  (puntero a "Enero" en .data)        │ ← Mes2[0]
├─────────────────────────────────────────────────┤
│ 0x7FF...5A  (puntero a "Febrero" en .data)      │ ← Mes2[1]
├─────────────────────────────────────────────────┤
│ 0x7FF...62  (puntero a "Marzo" en .data)        │ ← Mes2[2]
├─────────────────────────────────────────────────┤
│ ...                                             │
└─────────────────────────────────────────────────┘
Total: 12 punteros × 8 bytes = 96 bytes en stack

Los STRINGS están en la sección .data:
.data:
0x7FF...54: "Enero\0"
0x7FF...5A: "Febrero\0"
0x7FF...62: "Marzo\0"
...

Características: - Solo direcciones en el stack (8 bytes cada una en x64) - Los strings reales están en la sección .data - Sí se puede incrementar cada puntero individual - Ocupa menos espacio en stack (96 vs 132 bytes)

Comparación de Tamaños

Tipo Cálculo Tamaño Total
char Mes[][11] 12 strings × 11 chars 132 bytes
const char* Mes2[] 12 punteros × 8 bytes 96 bytes

Acceso a Caracteres Individuales

Array bidimensional:

Mes[0][0]   // 'E' - primer carácter de "Enero"
Mes[0][1]   // 'n' - segundo carácter de "Enero"
Mes[1][0]   // 'F' - primer carácter de "Febrero"

En ensamblador:

; Mes[0][0] - calcular: (0 * 11) + 0 = 0
imul eax, 0Bh           ; EAX = primer_indice × 11
add eax, 0              ; Sumar segundo índice
lea rcx, [rsp+Mes]      ; RCX = dirección base
movzx edx, byte ptr [rcx+rax]  ; EDX = caracter

Array de punteros:

Mes2[0][0]  // 'E' - primer carácter del string apuntado por Mes2[0]
Mes2[0][1]  // 'n'
Mes2[1][0]  // 'F'

En ensamblador:

; Mes2[0][1] - dos desreferencias
imul eax, 8             ; EAX = primer_indice × 8 (tamaño de puntero)
lea rcx, [rsp+Mes2]     ; RCX = dirección base del array de punteros
mov rax, [rcx+rax]      ; RAX = Mes2[0] (el puntero al string)
movzx edx, byte ptr [rax+1]  ; EDX = segundo carácter del string

Incrementar Punteros en el Array

Array bidimensional (ILEGAL):

Mes++;      // ❌ ERROR: Mes es una dirección constante
Mes[0]++;   // ❌ ERROR: Mes[0] tampoco se puede incrementar

Array de punteros (VÁLIDO):

Mes2[0]++;  // ✅ VÁLIDO: Mes2[0] es un puntero modificable
// Antes: Mes2[0] → "Enero"
// Después: Mes2[0] → "nero"

En ensamblador (incremento de Mes2[0]):

; Mes2[0]++
lea rax, [rsp+Mes2]     ; RAX = dirección del array
mov rcx, [rax]          ; RCX = Mes2[0] (puntero actual a "Enero")
inc rcx                 ; RCX++ (ahora apunta a "nero")
mov [rax], rcx          ; Guardar de vuelta en Mes2[0]

Impresión con cout

Imprimir un puntero vs un string:

cout << Mes2;       // Imprime la DIRECCIÓN del array (0x7FF...)
cout << Mes2[0];    // Imprime "Enero" (cout detecta char* y lo imprime como string)

En ensamblador:

; cout << Mes2 (imprimir dirección)
lea rdx, [rsp+Mes2]         ; RDX = dirección del array
call ostream::operator<<    ; Sobrecarga para void*

; cout << Mes2[0] (imprimir string)
mov rdx, [rsp+Mes2]         ; RDX = Mes2[0] (puntero al string)
call ostream::operator<<    ; Sobrecarga para char* (imprime string)

En IDA Pro: Inicialización

Array bidimensional:

IDA muestra la copia de strings al stack con rep movsb o movimientos directos:

; Copiar "Enero" al stack
lea rdi, [rsp+Mes]          ; Destino
lea rsi, aEnero             ; Source: "Enero" en .data
mov ecx, 6                  ; 6 bytes
rep movsb                   ; Copiar

; Rellenar con ceros (padding hasta 11 bytes)
lea rdi, [rsp+Mes+6]        ; Posición después de "Enero"
xor eax, eax
mov ecx, 5
rep stosb                   ; Poner 5 ceros

Array de punteros:

IDA muestra asignación de direcciones:

; Mes2[0] = &"Enero"
lea rax, aEnero             ; RAX = dirección de "Enero" en .data
mov [rsp+Mes2], rax         ; Mes2[0] = RAX

; Mes2[1] = &"Febrero"
lea rax, aFebrero           ; RAX = dirección de "Febrero"
mov [rsp+Mes2+8], rax       ; Mes2[1] = RAX (8 bytes después)

Tip para reversing: Si ves muchos lea + mov asignando direcciones consecutivas (de 8 en 8), es un array de punteros. Si ves rep movsb copiando datos y rep stosb rellenando, es un array bidimensional.

Caso Especial: char[] vs char*

char cadena1[] = "Cadena 1";      // Array: contenido en el stack
const char* cadena2 = "Cadena 2"; // Puntero: string en .data

cadena1++;  // ❌ ERROR: cadena1 es dirección fija
cadena2++;  // ✅ VÁLIDO: cadena2 es un puntero

Después de cadena2++:

cout << cadena1 << endl;    // "Cadena 1"
cout << cadena2 << endl;    // "adena 2" (sin la 'C')

Subíndices Relativos a la Posición Actual

Cuando usamos un puntero con notación de array, los índices son relativos a la posición actual:

const char* p = "Enero";    // p[0] = 'E'

p++;                        // Ahora p apunta a "nero"
// p[0] = 'n'
// p[1] = 'e'
// p[2] = 'r'
// p[3] = 'o'

Contraste con arrays fijos:

char arr[] = "Enero";
// arr[0] = 'E' siempre, porque arr no puede moverse

Parte 10: Reconocimiento de Punteros en IDA Pro (Sin Símbolos)

Patrón 1: LEA + MOV (Asignación de Puntero)

lea rax, [rsp+20]       ; Obtener dirección
mov [rsp+28], rax       ; Guardar en variable (puntero)

Indicador: lea seguido de mov → Probablemente asignación de puntero.

Patrón 2: MOV Doble (Desreferenciación)

mov rax, [rsp+28]       ; Cargar puntero
mov edx, [rax]          ; Desreferenciar puntero

Indicador: Dos mov sucesivos → Probablemente lectura a través de puntero.

Patrón 3: Escritura a través de Puntero

mov rax, [rsp+28]           ; Cargar puntero
mov dword ptr [rax], 10     ; Escribir a través de puntero

Indicador: mov dword ptr [registro], valor → Escritura desreferenciada.

Patrón 4: Puntero a Estructura

lea rax, [rsp+28]           ; RAX = dirección de estructura
mov [rsp+80], rax           ; Guardar puntero

mov rax, [rsp+80]           ; Cargar puntero
mov edx, [rax+82]           ; Leer campo en offset 82

Indicador: Offsets dentro de desreferenciación → Puntero a estructura.

Patrón 5: Llamada a Función con Puntero

lea rax, [rsp+28]           ; RAX = &variable
mov rcx, rax                ; RCX = puntero (primer parámetro)
call sub_401234             ; Pasar puntero a función

Indicador: lea, luego mov rcx, luego call → Pasar puntero como parámetro.


Parte 11: Importancia de Punteros en Reversing

¿Por Qué Son Críticos?

1. Estructuras Grandes

struct MuchisDatos {
    char buffer[8000];
};

void procesar(MuchisDatos* p) {
    p->buffer[0] = 'X';
}

Sin punteros, sería imposible pasar 8000 bytes eficientemente.

2. Modificación In-Place

void incrementar(int* ptr) {
    (*ptr)++;
}

int x = 10;
incrementar(&x);    // x ahora vale 11

El puntero permite modificar la variable original.

3. Arrays Dinámicos

int* arr = new int[1000];
arr[0] = 100;
arr[999] = 200;

Los arrays dinámicos se manejan completamente con punteros.

4. Listas Enlazadas, Árboles, etc.

struct Nodo {
    int dato;
    Nodo* siguiente;  // Puntero al siguiente nodo
};

Las estructuras recursivas requieren punteros.

En Reversing

Cuando ves punteros en ensamblador:

  • Hay transferencia de estructuras entre funciones
  • Hay modificación de datos desde otra función
  • Hay operaciones complejas (búsquedas, iteraciones)
  • El tamaño de datos es significativo

Parte 12: Stack Frame con Punteros

Ejemplo Completo

int main() {
    int a = 100;
    int b = 200;
    int* pa = &a;
    int* pb = &b;

    *pa = 500;
    *pb = 600;
}

Stack Layout

RSP+0x00:  Shadow Space (32 bytes)
RSP+0x20:  a (int, 4 bytes) = 100 → 500
RSP+0x24:  b (int, 4 bytes) = 200 → 600
RSP+0x28:  pa (puntero, 8 bytes) = &a
RSP+0x30:  pb (puntero, 8 bytes) = &b
RSP+0x38:  Security Cookie (8 bytes)

Ensamblador Completo

; a = 100
mov dword ptr [rsp+20], 100

; b = 200
mov dword ptr [rsp+24], 200

; pa = &a
lea rax, [rsp+20]
mov [rsp+28], rax

; pb = &b
lea rax, [rsp+24]
mov [rsp+30], rax

; *pa = 500
mov rax, [rsp+28]           ; RAX = pa
mov dword ptr [rax], 500    ; a ahora vale 500

; *pb = 600
mov rax, [rsp+30]           ; RAX = pb
mov dword ptr [rax], 600    ; b ahora vale 600

Estado Final

[RSP+0x20] = 500  (a)
[RSP+0x24] = 600  (b)

Parte 13: Errores Comunes con Punteros

Error 1: Usar Puntero Nulo

int* p = nullptr;
int x = *p;         // Crash!

En reversing: Si ves mov eax, [0] o similar → Probablemente crash intencional o bug.

Error 2: Puntero a Variable Local

int* puntero_global;

void funcion() {
    int a = 5;
    puntero_global = &a;    // ¡Peligro! a se destruye al salir
}

int main() {
    funcion();
    int x = *puntero_global;  // Crash o valor basura
}

En reversing: Punteros que apuntan a stack de funciones ya terminadas → Bug.

Error 3: Confundir Dirección con Valor

int a = 100;
int* p = &a;
cout << p << endl;      // Imprime dirección (0x1000)
cout << *p << endl;     // Imprime valor (100)

Error 4: Olvidar Desreferenciar

int a = 100;
int* p = &a;

int b = p;      // Asigna dirección a int (error de tipos)
int c = *p;     // Correcto: asigna valor

Checklist: Reconocer Punteros en Ensamblador

Cuando revises una función desconocida:

  • ¿Hay lea seguido de mov en el stack? → Puntero asignado
  • ¿Hay mov doble sobre el mismo registro? → Posible desreferenciación
  • ¿Hay mov dword ptr [registro], valor? → Escritura desreferenciada
  • ¿Hay offsets dentro de desreferenciación? → Puntero a estructura
  • ¿Se pasan registros a funciones con lea antes? → Puntero como parámetro
  • ¿Hay operaciones de suma en punteros? → Aritmética de punteros (arrays)

Parte 14: Comparación Directa con Ensamblador del Video

Código Original (del video)

int main() {
    int a;
    int* pa = &a;
    *pa = 5;
    int b = *pa;
    *pa = 10;
    return 0;
}

Ensamblador Esperado (sin optimizaciones)

00401000: push    rbp
00401001: mov     rbp, rsp
00401004: sub     rsp, 40h

; pa = &a
00401008: lea     rax, [rsp+20h]      ; Obtener dirección de a
00401010: mov     [rsp+28h], rax      ; pa = dirección

; *pa = 5
00401018: mov     rax, [rsp+28h]      ; Cargar pa
0040101C: mov     dword ptr [rax], 5  ; Escribir 5

; b = *pa
00401024: mov     rax, [rsp+28h]      ; Cargar pa
00401028: mov     edx, [rax]          ; Leer valor (5)
0040102C: mov     [rsp+24h], edx      ; b = 5

; *pa = 10
00401030: mov     rax, [rsp+28h]      ; Cargar pa
00401034: mov     dword ptr [rax], 10 ; Escribir 10

; Epilogo
00401038: xor     eax, eax            ; return 0
0040103A: add     rsp, 40h
00401041: pop     rbp
00401042: ret

Análisis Paso a Paso en Depuración

Instrucción RAX [RSP+20] (a) [RSP+28] (pa) [RSP+24] (b)
Inicial - ? - -
lea rax, [rsp+20] 0x...0x20 ? - -
mov [rsp+28], rax 0x...0x20 ? 0x...0x20 -
mov rax, [rsp+28] 0x...0x20 ? 0x...0x20 -
mov [rax], 5 0x...0x20 5 0x...0x20 -
mov rax, [rsp+28] 0x...0x20 5 0x...0x20 -
mov edx, [rax] 0x...0x20 5 0x...0x20 -
mov [rsp+24], edx 0x...0x20 5 0x...0x20 5
mov rax, [rsp+28] 0x...0x20 5 0x...0x20 5
mov [rax], 10 0x...0x20 10 0x...0x20 5

Resumen: Lo Más Importante

Conceptos Clave

  1. Puntero = Dirección de memoria
  2. Se obtiene con &variable
  3. Se desreferencia con *puntero

  4. En ensamblador:

  5. lea obtiene dirección (sin desreferenciar)
  6. mov carga/almacena direcciones o valores
  7. [registro] desreferencia el puntero

  8. Aritmética de punteros:

  9. puntero++ suma el tamaño del tipo (no 1 byte)
  10. puntero + N suma N * sizeof(tipo)
  11. Puntero a int: incrementa de 4 en 4
  12. Puntero a struct de 8 bytes: incrementa de 8 en 8

  13. Notaciones equivalentes:

  14. *(p + N) es igual a p[N]
  15. Pero arr++ es ilegal (array fijo), mientras que p++ es válido

  16. Array bidimensional vs Array de punteros:

Característica char arr[][11] char* arr[]
Contenido en stack Todos los caracteres Solo punteros (8 bytes c/u)
Strings en Stack (contiguos) Sección .data
Tamaño filas × columnas filas × 8 bytes
Incrementar elemento ❌ Ilegal ✅ Válido
Ejemplo 12 × 11 = 132 bytes 12 × 8 = 96 bytes
  1. Patrones a reconocer:
  2. lea + mov = asignación de puntero
  3. mov + mov = desreferenciación
  4. mov dword ptr [reg], val = escritura desreferenciada
  5. imul eax, 0Bh + add = acceso a array bidimensional (11 = 0Bh)
  6. imul eax, 8 + doble desreferencia = array de punteros

Para Reversing

  • Los punteros son fundamentales para pasar datos entre funciones
  • Sin punteros, es imposible trabajar con estructuras grandes
  • La aritmética de punteros implica multiplicación por el tamaño del tipo
  • Los punteros dobles (**p) son raros pero aparecen en callbacks y manipulación dinámica
  • Array bidimensional: Ves rep movsb y rep stosb para inicializar en stack
  • Array de punteros: Ves múltiples lea + mov asignando direcciones

Consejo Final de Narvaja

El reversing es un proceso iterativo. Muchas veces uno se confunde y después lo arregla. No se asusten si algo no sale de movida. El trabajo real es equivocarse, corregir, y poco a poco llegar al resultado correcto. Nadie lo hace perfecto la primera vez.

Parte 15: Punteros en el Heap

Qué es el Heap (objetos dinámicos)

  • Región de memoria dinámica del proceso (ni stack ni .data/.bss).
  • Las direcciones cambian en cada ejecución y el tamaño se decide en runtime.
  • Se usa para objetos que no conocemos de antemano o que pueden ser grandes.

New vs malloc (diferencias clave)

Aspecto new malloc
Tipo devuelto Devuelve puntero tipado (no requiere cast) void* (requiere cast)
Tamaño Lo calcula el compilador (new T) Debes pasar sizeof(T)
En caso de error Lanza excepción std::bad_alloc (por defecto) Devuelve NULL
Inicialización Llama constructor (si lo hay) No inicializa, solo reserva bytes
Liberación delete / delete[] free

Código del ejercicio (heap alocado dinámicamente)

#include <iostream>
using namespace std;

struct stComplejo {
    int real, imaginario;
};

void imprimir(stComplejo*, stComplejo*);

int main() {
    stComplejo* Complejo = new stComplejo;                    // Reserva en heap (tipado)
    stComplejo* Complejo2 = (stComplejo*) malloc(sizeof(stComplejo)); // Reserva en heap (void*, requiere cast)

    if (Complejo2 == NULL) { exit(1); }                       // malloc puede devolver NULL

    Complejo->real = 1;       Complejo->imaginario = 2;
    Complejo2->real = 3;      Complejo2->imaginario = 4;

    imprimir(Complejo, Complejo2);                            // Pasamos direcciones (punteros)

    free(Complejo2);                                          // Liberar malloc
    delete Complejo;                                          // Liberar new
}

void imprimir(stComplejo* _Complejo, stComplejo* _Complejo2) {
    _Complejo->real = 1;
    _Complejo->imaginario = 2;
    cout << "Complejo: "  << _Complejo->real  << " + " << _Complejo->imaginario  << "i" << endl;
    cout << "Complejo2: " << _Complejo2->real << " + " << _Complejo2->imaginario << "i" << endl;
}

Qué mirar en ensamblador

  • Llamadas a heap: call operator new o call malloc → puntero devuelto en RAX.
  • Chequear NULL (solo para malloc): comparación del puntero con cero y salto a exit.
  • Asignaciones a través de punteros: mov dword ptr [rax], <valor> donde RAX es el puntero devuelto.
  • Paso por puntero a función: argumentos en RCX / RDX (Win64); solo viajan 8 bytes por puntero, no toda la estructura.
  • Liberación: call free y call operator delete cerca del final.

Ventajas de usar punteros en el heap

  • Tamaño decidido en runtime (puede depender de entrada del usuario).
  • Evitas desbordar el stack con estructuras grandes.
  • Puedes compartir el mismo objeto entre funciones pasando solo la dirección.

Checklist rápida

  • ¿Ves operator new o malloc? → Objeto en heap.
  • ¿Ves free/delete emparejados? → Buen manejo de vida útil.
  • ¿Hay cast (tipo*) malloc(...)? → C indica malloc devolvió void*.
  • ¿Sin cast con new? → C++ lo tipa solo.

Análisis del ensamblador de main() (heap dinámico)

; Prototipo: int __fastcall main(int argc, const char **argv, const char **envp)
sub     rsp, 58h                 ; Reserva stack (locals: punteros/estado)

mov     ecx, 8                   ; size = sizeof(stComplejo)
call    operator new(unsigned __int64)
mov     [rsp+Complejo], rax      ; Complejo = puntero devuelto por new

mov     ecx, 8                   ; size = sizeof(stComplejo)
call    __imp_malloc             ; malloc(8)
mov     [rsp+Complejo2], rax     ; Complejo2 = puntero devuelto por malloc

cmp     [rsp+Complejo2], 0       ; ¿malloc devolvió NULL?
jnz     short ok_malloc
mov     ecx, 1
call    __imp_exit               ; exit(1) si falla malloc

ok_malloc:
mov     rax, [rsp+Complejo]
mov     dword ptr [rax], 1       ; Complejo->real = 1
mov     rax, [rsp+Complejo]
mov     dword ptr [rax+4], 2     ; Complejo->imaginario = 2

mov     rax, [rsp+Complejo2]
mov     dword ptr [rax], 3       ; Complejo2->real = 3
mov     rax, [rsp+Complejo2]
mov     dword ptr [rax+4], 4     ; Complejo2->imaginario = 4

mov     rdx, [rsp+Complejo2]     ; 2º parámetro (Win64): _Complejo2
mov     rcx, [rsp+Complejo]      ; 1º parámetro: _Complejo
call    imprimir(stComplejo *, stComplejo *)

mov     rcx, [rsp+Complejo2]
call    __imp_free               ; free(Complejo2)

mov     rax, [rsp+Complejo]
mov     [rsp+block], rax         ; block = Complejo
mov     edx, 8                   ; tamaño para delete
mov     rcx, [rsp+block]
call    operator delete(void *, unsigned __int64) ; delete Complejo, size-aware

cmp     [rsp+block], 0
jnz     short set_status         ; si había puntero, marca estado
mov     [rsp+var_18], 0
jmp     short end_status

set_status:
mov     [rsp+Complejo], 8123h    ; patrón/estado (diagnóstico)
mov     rax, [rsp+Complejo]
mov     [rsp+var_18], rax        ; guarda 0x8123 como estado

end_status:
xor     eax, eax                 ; return 0
add     rsp, 58h
retn

Puntos clave de reconocimiento: - operator new y malloc para reservar en heap; puntero en RAX. - Chequeo de NULL y exit(1) solo para malloc. - Escrituras a través de puntero ([rax], [rax+4]) para campos real e imaginario. - Paso de parámetros en Win64: RCX primer puntero, RDX segundo puntero. - Liberación emparejada: free para malloc, operator delete para new. - Bloque de “estado” (8123h) tras delete: relleno/diagnóstico del compilador; no afecta la lógica.

Función imprimir() (parámetros, escrituras y cout)

; Firma: void __fastcall imprimir(stComplejo* _Complejo, stComplejo* _Complejo2)
; Prologue: guarda parámetros en stack y reserva espacio
mov     [rsp+_Complejo2], rdx
mov     [rsp+_Complejo], rcx
sub     rsp, 68h

; Escrituras: _Complejo->real = 1; _Complejo->imaginario = 2
mov     rax, [rsp+68h+_Complejo]
mov     dword ptr [rax], 1
mov     rax, [rsp+68h+_Complejo]
mov     dword ptr [rax+4], 2

; cout << "Complejo: "
lea     rdx, _Val                         ; puntero a literal
mov     rcx, cs:std::cout                 ; ostream
call    std::operator<<<...>(std::ostream&, char const*)
mov     [rsp+68h+var_38], rax             ; guardar ostream

; cout << _Complejo->real
mov     rax, [rsp+68h+_Complejo]
mov     eax, [rax]                        ; cargar real
mov     [rsp+68h+var_48], eax
mov     edx, [rsp+68h+var_48]
mov     rcx, [rsp+68h+var_38]
call    std::ostream::operator<<(int)

; cout << " + "
lea     rdx, asc_1400032C4
mov     rcx, rax
call    std::operator<<<...>(std::ostream&, char const*)
mov     [rsp+68h+var_30], rax

; cout << _Complejo->imaginario
mov     rax, [rsp+68h+_Complejo]
mov     eax, [rax+4]
mov     [rsp+68h+var_44], eax
mov     edx, [rsp+68h+var_44]
mov     rcx, [rsp+68h+var_30]
call    std::ostream::operator<<(int)

; cout << "i" << std::endl
lea     rdx, aI_0
mov     rcx, rax
call    std::operator<<<...>(std::ostream&, char const*)
mov     [rsp+68h+var_28], rax
lea     rdx, std::endl<char,std::char_traits<char>>(std::ostream &)
mov     rcx, [rsp+68h+var_28]
call    std::ostream::operator<<(std::ostream& (*)(std::ostream&))

; Repite patrón para _Complejo2
lea     rdx, aComplejo2
mov     rcx, cs:std::cout
call    std::operator<<<...>(std::ostream&, char const*)
mov     [rsp+68h+var_20], rax
mov     rax, [rsp+68h+_Complejo2]
mov     eax, [rax]
mov     [rsp+68h+var_40], eax
mov     edx, [rsp+68h+var_40]
mov     rcx, [rsp+68h+var_20]
call    std::ostream::operator<<(int)
lea     rdx, asc_1400032D8
mov     rcx, rax
call    std::operator<<<...>(std::ostream&, char const*)
mov     [rsp+68h+var_18], rax
mov     rax, [rsp+68h+_Complejo2]
mov     eax, [rax+4]
mov     [rsp+68h+var_3C], eax
mov     edx, [rsp+68h+var_3C]
mov     rcx, [rsp+68h+var_18]
call    std::ostream::operator<<(int)
lea     rdx, aI
mov     rcx, rax
call    std::operator<<<...>(std::ostream&, char const*)
mov     [rsp+68h+var_10], rax
lea     rdx, std::endl<char,std::char_traits<char>>(std::ostream &)
mov     rcx, [rsp+68h+var_10]
call    std::ostream::operator<<(std::ostream& (*)(std::ostream&))

add     rsp, 68h
retn

Claves:

  • Prologue guarda RCX/RDX en el stack y reserva 0x68 bytes (temporales para ostream y valores).
  • Escrituras a _Complejo: [rax] y [rax+4] son real e imaginario (struct int,int).
  • Secuencia típica de cout: literal → entero → literal → entero → literal → endl.
  • Para _Complejo2, se repite el mismo patrón con RDX como base.

Notas prácticas en IDA (símbolos, optimizaciones y tecla T)

  • Sin optimizaciones: mantenemos /Od y símbolos para que el código se parezca al C++. Con /O2, el compilador puede inlinear imprimir() dentro de main() y mover variables a registros (ni siquiera verás el stack slot temporal). Para enseñar es mejor empezar sin optimizaciones; luego podrás comparar y ver cómo desaparecen funciones o se convierten en registros.
  • new vs malloc en binario: new ya incluye chequeo/reintento (llama al new_handler o lanza std::bad_alloc), por eso no ves if (ptr==0) tras new. Con malloc el chequeo es manual (aquí el if (Complejo2 == NULL) exit(1);).
  • Temporales no optimizados: notarás variables intermedias inútiles (guardar en [rsp+var] y reescribir). Son artefactos de código no optimizado; en build optimizado desaparecerán.
  • Tecla T en IDA: selecciona un puntero y pulsa T para aplicar el tipo de estructura; así los accesos [rax]/[rax+4] muestran real/imaginario en vez de offsets crudos.
  • Distinguir instancias: cuando dos punteros apuntan al mismo tipo (Complejo y Complejo2), duplica el tipo (Copy struct type) y renombra las instancias. Luego, con T, eliges Complejo o Complejo2 y ves los campos correctos en cada uso, evitando confundir qué objeto está tocando cada mov.
  • Detectar estructuras sin símbolos: si ves un puntero y accesos con desplazamientos constantes ([reg], [reg+4], etc.), asume estructura/objeto. Aunque no veas el malloc/new cercano, el patrón “puntero + offset = campo” es la pista clave.

Apéndice A: Smart Pointers (Punteros Inteligentes)

Introducción a Smart Pointers

Los smart pointers son objetos que actúan como punteros pero manejan automáticamente la memoria. En C++11 y posteriores, encontrarás tres tipos principales en <memory>:

  1. unique_ptr - Propiedad exclusiva
  2. shared_ptr - Propiedad compartida
  3. weak_ptr - Referencia débil a shared_ptr

¿Por Qué Son Importantes en Reversing?

En binarios compilados con C++11+, verás:

  • Más llamadas a funciones de destrucción automática
  • Contadores de referencias (refcount)
  • Deletores personalizados
  • Patrones de inicialización más complejos

unique_ptr: Propiedad Única

#include <memory>
using namespace std;

int main() {
    unique_ptr<int> ptr(new int(42));
    cout << *ptr << endl;           // 42
    // ptr se destruye automáticamente al salir del scope
    return 0;
}

Características:

  • Solo un propietario
  • No se puede copiar (se mueve)
  • Automáticamente destruido al salir del scope
  • Sin overhead de conteo de referencias

En ensamblador:

; Creación: unique_ptr<int> ptr(new int(42));
mov ecx, 4              ; size = 4
call operator new       ; RAX = puntero a memoria
mov edx, 42
mov [rax], edx          ; Escribir valor
mov [rsp+28], rax       ; ptr = RAX (almacenar puntero)

; Destrucción automática (cuando sale del scope):
mov rcx, [rsp+28]       ; RCX = ptr
cmp rcx, 0
je skip_delete
call operator delete    ; Liberar memoria
skip_delete:

Indicadores en IDA:

  • Llamadas a operator new y operator delete
  • Destructor registrado (probablemente en tabla de funciones)
  • Un único puntero de propiedad

shared_ptr: Propiedad Compartida

int main() {
    shared_ptr<int> ptr1(new int(100));
    shared_ptr<int> ptr2 = ptr1;     // Compartir propiedad

    cout << ptr1.use_count() << endl; // 2

    // Ambos se destruyen automáticamente
    // Memoria liberada cuando último shared_ptr se destruye
    return 0;
}

Características:

  • Múltiples propietarios
  • Conteo de referencias interno
  • Memoria liberada cuando refcount llega a cero
  • Overhead de sincronización en ambientes multi-thread

Estructura interna:

template<typename T>
class shared_ptr {
    T* ptr;                    // Puntero a datos
    control_block* control;    // Bloque de control (refcount, deletor)
};

struct control_block {
    long refcount;             // Contadores de referencias
    T* ptr;                    // Puntero a objeto
    deletor del;               // Función de destrucción personalizada
};

En ensamblador:

; Creación: shared_ptr<int> ptr1(new int(100));
mov ecx, 4
call operator new           ; RAX = puntero a datos (0x1000)

; Crear control_block
mov ecx, size_control_block
call operator new           ; RBX = puntero a control_block (0x2000)

mov [rbx], 1                ; control_block.refcount = 1
mov [rbx+8], rax            ; control_block.ptr = datos
mov [rbx+16], del_func      ; control_block.deletor

; Almacenar en shared_ptr
mov [rsp+28], rax           ; shared_ptr.ptr = 0x1000
mov [rsp+36], rbx           ; shared_ptr.control = 0x2000

; Asignación: shared_ptr<int> ptr2 = ptr1;
mov rax, [rsp+28]           ; ptr2.ptr = ptr1.ptr
mov rbx, [rsp+36]           ; ptr2.control = ptr1.control
mov [rsp+44], rax           ; Guardar ptr2.ptr
mov [rsp+52], rbx           ; Guardar ptr2.control

; Incrementar refcount
mov rax, [rbx]              ; RAX = refcount
inc rax
mov [rbx], rax              ; refcount = 2

; Destrucción: cuando ptr1 sale del scope
mov rbx, [rsp+36]           ; RBX = control_block
mov rax, [rbx]              ; RAX = refcount
dec rax
mov [rbx], rax              ; refcount--

cmp rax, 0
jne skip_destroy            ; Si refcount > 0, no destruir
mov rcx, [rbx+8]            ; RCX = ptr a datos
mov rax, [rbx+16]           ; RAX = deletor
call rax                    ; Llamar deletor
mov rcx, rbx                ; RCX = control_block
call operator delete        ; Liberar control_block
skip_destroy:

Indicadores en IDA:

  • Dos asignaciones de puntero (ptr + control_block)
  • Incremento de contador (inc [memoria])
  • Decremento de contador (dec [memoria])
  • Saltos condicionales basados en refcount
  • Múltiples llamadas a operator delete

weak_ptr: Referencia Débil

int main() {
    shared_ptr<int> sptr(new int(50));
    weak_ptr<int> wptr = sptr;       // Referencia débil

    // wptr NO incrementa el refcount
    cout << sptr.use_count() << endl; // 1 (no contado)

    // Para usar weak_ptr, convertir a shared_ptr:
    if (auto locked = wptr.lock()) {
        cout << *locked << endl;      // 50
    }
    return 0;
}

Uso común: Evitar ciclos de referencias que prevengan destrucción automática.

En ensamblador:

; weak_ptr = shared_ptr (NO incrementa refcount)
mov rax, [rsp+28]           ; RAX = ptr1.ptr
mov rbx, [rsp+36]           ; RBX = ptr1.control
mov [rsp+44], rax           ; wptr.ptr = ptr1.ptr
mov [rsp+52], rbx           ; wptr.control = ptr1.control
; Nota: NO hay incremento de refcount

; weak_ptr::lock() - convertir a shared_ptr
mov rbx, [rsp+52]           ; RBX = control_block
mov rax, [rbx]              ; RAX = refcount
test rax, rax
jz lock_failed              ; Si refcount == 0, objeto ya destruido

; Incrementar refcount (ahora es un shared_ptr)
inc [rbx]

mov rax, [rsp+44]           ; RAX = ptr a datos
; ... resultado: shared_ptr válido

jmp lock_success
lock_failed:
; ... resultado: nullptr
lock_success:

Comparación: Raw Pointer vs Smart Pointers

Operación Raw int* unique_ptr<int> shared_ptr<int>
Creación int* p = new int(5); unique_ptr<int> p(new int(5)); shared_ptr<int> p(new int(5));
Copia int* q = p; (comparte propiedad) ❌ Compilación falla shared_ptr<int> q = p; (refcount++)
Movimiento N/A unique_ptr<int> q = move(p); N/A
Destrucción Manual delete p; Automática (scope) Automática (refcount == 0)
Bytes en stack 8 bytes (x64) 8 bytes (x64) 16 bytes (ptr + control)
Overhead Ninguno Ninguno 1 long (refcount)

Reconocimiento en IDA Pro

Patrones de unique_ptr:

; Creación
call operator new
mov [rsp+28], rax           ; Guardar único puntero

; Destrucción (al salir)
mov rcx, [rsp+28]
call operator delete

Indicador: Un solo puntero, sin operaciones de refcount.

Patrones de shared_ptr:

; Creación (dos asignaciones)
call operator new           ; RAX = datos
call operator new           ; RBX = control_block
mov [rsp+28], rax           ; ptr a datos
mov [rsp+36], rbx           ; ptr a control

; Incremento de refcount (para copias)
mov rax, [rsp+36]           ; RAX = control_block
inc [rax]                   ; Incrementar refcount

; Decremento y verificación (destrucción)
dec [rax]
cmp [rax], 0
jne skip_delete

Indicador: Dos punteros, operaciones inc/dec en memoria, saltos condicionales.

Stack Frame: Ejemplo Completo con shared_ptr

void procesar_datos(shared_ptr<vector<int>> datos) {
    datos->push_back(100);
}

int main() {
    shared_ptr<vector<int>> vec(new vector<int>());
    procesar_datos(vec);
    return 0;
}

Stack layout en main():

RSP+0x00:  Shadow Space (32 bytes)
RSP+0x20:  vec.ptr (8 bytes)
RSP+0x28:  vec.control (8 bytes)
RSP+0x30:  Padding
RSP+0x38:  Security Cookie

En procesar_datos():

RCX:  datos.ptr (parámetro 1)
RDX:  datos.control (parámetro 2)

En ensamblador:

; Paso de shared_ptr a función
lea rax, [rsp+20]           ; RAX = &vec.ptr
mov rcx, [rax]              ; RCX = vec.ptr (parámetro 1)
lea rbx, [rsp+28]           ; RBX = &vec.control
mov rdx, [rbx]              ; RDX = vec.control (parámetro 2)
call procesar_datos         ; Llamar función

; Dentro de procesar_datos:
procesar_datos proc near:
    ; RCX = datos.ptr
    ; RDX = datos.control

    ; Incrementar refcount (copia para parámetro)
    inc [rdx]

    ; ... usar datos ...

    ; Decrementar refcount (al salir)
    dec [rdx]
    cmp [rdx], 0
    jne ret_func

    ; Liberar memoria
    call delete_vector
    call operator delete    ; Liberar control_block
ret_func:
    ret

Impacto en Reversing

Cuando veas smart pointers:

  • El código es probablemente C++11 o posterior
  • Hay automatización de memoria (más seguro)
  • Las operaciones de refcount aparecen como inc [memoria] / dec [memoria]
  • Destructores pueden ser complejos (deletores personalizados)
  • Es más difícil de debuggear (abstracciones adicionales)

Ventaja de reversing: Si ves refcount == 0 → memoria se libera automáticamente → potencial fuga de información u use-after-free.

Vulnerabilidades Comunes

1. Use-After-Free:

shared_ptr<int> ptr1(new int(100));
weak_ptr<int> wptr = ptr1;
ptr1.reset();  // Libera la memoria

auto locked = wptr.lock();
if (locked) {
    *locked = 200;  // ❌ Use-after-free!
}

2. Ciclo de Referencias:

struct Nodo {
    shared_ptr<Nodo> siguiente;
    shared_ptr<Nodo> anterior;
};

Nodo n1, n2;
n1.siguiente = &n2;
n2.anterior = &n1;
// ❌ Ambos nunca se destruyen (refcount nunca llega a cero)

Solución: Usar weak_ptr para referencias hacia atrás.