Saltar a contenido

Paso de Parámetros: Valor vs Referencia en C++

Índice


Introducción

El paso de parámetros a funciones es fundamental en C++ y crítico para reversing. Existen dos formas principales:

  1. Por valor (by value): Se copia el argumento a una variable local de la función.
  2. Por referencia (by reference): Se pasa la dirección del objeto original; trabajamos con el mismo objeto.

Concepto clave para reversing:

En C++, cuando pasas por referencia, conceptualmente trabajas con el mismo objeto. A bajo nivel, esto se implementa con punteros, pero debes mantener el concepto de alto nivel: modificas el objeto original, no una copia.


Parte 1: Paso por Valor (By Value)

Concepto

Cuando pasas un parámetro por valor, se copia el argumento a una variable local de la función. Cualquier modificación dentro de la función no afecta al objeto original.

Código Ejemplo

#include <iostream>
using namespace std;

int funcion(int n, int m);

int main() {
    int a = 10;
    int b = 20;

    cout << "Antes: a = " << a << ", b = " << b << endl;
    cout << "Resultado: " << funcion(a, b) << endl;
    cout << "Después: a = " << a << ", b = " << b << endl; // Sin cambios

    return 0;
}

int funcion(int n, int m) {
    n = n + 2;  // Modificación local
    m = m - 5;  // Modificación local
    return n + m;
}

Salida:

Antes: a = 10, b = 20
Resultado: 27
Después: a = 10, b = 20

Análisis en Ensamblador (x64 Windows)

; main():
mov     dword ptr [rsp+20h], 10    ; a = 10
mov     dword ptr [rsp+24h], 20    ; b = 20

; Preparar argumentos (Win64: RCX, RDX)
mov     edx, [rsp+24h]              ; EDX = b (20)
mov     ecx, [rsp+20h]              ; ECX = a (10)
call    funcion

; Al volver, a y b NO han cambiado
mov     edx, [rsp+20h]              ; a sigue siendo 10
mov     ecx, [rsp+24h]              ; b sigue siendo 20

; funcion(int n, int m):
mov     [rsp+8], edx                ; Guardar m en stack
mov     [rsp+10h], ecx              ; Guardar n en stack
; n y m son COPIAS locales

mov     eax, [rsp+10h]              ; EAX = n
add     eax, 2                      ; n += 2
mov     [rsp+10h], eax              ; Escribir n local

mov     eax, [rsp+8]                ; EAX = m
sub     eax, 5                      ; m -= 5
mov     [rsp+8], eax                ; Escribir m local

mov     eax, [rsp+10h]
add     eax, [rsp+8]                ; return n + m
ret

Claves del reconocimiento:

  • Los argumentos se copian a variables locales en el stack ([rsp+8], [rsp+10h]).
  • Las modificaciones no salen de la función.
  • Al retornar, las variables originales conservan su valor.

Parte 2: Referencias en C++

Concepto

Una referencia es un alias (nombre alternativo) para un objeto existente. No es un puntero; es el mismo objeto con otro nombre.

Sintaxis:

int a = 10;
int &r = a;  // r es una referencia a a

r = 20;      // Modifica a directamente
cout << a;   // Imprime 20

Características

Aspecto Referencia Puntero
Declaración int &r = a; int *p = &a;
Inicialización Obligatoria (no puede ser nula) Opcional (puede ser nullptr)
Acceso al valor r (directo) *p (desreferencia)
Modificación No puede cambiar de objeto Puede apuntar a otro objeto
A bajo nivel Puntero constante Puntero

Ejemplo Completo

#include <iostream>
using namespace std;

int main() {
    int a = 10;
    int &r = a;  // r es alias de a

    cout << "a = " << a << ", r = " << r << endl;  // 10, 10

    r = 20;      // Modificar a través de r
    cout << "a = " << a << ", r = " << r << endl;  // 20, 20

    a = 30;      // Modificar a directamente
    cout << "a = " << a << ", r = " << r << endl;  // 30, 30

    return 0;
}

A bajo nivel, el compilador mantiene una tabla donde r apunta a la misma dirección de a. No se reserva memoria adicional para r.


Parte 3: Paso por Referencia (By Reference)

Concepto

Cuando pasas un parámetro por referencia, trabajas con el mismo objeto que está en la función llamadora. Cualquier modificación sí afecta al objeto original.

Sintaxis:

void funcion(int &n, int &m);  // Parámetros por referencia

Código Ejemplo

#include <iostream>
using namespace std;

int funcion(int &n, int &m);

int main() {
    int a = 10;
    int b = 20;

    cout << "Antes: a = " << a << ", b = " << b << endl;
    cout << "Resultado: " << funcion(a, b) << endl;
    cout << "Después: a = " << a << ", b = " << b << endl; // ¡CON cambios!

    return 0;
}

int funcion(int &n, int &m) {
    n = n + 2;  // Modifica a directamente
    m = m - 5;  // Modifica b directamente
    return n + m;
}

Salida:

Antes: a = 10, b = 20
Resultado: 27
Después: a = 12, b = 15

Análisis en Ensamblador (x64 Windows)

Importante: A bajo nivel, las referencias se implementan como punteros.

; main():
mov     dword ptr [rsp+20h], 10    ; a = 10
mov     dword ptr [rsp+24h], 20    ; b = 20

; Pasar DIRECCIONES (no valores)
lea     rdx, [rsp+24h]              ; RDX = &b
lea     rcx, [rsp+20h]              ; RCX = &a
call    funcion

; Al volver, a y b SÍ han cambiado
mov     edx, [rsp+20h]              ; a ahora es 12
mov     ecx, [rsp+24h]              ; b ahora es 15

; funcion(int &n, int &m):
mov     [rsp+8], rdx                ; Guardar &m
mov     [rsp+10h], rcx              ; Guardar &n

; Modificar n (a través del puntero)
mov     rax, [rsp+10h]              ; RAX = &n
mov     ecx, [rax]                  ; ECX = *n (valor de a)
add     ecx, 2                      ; n += 2
mov     [rax], ecx                  ; Escribir en a (directo)

; Modificar m (a través del puntero)
mov     rax, [rsp+8]                ; RAX = &m
mov     edx, [rax]                  ; EDX = *m (valor de b)
sub     edx, 5                      ; m -= 5
mov     [rax], edx                  ; Escribir en b (directo)

mov     eax, [rsp+10h]
mov     eax, [rax]
mov     rcx, [rsp+8]
add     eax, [rcx]                  ; return n + m
ret

Claves del reconocimiento:

  • Se pasan direcciones (lea → dirección, no valor).
  • Dentro de la función, siempre hay desreferencia (mov [rax]).
  • Las modificaciones afectan memoria fuera del stack frame de la función.

Restricción Importante

No puedes pasar constantes literales por referencia:

funcion(10, 20);  // ❌ Error de compilación

Razón: Una referencia debe apuntar a un objeto modificable en memoria. Las constantes no tienen dirección mutable.


Parte 4: Punteros como Parámetros

Concepto

Los punteros son objetos como cualquier otro. Si pasas un puntero por valor, se copia la dirección, pero:

  • El puntero (la dirección) es local; modificarlo no afecta al puntero original.
  • El contenido apuntado sí se modifica, porque ambos punteros apuntan a la misma dirección.

Código Ejemplo

#include <iostream>
using namespace std;

void funcion(int *q);

int main() {
    int a = 100;
    int *p = &a;

    cout << "Antes: a = " << a << ", *p = " << *p << endl;
    funcion(p);
    cout << "Después: a = " << a << ", *p = " << *p << endl;

    funcion(&a);  // También válido
    cout << "Final: a = " << a << endl;

    return 0;
}

void funcion(int *q) {
    *q += 50;  // Modifica el contenido (a)
    q++;       // Modifica el puntero LOCAL (no afecta p)
}

Salida:

Antes: a = 100, *p = 100
Después: a = 150, *p = 150
Final: a = 200

Análisis en Ensamblador

; main():
mov     dword ptr [rsp+20h], 100   ; a = 100
lea     rax, [rsp+20h]              ; RAX = &a
mov     [rsp+28h], rax              ; p = &a

; Llamar funcion(p)
mov     rcx, [rsp+28h]              ; RCX = p (dirección de a)
call    funcion

; p NO cambió (sigue apuntando a a)
mov     rax, [rsp+28h]              ; p sigue siendo &a
mov     eax, [rax]                  ; *p = 150

; funcion(int *q):
mov     [rsp+8], rcx                ; q = p (copia de dirección)

; Modificar contenido
mov     rax, [rsp+8]                ; RAX = q
mov     ecx, [rax]                  ; ECX = *q (100)
add     ecx, 50                     ; *q += 50
mov     [rax], ecx                  ; Escribir 150 en a

; Modificar puntero LOCAL
mov     rax, [rsp+8]                ; RAX = q
add     rax, 4                      ; q++ (LOCAL)
mov     [rsp+8], rax                ; q ahora apunta a +4

; Al retornar, q desaparece; p NO cambió
ret

Claves del reconocimiento:

  • Se pasa la dirección en RCX.
  • Dentro de la función, q es una copia de p.
  • Las escrituras a través de [rax] afectan a a.
  • El incremento de q (add rax, 4) es local; p no cambia.

Parte 5: Punteros Pasados por Referencia

Concepto

Si quieres que las modificaciones al puntero mismo (no solo su contenido) se conserven, debes pasar el puntero por referencia.

Sintaxis:

void funcion(int* &q);  // Referencia a puntero

A bajo nivel, esto se implementa como puntero a puntero (doble indirección).

Código Ejemplo

#include <iostream>
using namespace std;

void funcion(int* &q);

int main() {
    int a = 100;
    int *p = &a;

    cout << "Antes: a = " << a << ", *p = " << *p << endl;
    cout << "Dirección de a: " << &a << ", p = " << p << endl;

    funcion(p);

    cout << "Después: a = " << a << ", *p = " << *p << endl;
    cout << "Dirección de a: " << &a << ", p = " << p << endl;
    // ¡p cambió! Ya no apunta a a

    return 0;
}

void funcion(int* &q) {
    *q += 50;  // Modifica contenido (a = 150)
    q++;       // Modifica p DIRECTAMENTE (no es copia)
}

Salida:

Antes: a = 100, *p = 100
Dirección de a: 0x00000000007BFE20, p = 0x00000000007BFE20
Después: a = 150, *p = 0
Dirección de a: 0x00000000007BFE20, p = 0x00000000007BFE24

Nota: *p ahora es 0 porque p apunta a &a + 4 (memoria no inicializada).

Análisis en Ensamblador (Doble Indirección)

; main():
mov     dword ptr [rsp+20h], 100   ; a = 100
lea     rax, [rsp+20h]              ; RAX = &a
mov     [rsp+28h], rax              ; p = &a

; Pasar DIRECCIÓN DE p (no el valor de p)
lea     rcx, [rsp+28h]              ; RCX = &p (puntero a puntero)
call    funcion

; p SÍ cambió (ahora apunta a a + 4)
mov     rax, [rsp+28h]              ; p = 0x...0x24
mov     eax, [rax]                  ; *p = 0 (basura)

; funcion(int* &q):
mov     [rsp+8], rcx                ; Guardar &p

; Modificar contenido de a (doble indirección)
mov     rax, [rsp+8]                ; RAX = &p
mov     rax, [rax]                  ; RAX = *(&p) = p = &a
mov     ecx, [rax]                  ; ECX = *p = a = 100
add     ecx, 50                     ; a += 50
mov     rax, [rsp+8]                ; RAX = &p
mov     rax, [rax]                  ; RAX = p
mov     [rax], ecx                  ; Escribir 150 en a

; Modificar p DIRECTAMENTE
mov     rax, [rsp+8]                ; RAX = &p
mov     rcx, [rax]                  ; RCX = p
add     rcx, 4                      ; p += 4
mov     [rax], rcx                  ; Escribir nuevo p (afecta main)

ret

Claves del reconocimiento:

  • Se pasa &p (lea rcx, [rsp+28h] → dirección de la variable puntero).
  • Dentro de la función, hay doble desreferencia:
  • mov rax, [rsp+8]RAX = &p
  • mov rax, [rax]RAX = p = &a
  • mov ecx, [rax]ECX = a
  • Las modificaciones a p (mov [rax], rcx) afectan al p en main().

Concepto C++ vs Bajo Nivel

Concepto C++ Implementación Bajo Nivel
Referencia a puntero (int* &q) Puntero a puntero (int **q)
q es el mismo puntero que p q = &p (dirección de p)
Modificar q modifica p Doble desreferencia: *q = nuevo_valor

Importante: En reversing, si ves doble indirección y paso de dirección de puntero, asume paso por referencia en el código original.


Parte 6: Arrays como Parámetros

Concepto

Cuando pasas un array como parámetro, realmente pasas un puntero al primer elemento. Por eso, las modificaciones en los elementos son permanentes.

Sintaxis:

void funcion(int tabla[]);      // Equivalente a int *tabla
void funcion(int tabla[][M]);   // Array 2D: primera dimensión variable

Problemas con Arrays Multidimensionales

  • Si pasas solo el nombre del array (tabla), no tienes información del tamaño.
  • Solución: Pasar dimensiones como parámetros adicionales.

Código Ejemplo

#include <iostream>
using namespace std;

#define N 10
#define M 20

void funcion(int tabla[][M], int n);

int main() {
    int Tabla[N][M];
    funcion(Tabla, N);
    return 0;
}

void funcion(int tabla[][M], int n) {
    cout << "Total elementos: " << n * M << endl;
    tabla[2][3] = 99;  // Modificación permanente
}

Acceso Manual con Puntero

Si quieres máxima flexibilidad (arrays de cualquier dimensión), usa puntero y calcula offsets manualmente:

void funcion(int *tabla, int n, int m) {
    // Acceder a tabla[x][y]:
    // tabla[x * m + y]
}

int main() {
    int Tabla[10][20];
    funcion((int*)Tabla, 10, 20);
}

Fórmula general para tabla[n][m][o][p]:

elemento = tabla[p + o*P + m*O*P + n*M*O*P];

Parte 7: Estructuras como Parámetros

Concepto

Las estructuras pueden pasarse por valor o por referencia. Por eficiencia, si la estructura es grande, siempre pasa por referencia (o puntero) para evitar copiar miles de bytes en el stack.

Código Ejemplo

struct Persona {
    char nombre[50];
    int edad;
    float altura;
};

// Paso por valor (COPIA toda la estructura)
void funcion1(Persona p) {
    p.edad = 30;  // No afecta al original
}

// Paso por referencia (sin copia)
void funcion2(Persona &p) {
    p.edad = 30;  // SÍ afecta al original
}

// Paso por puntero (sin copia)
void funcion3(Persona *p) {
    p->edad = 30;  // SÍ afecta al original
}

Análisis en reversing:

  • Por valor: Verás rep movsb o múltiples mov copiando datos al stack.
  • Por referencia/puntero: Solo pasa 8 bytes (dirección en x64).

Parte 8: Funciones que Devuelven Referencias

Concepto

Una función puede devolver una referencia, lo que permite usar la llamada como un lvalue (lado izquierdo de asignación).

Sintaxis:

int& funcion();  // Devuelve referencia a int

Código Ejemplo

#include <iostream>
using namespace std;

int& Acceso(int* array, int indice);

int main() {
    int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    Acceso(array, 3)++;                          // array[3] = 4
    Acceso(array, 6) = Acceso(array, 4) + 10;    // array[6] = 14

    cout << "array[3] = " << array[3] << endl;   // 4
    cout << "array[6] = " << array[6] << endl;   // 14

    return 0;
}

int& Acceso(int* vector, int indice) {
    return vector[indice];  // Devuelve REFERENCIA al elemento
}

Clave: La función devuelve una dirección (en RAX), que se puede usar directamente para lectura/escritura.

Análisis en Ensamblador

; Acceso(array, 3)++
lea     rcx, [rsp+20h]              ; RCX = array
mov     edx, 3                      ; EDX = indice
call    Acceso
; RAX = &array[3]
mov     ecx, [rax]                  ; ECX = array[3]
inc     ecx                         ; array[3]++
mov     [rax], ecx                  ; Escribir de vuelta

; Acceso(int* vector, int indice):
imul    edx, edx, 4                 ; indice * 4
add     rcx, rdx                    ; vector + offset
mov     rax, rcx                    ; Devolver dirección
ret

Reconocimiento: La función devuelve una dirección (no un valor), y esa dirección se usa para modificar memoria.


Resumen y Reconocimiento en Reversing

Tabla Comparativa

Tipo de Paso Sintaxis A Bajo Nivel Modificable Uso Común
Por valor int n Copia en stack ❌ No Tipos pequeños
Por referencia int &n Pasa dirección ✅ Sí Objetos grandes, modificación
Puntero por valor int *p Copia dirección Contenido ✅, Puntero ❌ Manipular contenido
Puntero por referencia int* &p Puntero a puntero Contenido ✅, Puntero ✅ Modificar puntero mismo
Array int arr[] Puntero al primer elemento ✅ Sí Siempre por puntero
Estructura pequeña struct S Copia en stack ❌ No Estructuras < 16 bytes
Estructura grande struct S& o struct S* Pasa dirección ✅ Sí Estructuras > 16 bytes

Patrones de Reconocimiento en IDA

Paso por Valor

mov     ecx, [rsp+20h]              ; Cargar valor
call    funcion
; Dentro:
mov     [rsp+8], ecx                ; Copiar a variable local

Clave: Copia del valor; modificaciones no salen de la función.

Paso por Referencia

lea     rcx, [rsp+20h]              ; Pasar DIRECCIÓN
call    funcion
; Dentro:
mov     [rsp+8], rcx                ; Guardar dirección
mov     rax, [rsp+8]                ; Cargar dirección
mov     dword ptr [rax], 123        ; DESREFERENCIAR y escribir

Clave: lea para obtener dirección; siempre hay desreferencia dentro.

Puntero a Puntero (Referencia a Puntero)

lea     rcx, [rsp+28h]              ; RCX = &puntero
call    funcion
; Dentro:
mov     rax, [rsp+8]                ; RAX = &puntero
mov     rax, [rax]                  ; RAX = puntero (DOBLE INDIRECCIÓN)
mov     ecx, [rax]                  ; ECX = contenido

Clave: Doble desreferencia ([rax] dos veces); paso de &puntero.

Checklist para Reversing

  • ¿Ves lea + dirección antes de call? → Probablemente por referencia o puntero.
  • ¿Hay desreferencia [reg] dentro de la función? → Puntero o referencia.
  • ¿Doble desreferencia ([reg][result])? → Puntero a puntero (referencia a puntero).
  • ¿Modificaciones locales sin escribir fuera del stack? → Por valor.
  • ¿Estructura grande copiada con rep movsb? → Por valor (ineficiente).
  • ¿Solo se pasa dirección de estructura? → Por referencia/puntero (eficiente).

Consejos de Narvaja

Conceptualmente, en C++ trabajas con el mismo objeto cuando pasas por referencia. A bajo nivel es un puntero, pero mentalmente debes separar: en C++ es el mismo objeto, en ensamblador es una dirección.

Al reversear, si ves doble indirección, piensa: "Estoy modificando un puntero, no solo su contenido". Eso indica paso de puntero por referencia.

Siempre trata de mantener el concepto de C++ al analizar código. Si sabes que es una referencia, piensa "modifico el original", aunque veas punteros en ensamblador.


Notas Adicionales

¿Por Qué Usar Referencias en C++?

  1. Eficiencia: Evita copiar objetos grandes.
  2. Claridad: Sintaxis más limpia que punteros (obj.campo vs obj->campo).
  3. Seguridad: No pueden ser nullptr (obligatorio inicializar).

Diferencias con C

En C no existen referencias. Para pasar por referencia, usas punteros:

void funcion(int *n) {  // En C, esto es "por referencia"
    *n = 10;
}

int main() {
    int a = 5;
    funcion(&a);  // Debes pasar &a explícitamente
}

En C++, con referencias:

void funcion(int &n) {  // En C++, paso por referencia
    n = 10;
}

int main() {
    int a = 5;
    funcion(a);  // NO necesitas &a
}

Optimizaciones y Reversing

  • Sin optimizaciones (/Od): Verás todas las copias y variables locales.
  • Con optimizaciones (/O2): El compilador puede:
  • Pasar argumentos en registros sin tocar el stack.
  • Eliminar copias innecesarias.
  • Inlinear funciones pequeñas.

Para aprender, usa /Od y símbolos. Luego, compara con /O2 para ver las transformaciones.


Ejemplos de Reconocimiento

Ejemplo 1: Detectar Paso por Referencia

Código sospechoso en IDA:

lea     rcx, [rbp+var_10]
call    modificar

Conclusión: Se pasa la dirección de var_10, no su valor. Probablemente paso por referencia.

Ejemplo 2: Detectar Doble Indirección

Código en IDA:

mov     rax, [rsp+puntero]
mov     rax, [rax]
mov     edx, [rax]

Conclusión:

  1. rax = puntero (dirección de otra variable puntero)
  2. rax = *puntero (puntero original)
  3. edx = **puntero (contenido final)

Esto indica puntero a puntero, típico de paso de puntero por referencia.

Ejemplo 3: Detectar Paso por Valor

Código en IDA:

mov     ecx, [rbp+var_4]
call    funcion

; Dentro de funcion:
mov     [rsp+8], ecx
add     dword ptr [rsp+8], 10
; No hay escritura fuera del stack