Saltar a contenido

Funciones con Argumentos Variables (Variadic Functions)

Introducción

Las funciones con argumentos variables permiten pasar una cantidad indeterminada de parámetros a una función. Esto es posible gracias a macros especiales definidas en <cstdarg> (C++) o <stdarg.h> (C).

Declaración

return_type function_name(fixed_params, ...);

Ejemplo:

void funcion(int a, char c, ...);

  • Los parámetros fijos (a, c) siempre están presentes
  • Los tres puntos suspensivos (...) indican argumentos variables adicionales

Componentes Principales

1. Tipo va_list

va_list ap;
  • Es un tipo de dato que funciona como un puntero
  • A bajo nivel es equivalente a char* o void*
  • Guarda la dirección de un array con los argumentos variables

2. Macro va_start

va_start(ap, last_fixed_param);

Función: - Inicializa ap con la dirección del primer argumento variable - Requiere dos parámetros: - ap: variable del tipo va_list - last_fixed_param: último parámetro conocido/fijo

Ejemplo:

void funcion(int a, ...) {
    va_list p;
    va_start(p, a);  // p apunta al argumento después de 'a'
}

3. Macro va_arg

value = va_arg(ap, type);

Función: - Devuelve el siguiente valor de la lista de parámetros - Requiere especificar el tipo del argumento - Incrementa automáticamente el puntero al siguiente argumento

Comportamiento: 1. Lee el valor actual apuntado por ap 2. Incrementa ap para apuntar al siguiente 3. Devuelve el valor según el tipo especificado

4. Macro va_end

va_end(ap);

Función: - Restaura el estado de ap - Debe llamarse antes de retornar de la función

Convención de Llamada x64

Paso de Argumentos

En arquitectura x64, los argumentos se pasan en el siguiente orden:

  1. Primeros 4 argumentos: Registros
  2. 1º → RCX
  3. 2º → RDX
  4. 3º → R8
  5. 4º → R9

  6. Argumentos 5+: Stack (pila)

  7. Se colocan debajo del shadow space

Shadow Space

+------------------+  <- RSP al inicio de función
| Shadow Space     |  16 bytes (4 slots de 8 bytes)
| (4 registros)    |
+------------------+
| Arg #1 (RCX)     |  [RSP+8]
| Arg #2 (RDX)     |  [RSP+16]
| Arg #3 (R8)      |  [RSP+24]
| Arg #4 (R9)      |  [RSP+32]
+------------------+
| Arg #5           |  [RSP+40]
| Arg #6           |  [RSP+48]
| Arg #7           |  [RSP+56]
| ...              |
+------------------+

Importante: Aunque los primeros 4 argumentos se pasen por registro, se copian al stack en el shadow space, permitiendo tratarlos como un array continuo.

Ejemplo 1: Tipos Homogéneos

Código Fuente

#include <iostream>
#include <cstdarg>
using namespace std;

void funcion(int a, ...);

int main() {
    funcion(1, "cadena 1", 0);
    funcion(1, "cadena 1", "cadena 2", "cadena 3", "cadena4", "cadena5", 0);
    funcion(2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0);
    return 0;
}

void funcion(int a, ...) {
    va_list p;
    va_start(p, a);
    char* arg;
    int arg2;

    if (a == 1) {
        while ((arg = va_arg(p, char*))) {
            cout << arg << " ";
        }
    }
    else {
        while ((arg2 = va_arg(p, int))) {
            cout << arg2 << " ";
        }
    }

    va_end(p);
    cout << endl;
}

Análisis del Comportamiento

Primera llamada: funcion(1, "cadena 1", 0)

  1. a = 1 → Usa tipo char* (strings)
  2. va_start(p, a)p apunta a "cadena 1"
  3. Primera iteración:
  4. va_arg(p, char*) devuelve "cadena 1"
  5. p se incrementa para apuntar a 0
  6. Imprime: "cadena 1"
  7. Segunda iteración:
  8. va_arg(p, char*) devuelve 0
  9. El while termina (condición falsa)

Segunda llamada: funcion(1, "cadena 1", ..., "cadena5", 0)

  • Itera 5 veces imprimiendo todas las cadenas
  • Se detiene al encontrar 0

Tercera llamada: funcion(2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0)

  1. a = 2 → Usa tipo int
  2. Itera imprimiendo los enteros hasta encontrar 0

Análisis a Bajo Nivel

Alineación en Stack

Después de guardar los registros en shadow space:

[RSP+8]   → Arg #1 (tipo: 1 o 2)
[RSP+16]  → Arg #2 (primer argumento variable)
[RSP+24]  → Arg #3
[RSP+32]  → Arg #4
[RSP+40]  → Arg #5
[RSP+48]  → Arg #6
[RSP+56]  → Arg #7
...

Operación va_start

lea rax, QWORD PTR [rsp+16]  ; Dirección del 2º argumento
mov QWORD PTR p$[rsp], rax   ; p = dirección del array de args variables

Resultado: p apunta al primer argumento variable (el siguiente después de a)

Operación va_arg

; Incrementar puntero
mov rax, QWORD PTR p$[rsp]   ; Cargar p
add rax, 8                    ; p += 8 (siguiente elemento)
mov QWORD PTR p$[rsp], rax   ; Guardar p incrementado

; Leer valor anterior
mov rax, QWORD PTR p$[rsp]   ; Cargar p (ya incrementado)
mov rax, QWORD PTR [rax-8]   ; Leer valor en p-8 (elemento actual)

Mecanismo: 1. Incrementa p en 8 bytes (tamaño de un QWORD) 2. Lee el valor en p - 8 (el elemento que acabamos de pasar) 3. Esto permite avanzar al siguiente elemento mientras se devuelve el actual

Tipos char* vs int

Para char* (strings):

mov rax, QWORD PTR [rax-8]   ; Leer dirección (8 bytes)
mov rax, QWORD PTR [rax]     ; Dereferencia adicional (opcional)

Para int:

mov eax, DWORD PTR [rax-8]   ; Leer solo 4 bytes (int)

Condición de Salida

cmp QWORD PTR arg$[rsp], 0   ; Comparar con 0
je  SHORT $LN3@funcion       ; Si es 0, salir del loop

Importante: Por esto es crítico terminar con 0 los argumentos variables.

Ejemplo 2: Tipos Mixtos

Código Fuente

#include <iostream>
#include <cstdarg>
using namespace std;

void funcion(int a, ...);

int main() {
    funcion(1, "cadena 1", 2, 9, "cadena 2", "cadena 3", "cadena4", "cadena5", 0);
    return 0;
}

void funcion(int a, ...) {
    va_list p;
    va_start(p, a);

    for (int i = 0; i < 10; i++) {
        if (i == 0 || i == 3 || i == 4 || i == 5 || i == 6) {
            cout << "Cadena: ";
            cout << va_arg(p, char*) << " " << endl;
        }
        else {
            cout << "Entero: ";
            cout << va_arg(p, int) << " " << endl;
        }
    }

    va_end(p);
    cout << endl;
}

Análisis

Secuencia de Argumentos

Índice Posición Tipo Valor
0 Arg #2 char* "cadena 1"
1 Arg #3 int 2
2 Arg #4 int 9
3 Arg #5 char* "cadena 2"
4 Arg #6 char* "cadena 3"
5 Arg #7 char* "cadena4"
6 Arg #8 char* "cadena5"
7 Arg #9 int 0

Manejo de Tipos Mixtos

El código debe conocer explícitamente el tipo de cada argumento:

if (i == 0 || i == 3 || i == 4 || i == 5 || i == 6) {
    // Posiciones con strings
    va_arg(p, char*)
}
else {
    // Posiciones con enteros
    va_arg(p, int)
}

Análisis a Bajo Nivel

Alineación en Stack

[RSP+8]   → 1 (tipo, parámetro fijo)
[RSP+16]  → "cadena 1" (char*, 8 bytes)
[RSP+24]  → 2 (int, ocupa 8 bytes en stack)
[RSP+32]  → 9 (int, ocupa 8 bytes en stack)
[RSP+40]  → "cadena 2" (char*, 8 bytes)
[RSP+48]  → "cadena 3" (char*, 8 bytes)
[RSP+56]  → "cadena4" (char*, 8 bytes)
[RSP+64]  → "cadena5" (char*, 8 bytes)
[RSP+72]  → 0 (int, ocupa 8 bytes en stack)

Nota: Aunque int ocupa 4 bytes, cada slot en el stack es de 8 bytes (QWORD).

Verificación de Tipo en Ensamblador

; Comparaciones para determinar el tipo
cmp DWORD PTR i$1[rsp], 0
je  SHORT $LN7@funcion        ; Si i==0, es string

cmp DWORD PTR i$1[rsp], 3
je  SHORT $LN7@funcion        ; Si i==3, es string

cmp DWORD PTR i$1[rsp], 4
je  SHORT $LN7@funcion        ; Si i==4, es string

; ... más comparaciones

jne $LN5@funcion              ; Si no coincide, es entero

Consideraciones Importantes

1. Responsabilidad del Programador

El programador debe: - Conocer el tipo de cada argumento - Especificar correctamente el tipo en va_arg - Terminar la lista con un valor centinela (generalmente 0)

2. Peligros de Tipos Incorrectos

Si se especifica un tipo incorrecto:

// Argumento real: "cadena"
int valor = va_arg(p, int);  // ❌ ERROR: interpreta dirección como int

Consecuencias: - Lectura de datos incorrectos - Posibles accesos a memoria inválida - Crash del programa

3. Centinela 0

Siempre terminar con 0:

funcion(1, "arg1", "arg2", 0);  // ✅ Correcto

Razón: Sin centinela, el loop podría: - Leer basura en el stack - Interpretar datos aleatorios como argumentos válidos - Causar comportamiento indefinido

4. Limitaciones

  • No hay verificación de tipos en tiempo de compilación
  • No se puede saber la cantidad de argumentos automáticamente
  • Requiere convención entre llamador y función

Formas Alternativas de Manejo

Con Cantidad Explícita

void funcion(int count, ...) {
    va_list p;
    va_start(p, count);

    for (int i = 0; i < count; i++) {
        int valor = va_arg(p, int);
        cout << valor << " ";
    }

    va_end(p);
}

// Uso
funcion(3, 10, 20, 30);  // No necesita centinela

Sin While

void funcion(int a, ...) {
    va_list p;
    va_start(p, a);

    int arg1 = va_arg(p, int);
    int arg2 = va_arg(p, int);
    int arg3 = va_arg(p, int);
    // Acceso directo a argumentos conocidos

    va_end(p);
}

Resumen de Macros

Macro Propósito Sintaxis
va_list Tipo de dato puntero va_list ap;
va_start Inicializa el puntero va_start(ap, last_param);
va_arg Lee y avanza va_arg(ap, type);
va_end Limpia/restaura va_end(ap);

Flujo Completo

void ejemplo(int fijo, ...) {
    // 1. Declarar va_list
    va_list args;

    // 2. Inicializar
    va_start(args, fijo);

    // 3. Procesar argumentos
    while (condicion) {
        tipo valor = va_arg(args, tipo);
        // usar valor
    }

    // 4. Limpiar
    va_end(args);
}

Patrones de Implementación a Bajo Nivel

Patrón de Incremento y Lectura

; Patrón repetido para cada va_arg:

; 1. Incrementar puntero
mov rax, QWORD PTR p[rsp]     ; Cargar p
add rax, 8                     ; p += sizeof(arg)
mov QWORD PTR p[rsp], rax     ; Guardar p actualizado

; 2. Leer valor anterior
mov rax, QWORD PTR p[rsp]     ; Cargar p (ya incrementado)
mov rax, QWORD PTR [rax-8]    ; Leer valor en posición anterior

; 3. Usar valor en rax

Optimización del Compilador

A veces el compilador genera código más eficiente:

; Versión optimizada
lea rax, [p + 8]              ; p_nuevo = p + 8
mov rdx, [p]                  ; valor = *p
mov p, rax                    ; p = p_nuevo

Referencias

  • Microsoft MSVC Documentation
  • <cstdarg> / <stdarg.h> headers
  • x64 calling convention

Ejemplo 3: Formato con String de Tipos

Concepto

Una solución más elegante para manejar tipos mixtos es usar el primer argumento como descriptor de formato, similar a printf().

Código Fuente

#include <iostream>
#include <cstdarg>
#include <cstring>
using namespace std;

void funcion(const char* formato, ...);

int main() {
    funcion("ciic", "Hola", 12, 34, "Adios");
    funcion("ccci", "Uno", "Dos", "Tres", 4);
    funcion("i", 1);
    return 0;
}

void funcion(const char* formato, ...) {
    va_list p;
    va_start(p, formato);

    char* szarg;
    int iarg;

    for (int i = 0; i < strlen(formato); i++) {
        switch (formato[i]) {
            case 'c':  // char* (string)
                szarg = va_arg(p, char*);
                cout << szarg << " ";
                break;
            case 'i':  // int
                iarg = va_arg(p, int);
                cout << iarg << " ";
                break;
            // Se pueden agregar más tipos: 'f' para float, 'd' para double, etc.
        }
    }

    va_end(p);
    cout << endl;
}

Ventajas

  1. No necesita centinela: Usa strlen(formato) para saber cuántos argumentos procesar
  2. Autodescriptivo: El formato indica el tipo de cada argumento
  3. Extensible: Fácil agregar nuevos tipos al switch
  4. Seguro: Cantidad exacta conocida, no lee basura del stack

Interpretación del Formato

Carácter Tipo va_arg
'c' char* va_arg(p, char*)
'i' int va_arg(p, int)
'f' float va_arg(p, float)
'd' double va_arg(p, double)

Análisis de las Llamadas

Primera llamada: funcion("ciic", "Hola", 12, 34, "Adios")

formato = "ciic" (4 caracteres)
Arg #1: "Hola"  (c → char*)
Arg #2: 12      (i → int)
Arg #3: 34      (i → int)
Arg #4: "Adios" (c → char*)

Output: Hola 12 34 Adios

Segunda llamada: funcion("ccci", "Uno", "Dos", "Tres", 4)

formato = "ccci" (4 caracteres)
Arg #1: "Uno"   (c → char*)
Arg #2: "Dos"   (c → char*)
Arg #3: "Tres"  (c → char*)
Arg #4: 4       (i → int)

Output: Uno Dos Tres 4

Tercera llamada: funcion("i", 1)

formato = "i" (1 carácter)
Arg #1: 1       (i → int)

Output: 1

Análisis a Bajo Nivel

Stack Layout - Primera Llamada

[RSP+8]   → formato: dirección de "ciic"
[RSP+16]  → "Hola" (char*, 8 bytes)
[RSP+24]  → 12 (int, ocupa 8 bytes en stack)
[RSP+32]  → 34 (int, ocupa 8 bytes en stack)
[RSP+40]  → "Adios" (char*, 8 bytes)

Flujo del Switch en Ensamblador

; Obtener carácter del formato
movsxd  rax, DWORD PTR i$[rsp]          ; i (índice)
mov     rcx, QWORD PTR formato$[rsp]    ; Dirección de formato
movzx   eax, BYTE PTR [rcx+rax]         ; formato[i]
mov     BYTE PTR char[rsp], al          ; Guardar carácter

; Comparar con 'c' (0x63)
cmp     BYTE PTR char[rsp], 99          ; 0x63 = 'c'
je      SHORT $LN7@funcion              ; Si es 'c', branch

; Comparar con 'i' (0x69)
cmp     BYTE PTR char[rsp], 105         ; 0x69 = 'i'
je      SHORT $LN8@funcion              ; Si es 'i', branch

Caso 'c' - Procesar String

$LN7@funcion:
    ; Incrementar puntero p
    mov     rax, QWORD PTR p$[rsp]
    add     rax, 8
    mov     QWORD PTR p$[rsp], rax

    ; Leer valor anterior (char*)
    mov     rax, QWORD PTR p$[rsp]
    mov     rax, QWORD PTR [rax-8]      ; Dirección del string
    mov     QWORD PTR szarg$[rsp], rax

    ; Imprimir como string
    mov     rdx, QWORD PTR szarg$[rsp]
    mov     rcx, QWORD PTR std::cout
    call    std::operator<<             ; cout << szarg

Caso 'i' - Procesar Entero

$LN8@funcion:
    ; Incrementar puntero p
    mov     rax, QWORD PTR p$[rsp]
    add     rax, 8
    mov     QWORD PTR p$[rsp], rax

    ; Leer valor anterior (int)
    mov     rax, QWORD PTR p$[rsp]
    mov     eax, DWORD PTR [rax-8]      ; Solo 4 bytes (int)
    mov     DWORD PTR iarg$[rsp], eax

    ; Imprimir como entero
    mov     edx, DWORD PTR iarg$[rsp]
    mov     rcx, QWORD PTR std::cout
    call    std::basic_ostream::operator<<  ; cout << iarg

Control del Loop

; Condición del for
mov     eax, DWORD PTR i$[rsp]
inc     eax
mov     DWORD PTR i$[rsp], eax

; Comparar i con strlen(formato)
movsxd  rax, DWORD PTR i$[rsp]
mov     rcx, QWORD PTR formato$[rsp]
call    strlen
cmp     rcx, rax
jae     $LN3@funcion                    ; Si i >= strlen, salir

Ventaja: No necesita verificar un centinela, usa el tamaño exacto del formato.

Argumentos del Main (argc, argv)

Declaración

int main(int argc, char* argv[]) {
    // argc: número de argumentos (incluye el nombre del programa)
    // argv: array de strings con los argumentos
    return 0;
}

Componentes

argc (Argument Count): - Tipo: int - Contiene el número total de argumentos - Incluye el nombre/path del programa como primer argumento - Mínimo valor: 1 (solo el programa)

argv (Argument Vector): - Tipo: char*[] (array de punteros a strings) - argv[0]: Path completo del ejecutable - argv[1] a argv[argc-1]: Argumentos adicionales - argv[argc]: NULL (terminador)

Ejemplo

#include <iostream>
using namespace std;

int main(int argc, char* argv[]) {
    for (int i = 0; i < argc; i++) {
        cout << argv[i] << endl;
    }
    return 0;
}

Ejecución: programa.exe Pepe Pipo loquito

Stack en x64:

RCX → argc = 4
RDX → argv (dirección del array de punteros)

Contenido de argv:

argv[0] → "C:\\path\\to\\programa.exe"
argv[1] → "Pepe"
argv[2] → "Pipo"
argv[3] → "loquito"
argv[4] → NULL

Output:

C:\path\to\programa.exe
Pepe
Pipo
loquito

Análisis a Bajo Nivel

Convención x64

main PROC
    mov     DWORD PTR [rsp+8], ecx      ; Guardar argc
    mov     QWORD PTR [rsp+16], rdx     ; Guardar argv
    sub     rsp, 72
  • RCX contiene argc (cantidad)
  • RDX contiene argv (dirección del array)

Estructura de argv en Memoria

argv → [Offset Ptr1] ─────→ "C:\\programa.exe\0"
       [Offset Ptr2] ─────→ "Pepe\0"
       [Offset Ptr3] ─────→ "Pipo\0"
       [Offset Ptr4] ─────→ "loquito\0"
       [NULL]

Cada elemento es un OFFSET (puntero de 8 bytes) a la string real.

Loop de Impresión

; Inicializar i = 0
mov     DWORD PTR i$[rsp], 0

; Condición del loop
$LN2@main:
    mov     eax, DWORD PTR i$[rsp]      ; Cargar i
    cmp     eax, DWORD PTR argc$[rsp]   ; Comparar i con argc
    jge     SHORT $LN3@main             ; Si i >= argc, salir

    ; Calcular argv[i]
    movsxd  rax, DWORD PTR i$[rsp]      ; i a 64 bits
    mov     rcx, QWORD PTR argv$[rsp]   ; Dirección de argv
    mov     rdx, QWORD PTR [rcx+rax*8]  ; argv[i] (puntero al string)

    ; Imprimir
    mov     rcx, QWORD PTR std::cout
    call    std::operator<<

    ; i++
    inc     DWORD PTR i$[rsp]
    jmp     SHORT $LN2@main

$LN3@main:
    xor     eax, eax                    ; return 0
    ret     0

Cálculo de dirección: argv[i] = *(argv + i * 8)

Configurar Argumentos en Visual Studio

Propiedades del Proyecto → Debugging → Command Arguments:

Pepe Pipo loquito

Configurar Argumentos en IDA Pro

Debugger → Process Options → Parameters:

Pepe Pipo loquito

Funciones Inline

Concepto

El modificador inline sugiere al compilador que inserte el código de la función directamente en el punto de llamada, en lugar de usar una instrucción call.

Declaración

inline int suma(int a, int b) {
    return a + b;
}

int main() {
    int x = suma(5, 3);  // Puede expandirse inline
    return 0;
}

Ventajas y Desventajas

Ventajas Desventajas
Más rápido: No hay overhead de call/ret Mayor tamaño: Código duplicado
Sin stack frame: Ahorra push/pop Cache: Más código puede afectar cache
Optimización: Mejor análisis del compilador Recompilación: Cambios requieren recompilar todo

Comportamiento del Compilador

Importante: inline es solo una sugerencia, no una orden.

El compilador decide si aplicar inline basándose en: - Tamaño de la función (pequeñas son candidatas) - Complejidad (loops, recursión → menos probable) - Nivel de optimización (/O1, /O2, /Ox) - Frecuencia de uso

Comparación: Normal vs Inline

Sin Inline

int suma(int a, int b) {
    return a + b;
}

int main() {
    int x = suma(5, 3);
}

Ensamblador:

main:
    mov     edx, 3
    mov     ecx, 5
    call    suma        ; Llamada a función
    mov     x, eax

suma:
    add     ecx, edx
    mov     eax, ecx
    ret

Con Inline

inline int suma(int a, int b) {
    return a + b;
}

int main() {
    int x = suma(5, 3);
}

Ensamblador (con optimización):

main:
    mov     eax, 8      ; Calculado directamente: 5 + 3
    mov     x, eax
    ; No hay call, código insertado directamente

Cuándo Usar Inline

Buenas candidatas: - Funciones pequeñas (1-5 líneas) - Funciones llamadas frecuentemente - Getters/setters - Operaciones matemáticas simples

Malas candidatas: - Funciones grandes (>10 líneas) - Funciones con loops - Funciones recursivas - Funciones con lógica compleja

Punteros a Funciones

Concepto

Un puntero a función almacena la dirección de memoria donde comienza el código de una función, permitiendo llamarla indirectamente.

Declaración

return_type (*pointer_name)(param_types);

Ejemplos:

int (*pf1)();                    // Puntero a función que devuelve int, sin parámetros
void (*pf2)(int);                // Puntero a función void con parámetro int
float* (*pf3)(char*, int);       // Puntero a función que devuelve float*, con char* e int
void (*pf4)(void (*)(int));      // Puntero a función que recibe otro puntero a función
int (*arr[10])(int);             // Array de 10 punteros a funciones

Sintaxis Alternativa (usando typedef)

// Más legible
typedef int (*FuncPtr)(int, int);
FuncPtr pf;  // Puntero a función

Ejemplo Básico

#include <iostream>
using namespace std;

int funcion() {
    return 1;
}

int main() {
    int (*pf1)();           // Declarar puntero a función
    pf1 = funcion;          // Asignar dirección de función
    int x = pf1();          // Llamar función a través del puntero
    cout << x << endl;      // Output: 1
    return 0;
}

Análisis a Bajo Nivel

Asignación de Dirección

lea     rax, OFFSET FLAT:funcion    ; Obtener dirección de función
mov     QWORD PTR pf1[rsp], rax     ; Guardar en puntero

pf1 ahora contiene: 0x00401050 (ejemplo de dirección)

Llamada Indirecta

call    QWORD PTR pf1[rsp]          ; Call indirecto (a través del puntero)

vs llamada directa:

call    funcion                      ; Call directo (dirección hardcoded)

Ver Referencias en IDA

Para resolver calls indirectos en IDA:

  1. Ubicar el call indirecto: call QWORD PTR pf1[rsp]
  2. Edit → Plugins → Change Call Address
  3. Ingresar dirección: 0x401050
  4. Ahora Ctrl+X mostrará las referencias

Ejemplo 2: Selector de Funciones

#include <iostream>
using namespace std;

void muestra_1() { cout << "Muestra 1" << endl; }
void muestra_2() { cout << "Muestra 2" << endl; }
void muestra_3() { cout << "Muestra 3" << endl; }
void muestra_4() { cout << "Muestra 4" << endl; }

int main() {
    void (*pf1)();              // Puntero a función
    int num;

    do {
        cout << "Introduce un número (1-4, 0 para salir): ";
        cin >> num;

        if (num >= 1 && num <= 4) {
            switch (num) {
                case 1: pf1 = muestra_1; break;
                case 2: pf1 = muestra_2; break;
                case 3: pf1 = muestra_3; break;
                case 4: pf1 = muestra_4; break;
            }
            pf1();  // Llamar función seleccionada
        }
    } while (num != 0);

    return 0;
}

Flujo

Usuario ingresa: 2
→ pf1 = muestra_2
→ pf1()
→ Output: "Muestra 2"

Ensamblador del Switch

; Comparar con 1
cmp     DWORD PTR num[rsp], 1
jne     SHORT $L2

; Case 1: pf1 = muestra_1
lea     rax, OFFSET FLAT:muestra_1
mov     QWORD PTR pf1[rsp], rax
jmp     SHORT $L_exit

$L2:
; Case 2: pf1 = muestra_2
cmp     DWORD PTR num[rsp], 2
jne     SHORT $L3
lea     rax, OFFSET FLAT:muestra_2
mov     QWORD PTR pf1[rsp], rax
; ... y así con 3 y 4

$L_exit:
; Llamar función seleccionada
call    QWORD PTR pf1[rsp]

Ejemplo 3: Array de Punteros a Funciones

#include <iostream>
using namespace std;

void f1(int n) { while (n--) cout << "Muestra 1 "; cout << endl; }
void f2(int n) { while (n--) cout << "Muestra 2 "; cout << endl; }
void f3(int n) { while (n--) cout << "Muestra 3 "; cout << endl; }
void f4(int n) { while (n--) cout << "Muestra 4 "; cout << endl; }

int main() {
    void (*pf1[4])(int);        // Array de 4 punteros a funciones

    // Inicializar array
    pf1[0] = f1;
    pf1[1] = f2;
    pf1[2] = f3;
    pf1[3] = f4;

    int num, valores;

    do {
        cout << "Introduce función (1-4, 0 para salir): ";
        cin >> num;

        if (num >= 1 && num <= 4) {
            cout << "Introduce cantidad (1-10): ";
            cin >> valores;

            if (valores > 0 && valores < 11) {
                pf1[num - 1](valores);  // Llamar con índice ajustado
            }
        }
    } while (num != 0);

    return 0;
}

Estructura en Memoria

pf1 → [Offset f1] → Código de f1
      [Offset f2] → Código de f2
      [Offset f3] → Código de f3
      [Offset f4] → Código de f4

Cada elemento ocupa 8 bytes (QWORD en x64).

Inicialización en Ensamblador

; pf1[0] = f1
lea     rcx, OFFSET FLAT:f1
mov     QWORD PTR pf1[rsp+0], rcx

; pf1[1] = f2
lea     rcx, OFFSET FLAT:f2
mov     QWORD PTR pf1[rsp+8], rcx

; pf1[2] = f3
lea     rcx, OFFSET FLAT:f3
mov     QWORD PTR pf1[rsp+16], rcx

; pf1[3] = f4
lea     rcx, OFFSET FLAT:f4
mov     QWORD PTR pf1[rsp+24], rcx

Offsets: 0, 8, 16, 24 (incrementos de 8 bytes)

Llamada con Índice

; Calcular pf1[num - 1]
mov     eax, DWORD PTR num[rsp]
dec     eax                         ; num - 1
movsxd  rax, eax                    ; Extender a 64 bits
imul    rax, 8                      ; Multiplicar por 8 (tamaño de puntero)

; Obtener dirección de función
lea     rcx, QWORD PTR pf1[rsp]
add     rcx, rax                    ; pf1 + (num-1)*8
mov     rax, QWORD PTR [rcx]        ; Cargar puntero a función

; Pasar argumento y llamar
mov     ecx, DWORD PTR valores[rsp]
call    rax                         ; Call indirecto

Cálculo: pf1[num-1] = *(pf1 + (num-1) * 8)

Utilidad de Punteros a Funciones

  1. Callbacks: Pasar funciones como parámetros
  2. Polimorfismo en C: Simular comportamiento orientado a objetos
  3. Tablas de dispatch: Jump tables para optimización
  4. Plugins/Extensiones: Cargar funcionalidad dinámica
  5. State machines: Cambiar comportamiento según estado

Ejemplo: Función que Recibe Puntero a Función

void ejecutar(int (*func)(int), int valor) {
    int resultado = func(valor);
    cout << "Resultado: " << resultado << endl;
}

int doble(int x) { return x * 2; }
int triple(int x) { return x * 3; }

int main() {
    ejecutar(doble, 5);   // Output: Resultado: 10
    ejecutar(triple, 5);  // Output: Resultado: 15
    return 0;
}

Resumen de Técnicas Avanzadas

Comparación de Métodos para Argumentos Variables

Método Ventajas Desventajas
Centinela (0) Simple, automático Requiere valor especial, no puede usar 0 como dato
Tipo en arg fijo Distingue categorías No sabe cantidad exacta
String de formato Preciso, autodescriptivo Requiere formato correcto
Count explícito Más seguro, flexible Requiere contar argumentos

Convenciones x64 - Resumen

Registros para argumentos:
1º → RCX
2º → RDX
3º → R8
4º → R9
5+ → Stack ([RSP+32], [RSP+40], ...)

Shadow Space: [RSP+8] a [RSP+32] (siempre reservado)

Patterns Comunes en Reversing

Puntero a función:

lea     rax, OFFSET FLAT:funcion
mov     QWORD PTR ptr[rsp], rax
call    QWORD PTR ptr[rsp]

Array de punteros a funciones:

lea     rax, QWORD PTR array[rsp]
mov     rcx, index
mov     rax, QWORD PTR [rax+rcx*8]
call    rax

Variadic con formato:

; Loop por strlen(formato)
mov     rcx, formato
call    strlen
; Switch por formato[i]
movzx   eax, BYTE PTR [formato+i]
cmp     al, 'c'
je      case_char
cmp     al, 'i'
je      case_int