Saltar a contenido

El preprocesador en C++: Guía de Reversing

Índice


Introducción

El preprocesador se ejecuta antes de la compilación: elimina comentarios, expande macros, evalúa directivas condicionales y genera el código que verá el compilador. Para reversing, entender el preprocesador ayuda a explicar por qué cierto código no aparece en el binario (condicionales eliminados, macros inline, padding distinto, etc.).


Parte 1: ¿Qué hace el preprocesador?

  • Sustituye macros (#define).
  • Evalúa condicionales (#if, #ifdef, #ifndef, #elif, #else, #endif).
  • Inserta ficheros con #include.
  • Procesa directivas específicas del compilador (#pragma, advertencias, errores).

Parte 2: #define y macros con argumentos

#define TABLE_SIZE 100          // Macro simple: reemplaza símbolo por literal
#define getmax(x,y) ((x)>(y)?(x):(y))  // Macro con argumentos

Claves: - Usa paréntesis en argumentos y en toda la expresión para evitar precedencia inesperada. - Las macros con argumentos no son funciones: el preprocesador hace sustitución textual. - Cuidado con efectos secundarios: getmax(i++, j++) incrementaría dos veces a un operando si la condición se evalúa varias veces.


Parte 3: #undef

#undef TABLE_SIZE
- Elimina una definición previa. Útil para redefinir o limpiar macros heredadas de otros headers.


Parte 4: Condicionales de preprocesador

Guardas típicas:

#ifndef TABLE_SIZE
#define TABLE_SIZE 100
#endif

Condicional con expresión constante:

#define VALUE 1

#if VALUE == 1
    // Se compila solo si VALUE es 1
#elif VALUE == 2
    // Otra rama posible
#else
    // Rama por defecto
#endif

Puntos clave: - Las expresiones se evalúan en el preprocesador; deben ser conocidas en compilación. - Solo se mantiene la primera rama verdadera; el resto se descarta y no aparece en el binario. - #ifdef y #ifndef comprueban existencia de una macro, no su valor.


Parte 5: #include

  • #include <...> busca en rutas de sistema (headers de la plataforma).
  • #include "..." busca primero en el directorio del archivo actual, luego en rutas de sistema.
  • El preprocesador pega el contenido del header en el punto de inclusión.

Parte 6: pragma pack y atributos

Control de alineamiento y padding:

#pragma pack(push, 1)
struct A { int x; int y; char c; char d; };  // Tamaño: 10 bytes (sin padding)
#pragma pack(pop)

struct B { int x; int y; char c; char d; };  // Tamaño: 12 bytes (padding por defecto a 4)

Alternativa GCC/Clang:

struct __attribute__((packed)) A { int x; int y; char c; char d; };

Claves para reversing: - El padding cambia offsets y tamaños; verificar el alineamiento aplicado. - #pragma pack es específico del compilador; __attribute__((packed)) es más portátil en GCC/Clang.


Parte 7: warning y error

#warning "mensaje"   // Emite aviso en compilación (no estándar, común en GCC/Clang)
#error "mensaje"     // Fuerza error de compilación
- Útiles para validar configuraciones o abortar builds en casos no soportados.


Parte 8: Ejemplo completo del video

Código base:

#ifndef TABLE_SIZE
#define TABLE_SIZE 100
#endif

#define VALUE 1
#define getmax(x,y) ((x)>(y)?(x):(y))

int main() {
    int a = 5, b = 7;

    int table1[TABLE_SIZE];
    int table2[TABLE_SIZE];

    getmax(a, b);  // Macro expandida inline; el resultado no se usa ni se imprime

    #if VALUE == 1
        std::cout << "one\n";
    #else
        std::cout << "not one\n";
    #endif
}

Salida esperada:

a: 5
b: 7
size of table1: 400
size of table2: 400
one

Notas: - TABLE_SIZE se sustituye por 100 → arrays de 100 enteros (400 bytes cada uno). - getmax se expande a comparación directa; no hay llamada de función. - La rama #else se elimina completamente porque VALUE == 1 en compilación.


Parte 9: Análisis a bajo nivel

Aspectos visibles en el binario (MSVC x64): - Sin llamada a getmax: la macro se expande a un cmp y salto condicional; el resultado temporal (tv129) ni se usa. - Tamaños de arrays: los sizeof se materializan como literales 400 en las llamadas a operator<<. - Condicional eliminado: solo queda la cadena "one"; no existe código para "not one". - Stack frame sencillo: locals a, b y temporales para impresiones; sin reservas para funciones auxiliares porque no hay call a getmax.

Patrones que delatan macros: - Comparaciones y asignaciones inlined donde esperarías una llamada a función. - Literales repetidos (como el 400) en lugar de cálculos dinámicos. - Ausencia total de símbolos o referencias a la macro en la tabla de símbolos.

Fragmento de ensamblador del video (MSVC x64)

mov     DWORD PTR a$[rsp], 5          ; a = 5
mov     DWORD PTR b$[rsp], 7          ; b = 7
...
mov     edx, 400                      ; sizeof(table1) literal
call    std::basic_ostream::operator<<(unsigned __int64)
...
cmp     DWORD PTR a$[rsp], eax        ; getmax(a,b) expandido
jle     SHORT $LN3@main               ; si a <= b
mov     eax, DWORD PTR a$[rsp]        ; tv129 = a
mov     DWORD PTR tv129[rsp], eax
jmp     SHORT $LN4@main
$LN3@main:
    mov     eax, DWORD PTR b$[rsp]    ; tv129 = b
    mov     DWORD PTR tv129[rsp], eax
$LN4@main:
...
lea     rdx, OFFSET FLAT:$SG35278     ; "one"
call    std::operator<<               ; rama #if VALUE==1

Lo que muestra: - TABLE_SIZE ya está resuelto: los 400 están embebidos en las llamadas de impresión. - getmax no genera call: solo cmp + salto y asignación a un temporal (tv129). - El bloque #else no existe: solo queda la carga de la cadena one y su impresión. - El frame reserva 56 bytes; solo guarda a, b y temporales de impresión.


Resumen

  • El preprocesador opera antes de la compilación: expande macros, evalúa condicionales y ajusta padding.
  • Macros con argumentos necesitan paréntesis para evitar sorpresas y pueden duplicar efectos secundarios.
  • Condicionales (#if/#ifdef) eliminan ramas completas: lo que no cumple la condición no existe en el binario.
  • #pragma pack altera tamaños y offsets; verifica el alineamiento al analizar estructuras en reversing.
  • #error/#warning ayudan a detectar configuraciones no deseadas durante el build.