El preprocesador en C++: Guía de Reversing¶
Índice¶
- Introducción
- Parte 1: ¿Qué hace el preprocesador?
- Parte 2: #define y macros con argumentos
- Parte 3: #undef
- Parte 4: Condicionales de preprocesador
- Parte 5: #include
- Parte 6: pragma pack y atributos
- Parte 7: warning y error
- Parte 8: Ejemplo completo del video
- Parte 9: Análisis a bajo nivel
- Resumen
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¶
- Elimina una definición previa. Útil para redefinir o limpiar macros heredadas de otros headers.Parte 4: Condicionales de preprocesador¶
Guardas típicas:
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:
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
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:
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 packaltera tamaños y offsets; verifica el alineamiento al analizar estructuras en reversing.#error/#warningayudan a detectar configuraciones no deseadas durante el build.