Definición de Tipos con typedef en C++: Guía Completa de Reversing¶
Índice¶
- Introducción
- Parte 1: ¿Qué es typedef?
- Parte 2: Sintaxis Básica
- Parte 3: typedef con Tipos Fundamentales
- Parte 4: typedef con Estructuras
- Parte 5: typedef con Punteros a Funciones
- Parte 6: Análisis en Ensamblador
- Parte 7: Debugging y Análisis en IDA Pro
- Parte 8: Portabilidad y Dependencia de Plataforma
- Resumen: Lo Más Importante
Introducción¶
El typedef es una palabra clave en C++ que permite crear nombres alternativos (alias) para tipos de datos existentes. Esta funcionalidad:
- Mejora la legibilidad del código
- Facilita la portabilidad entre plataformas
- Simplifica la declaración de tipos complejos (como punteros a funciones)
- Reduce la duplicación de código
Objetivo: Entender cómo typedef funciona internamente, cómo se ve en ensamblador y cómo reconocerlo en reverse engineering.
Parte 1: ¿Qué es typedef?¶
Concepto Fundamental¶
typedef es un mecanismo de compilación que crea un alias para un tipo existente. No crea un nuevo tipo en tiempo de ejecución, sino que es una instrucción para el compilador que dice: "usa este nuevo nombre como sinónimo para este tipo".
Importante: A nivel de ensamblador, typedef desaparece completamente. El compilador lo resuelve y usa el tipo original.
Ejemplo Conceptual¶
// Declaración original
unsigned int contador = 0;
// Con typedef
typedef unsigned int UINT;
UINT contador = 0; // Exactamente lo mismo a nivel de compilación
En ambos casos, contador es un unsigned int de 4 bytes. La única diferencia es la legibilidad.
Parte 2: Sintaxis Básica¶
Forma General¶
Componentes:
| Componente | Significado |
|---|---|
typedef |
Palabra clave |
<tipo_original> |
Cualquier tipo C++ (fundamental o derivado) |
<nuevo_nombre> |
El alias que queremos crear |
Cosas Que Puedes Hacer con typedef¶
- Alias para tipos fundamentales
- Alias para estructuras
- Alias para uniones
- Alias para punteros
- Alias para punteros a funciones (lo más complejo)
Parte 3: typedef con Tipos Fundamentales¶
Ejemplos Comunes¶
// Tipos sin signo
typedef unsigned int UINT;
typedef unsigned char BYTE;
typedef unsigned short int WORD;
typedef unsigned long int DWORD;
// Tipos con signo
typedef signed int INT;
typedef signed char CHAR;
// Enteros de tamaño fijo (modernamente se usan <cstdint>)
typedef unsigned int uint32_t;
typedef unsigned char uint8_t;
Caso Práctico: WORD¶
typedef unsigned short int WORD;
// Ahora podemos usar WORD en lugar de unsigned short int
WORD resultado = 1024;
¿Por qué esto es útil?
En algunas plataformas, una palabra (WORD) es de 16 bits, pero en otras puede ser diferente. Si defines WORD en un archivo de cabecera central, puedes cambiar fácilmente la definición según la plataforma:
// En plataforma A (16 bits)
typedef unsigned short int WORD;
// En plataforma B (32 bits)
typedef unsigned int WORD;
// El resto del código no cambia
A Nivel de Compilación¶
El compilador internamente reemplaza UINT por unsigned int. A nivel de ensamblador:
Parte 4: typedef con Estructuras¶
Simplificación de Sintaxis¶
Sin typedef, para usar una estructura necesitas escribir la palabra struct:
struct stpunto {
int x;
int y;
int z;
};
// Declarar variable
stpunto p; // ERROR: falta 'struct'
struct stpunto p; // CORRECTO: necesitas 'struct'
Con typedef, puedes simplificar:
typedef struct stpunto tipoPunto;
// Ahora puedes usar directamente
tipoPunto p; // SIN necesidad de 'struct'
Forma Compacta¶
También puedes definir el nombre del tipo al mismo tiempo que la estructura:
typedef struct {
int x;
int y;
int z;
} Punto3D;
// Declarar variable
Punto3D punto; // Directo, sin 'struct'
En este caso, la estructura no tiene nombre (es anónima), pero el tipo Punto3D sí existe.
Comparación¶
// Forma clásica (sin typedef)
struct Punto3D {
int x;
int y;
int z;
};
struct Punto3D p1; // Necesitas 'struct'
// Forma con typedef
typedef struct {
int x;
int y;
int z;
} Punto3D;
Punto3D p1; // Directo
A Nivel de Compilación¶
A nivel de ensamblador, ambas formas son idénticas. El compilador genera el mismo código para acceder a los miembros:
typedef struct {
int x; // offset +0
int y; // offset +4
int z; // offset +8
} Punto3D;
Punto3D p;
p.x = 10; // mov dword ptr [rbp - 12], 10 (p está 12 bytes abajo, x está en +0)
Parte 5: typedef con Punteros a Funciones¶
El Caso Más Complejo¶
Los punteros a funciones tienen una sintaxis complicada en C++. Aquí es donde typedef realmente brilla:
Sin typedef (Confuso)¶
// Puntero a función que retorna int y no toma argumentos
int (*pf)();
// Múltiples punteros
int (*pf1)();
int (*pf2)();
int (*pf3)();
Problema: La sintaxis int (*pf)() es confusa y repetitiva.
Con typedef (Claro)¶
// Define el tipo una vez
typedef int (*PFI)();
// Ahora crea variables fácilmente
PFI f1;
PFI f2;
PFI f3;
Anatomía de la Sintaxis¶
typedef int (*PFI)();
| | | |
| | | +-- Parámetros (vacío = sin parámetros)
| | +------ Nombre del tipo (PFI)
| +---------- * indica que es un puntero
+-------------- Tipo de retorno (int)
Con Parámetros¶
// Puntero a función que toma (int, float) y retorna double
typedef double (*CALC)(int, float);
CALC calculadora;
Parte 6: Análisis en Ensamblador¶
Ejemplo Completo¶
A nivel de ensamblador:
main:
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 1h ; a = 1, DWORD (4 bytes)
xor eax, eax ; return 0
pop rbp
ret
Observación: UINT desaparece completamente. El compilador ve unsigned int, que es un DWORD (4 bytes).
Ejemplo con Estructuras¶
typedef struct {
int x;
int y;
int z;
} Punto3D;
int main() {
Punto3D p;
p.x = 10;
p.y = 20;
p.z = 30;
}
A nivel de ensamblador:
main:
push rbp
mov rbp, rsp
sub rsp, 12h ; Reservar 12 bytes para p (3 ints)
mov dword ptr [rbp - 12], 10h ; p.x = 10 (offset +0)
mov dword ptr [rbp - 8], 14h ; p.y = 20 (offset +4)
mov dword ptr [rbp - 4], 1eh ; p.z = 30 (offset +8)
xor eax, eax
mov rsp, rbp
pop rbp
ret
El nombre Punto3D desaparece. Solo quedan los accesos a memoria con offsets.
Parte 7: Debugging y Análisis en IDA Pro¶
Reconocimiento de typedef¶
Cuando abres un programa compilado en IDA Pro o x64dbg, es difícil saber si un tipo fue creado con typedef porque:
- typedef desaparece en el binario (es información del compilador)
- Solo ves los tipos subyacentes en el código ensamblador
- Sin símbolos de debug, es imposible conocer los nombres de typedef
Debugging de Punteros a Funciones¶
Cuando encuentras un call indirecto (call a través de un puntero), puedes usar un truco en IDA Pro para anotar adónde va:
Truco 1: Ver la Dirección de la Función¶
typedef int (*PFI)();
int miFunc() { return 2; }
int main() {
PFI f = &miFunc;
int result = f();
return 0;
}
En ensamblador:
lea rax, miFunc ; Cargar dirección de miFunc en rax
mov [rbp - 8], rax ; Guardar en f
call qword ptr [rbp - 8] ; Call indirecto a través de f
En IDA Pro:
- Ve a donde está la función (ej:
miFuncen0x140001000) - Haz clic derecho en
miFunc→ "Copy address" - Ve al
call qword ptr [...]→ Haz clic derecho - Edit → Change call address
- Pega la dirección con
0xal inicio:0x140001000
Esto añade una anotación y permite hacer doble clic para navegar a la función.
Truco 2: Usar el Menú "Assemble"¶
Si haces clic derecho → Assemble, ves la sintaxis clásica:
Esto te ayuda a confirmar que es un puntero a función.
Debugging en Tiempo de Ejecución¶
typedef int (*PFI)();
int miFunc() { return 2; }
PFI q = &miFunc; // Global
int main() {
int result = q();
printf("%d", result); // Imprime: 2
return 0;
}
En x64dbg o WinDbg:
- Establece un breakpoint en
q() - Inspecciona
q→ Verás la dirección (ej:0x140001000) - Navega a esa dirección para ver el código de
miFunc - Los call registros como
call raxocall qword ptr [...]son indicios de punteros a funciones
Parte 8: Portabilidad y Dependencia de Plataforma¶
Problema: Tamaños Diferentes¶
En distintas plataformas, los tipos pueden tener tamaños diferentes:
// Plataforma de 32 bits
sizeof(int) = 4 bytes
sizeof(long) = 4 bytes
sizeof(long long) = 8 bytes
// Plataforma de 64 bits
sizeof(int) = 4 bytes
sizeof(long) = 8 bytes // DIFERENTE
sizeof(long long) = 8 bytes
Solución: typedef Condicional¶
Si necesitas una palabra de exactamente 16 bits en cualquier plataforma, puedes usar typedef con condicionales:
// En windows_types.h para plataforma Windows
#ifdef _WIN32
typedef unsigned short int WORD;
#endif
// En unix_types.h para plataforma Unix
#ifdef __unix__
typedef unsigned short int WORD;
#endif
// El resto del código no cambia
WORD valor = 1000;
O mejor aún, usa los tipos estándar de C++11:
// En <cstdint> (moderno y recomendado)
typedef uint16_t WORD;
typedef uint32_t DWORD;
typedef uint8_t BYTE;
Resumen: TL;DR¶
¿Qué es typedef?¶
- Crea un alias para un tipo existente
- Desaparece en la compilación (el compilador lo resuelve)
- Mejora legibilidad y facilita portabilidad
Casos de Uso¶
| Caso | Ejemplo | Beneficio |
|---|---|---|
| Tipos fundamentales | typedef unsigned int UINT; |
Portabilidad |
| Estructuras | typedef struct { ... } Punto3D; |
No necesita struct |
| Punteros a funciones | typedef int (*PFI)(); |
Sintaxis clara |
En Ensamblador¶
- typedef desaparece completamente
- Solo ves los tipos subyacentes
- Los offsets en memoria se calculan según los tipos originales
En Debugging/Reversing¶
- Sin símbolos de debug, es difícil saber si un tipo tiene typedef
- Los call indirectos son indicio de punteros a funciones
- Usa IDA Pro para anotar direcciones y mejorar el análisis
Lo Más Importante¶
En reversing:
mov dword ptr [rbp - 4], 5h ; ¿Es UINT? ¿unsigned int? ¿int?
; A nivel de asm, da lo mismo.
call qword ptr [rbp - 8] ; Aquí hay un puntero a función.
; ¿Con typedef? Sin símbolo, no sabes.
El tipo de dato (typedef o no) es irrelevante para la ejecución, pero es crucial para la comprensión humana del código.
Ejercicio BCD¶
Este ejercicio demuestra un caso práctico donde typedef simplifica significativamente la sintaxis de tipos complejos. Pero más importante, enseña:
- Cómo trabajar con datos personalizados que no son tipos primitivos
- Manipulación de bits a bajo nivel
- Cómo se ve a nivel de ensamblador
- La diferencia entre la interpretación lógica (lo que queremos que haga) y la física (lo que el procesador realmente hace)
¿Qué es BCD (Binary Coded Decimal)?¶
BCD es una forma de codificar números decimales donde:
- Cada dígito decimal (0-9) se almacena en 4 bits
- Un byte (8 bits) almacena 2 dígitos decimales
- El compilador no sabe nada de esto - es nuestra interpretación
Ejemplo:
Número decimal: 98
En BCD: 0x98
Desglose:
Byte: [1001 1000]
↑ ↑
9 8 (dígito mayor y menor)
Cada nibble (4 bits) representa un dígito decimal
La Línea Clave: typedef¶
Esto define:
- numero = array de 32 bytes = 64 dígitos decimales
- Permite sumar números mucho más grandes que unsigned long long (máx 19 dígitos)
Sin typedef:
bool Sumar(unsigned char n1[32], unsigned char n2[32], unsigned char r[32]);
unsigned char num1[32];
unsigned char num2[32];
unsigned char resultado[32];
Con typedef:
El Código C++¶
int main() {
numero n1 = {
0x00,0x00,0x00,0x00, // ...ceros...
0x00,0x00,0x10,0x00 // = 0010 = 1, 0 = 10
};
numero n2 = {
0x00,0x00,0x00,0x00, // ...ceros...
0x00,0x09,0x98,0x12 // = 0x09 (09), 0x98 (98), 0x12 (12) = 9812
};
numero suma;
Sumar(n1, n2, suma); // 10 + 9812 = 9822
}
La Función Sumar() - Paso a Paso¶
bool Sumar(numero n1, numero n2, numero r) {
int acarreo = 0;
int c;
for(int i = cadmax-1; i >= 0; i--) { // Empieza desde el final (dígito menos significativo)
// ========== SUMAR DÍGITO DE MENOR PESO (4 bits bajos) ==========
c = acarreo + (n1[i] & 0x0f) + (n2[i] & 0x0f);
// ↑ ↑
// Extrae 4 bits Extrae 4 bits
// inferiores inferiores
if(c > 9) {
c -= 10; // Si se pasó de 9, restar 10
acarreo = 1; // Marcar acarreo para el siguiente dígito
} else
acarreo = 0;
r[i] = c; // Guardar en la parte baja del resultado
// ========== SUMAR DÍGITO DE MAYOR PESO (4 bits altos) ==========
c = acarreo + ((n1[i] >> 4) & 0x0f) + ((n2[i] >> 4) & 0x0f);
// ↑ ↑
// Desplaza 4 bits Desplaza 4 bits
// y extrae el y extrae el
// dígito mayor dígito mayor
if(c > 9) {
c -= 10;
acarreo = 1;
} else
acarreo = 0;
r[i] |= (c << 4); // Guardar en la parte alta (desplaza 4 bits a la izquierda)
}
return !acarreo; // Retorna true si NO hay acarreo final
}
Ejemplo Concreto: 10 + 9812 = 9822¶
Iteración i=31 (último byte):
n1[31] = 0x10 (dígitos: 1, 0)
n2[31] = 0x12 (dígitos: 1, 2)
Dígito menor:
c = 0 + (0x10 & 0x0f) + (0x12 & 0x0f)
c = 0 + 0 + 2 = 2
r[31] = 0x02 ✓
Dígito mayor:
c = 0 + ((0x10 >> 4) & 0x0f) + ((0x12 >> 4) & 0x0f)
c = 0 + 1 + 1 = 2
r[31] |= (2 << 4) = r[31] | 0x20 = 0x02 | 0x20 = 0x22 ✓
Iteración i=30:
n1[30] = 0x00 (dígitos: 0, 0)
n2[30] = 0x98 (dígitos: 9, 8)
Dígito menor:
c = 0 + 0 + 8 = 8
r[30] = 0x08 ✓
Dígito mayor:
c = 0 + 0 + 9 = 9
r[30] |= 0x90 = 0x08 | 0x90 = 0x98 ✓
Resultado: 0x00...9822 = 9822 ✓
La Función MostrarBCD() - Filtrado de Ceros¶
void MostrarBCD(numero n) {
char c;
bool cero = true; // Flag: ¿estamos en los ceros iniciales?
for(unsigned int i = 0; i < cadmax; i++) {
// Procesar dígito mayor (4 bits altos)
c = '0' + ((n[i] >> 4) & 0x0f); // Convertir a carácter ASCII
if(c != '0') cero = false; // Ya pasamos los ceros iniciales
if(!cero) cout << c; // Solo imprimir si no estamos en ceros iniciales
// Procesar dígito menor (4 bits bajos)
c = '0' + (n[i] & 0x0f);
if(c != '0') cero = false;
if(!cero) cout << c;
}
}
¿Por qué la conversión '0' +?
Para convertir un dígito (0-9) a su carácter ASCII:
- Dígito 0 + '0' (ASCII 48) = '0' (ASCII 48)
- Dígito 8 + '0' (ASCII 48) = '8' (ASCII 56)
El flag cero:
Evita imprimir los ceros a la izquierda. Ejemplo:
Análisis en Ensamblador (x64 MSVC)¶
La Inicialización¶
main:
sub rsp, 152 ; Reservar espacio en stack
mov BYTE PTR n1$[rsp+30], 16 ; 0x10 en posición 30
mov BYTE PTR n1$[rsp+31], 0 ; 0x00 en posición 31
mov BYTE PTR n2$[rsp+29], 9 ; 0x09
mov BYTE PTR n2$[rsp+30], 152 ; 0x98 (152 decimal)
mov BYTE PTR n2$[rsp+31], 18 ; 0x12 (18 decimal)
Observación: Los valores se guardan en decimal, no hexadecimal. 0x98 = 152 decimal.
El Loop de Sumar()¶
Sumar PROC:
mov DWORD PTR i$1[rsp], 31 ; i = 31
jmp SHORT $LN4@Sumar
$LN2@Sumar:
dec DWORD PTR i$1[rsp] ; i--
$LN4@Sumar:
cmp DWORD PTR i$1[rsp], 0 ; Comparar i con 0
jl $LN3@Sumar ; Si i < 0, salir
movsxd rax, DWORD PTR i$1[rsp] ; Cargar índice i en rax
mov rcx, QWORD PTR n1$[rsp] ; rcx = dirección de n1
movzx eax, BYTE PTR [rcx+rax] ; Cargar n1[i]
and eax, 15 ; AND con 0x0f (máscara 4 bits)
; ... continuar sumando ...
Patrón clave:
and eax, 15 ; Máscara para 4 bits bajos (0x0f)
sar eax, 4 ; Desplaza 4 bits a la derecha (para altos)
shl ecx, 4 ; Desplaza 4 bits a la izquierda (para guardar en altos)
or eax, ecx ; Combina ambas partes
Debugging en x64dbg¶
Paso 1: Ver las variables iniciales
n1[31] = 0x10 (10 en decimal = 0x0A, pero se almacena como 0x10)
n2[29] = 0x09
n2[30] = 0x98
n2[31] = 0x12
Observar: El array está "de atrás para adelante"
porque las sumas se hacen desde el final
Paso 2: Ejecutar hasta Sumar()
Paso 3: Seguir el loop
En cada iteración: 1. Cargar n1[i] y n2[i] 2. Extraer los 4 bits bajos y sumarlos 3. Extraer los 4 bits altos y sumarlos (con acarreo) 4. Guardar el resultado en r[i] 5. El acarreo se propaga a la siguiente iteración
Paso 4: Ver el resultado
Lo Más Importante de Este Ejercicio¶
1. typedef Simplifica Sintaxis Compleja¶
// Sin typedef: verboso y fácil de equivocarse
unsigned char resultado[32];
// Con typedef: claro y mantenible
numero resultado;
2. Los Compiladores "Resuelven" typedef¶
- En ensamblador, no existe
numero - Solo ves operaciones con bytes:
mov BYTE PTR ... - typedef es puramente para el programador, no para la máquina
3. Manipulación de Bits es Fundamental en Reversing¶
and eax, 15 ; Aislar 4 bits
sar eax, 4 ; Extraer bits altos
shl ecx, 4 ; Desplazar para combinar
or eax, ecx ; Fusionar nibbles
Estas operaciones aparecen constantemente en código compilado.
4. Los Datos Tienen "Dos Vidas"¶
- Vida física: bytes en memoria (0x98 es solo un byte)
- Vida lógica: cómo los interpretamos (0x98 = dígitos 9 y 8 en BCD)
El procesador solo ve bytes. Nosotros (como reversers) debemos interpretar el significado.
Resumen: BCD vs Tipos Modernos¶
| Aspecto | BCD (Este Ejercicio) | Tipos Modernos |
|---|---|---|
| Tamaño | 2 dígitos por byte (compacto) | Depende del tipo |
| Precisión | Exacta (sin punto flotante) | Exacta solo con BigInt |
| Velocidad | Lenta (manipulación manual) | Rápida (nativa del HW) |
| Uso Moderno | Raro (legacy systems) | Lo normal |
| Educativo | EXCELENTE (bits, acarreo, lógica) | Menos instructivo |
Conclusión: Este ejercicio es "rebuscado" por una razón: enseña conceptos fundamentales que encontrarás en reversing de sistemas legacy, firmware, y código optimizado.
El typedef aquí es el mecanismo que hace todo esto manejable sin repetir unsigned char[32] constantemente.