Paso de Parámetros: Valor vs Referencia en C++¶
Índice¶
- Introducción
- Parte 1: Paso por Valor (By Value)
- Parte 2: Referencias en C++
- Parte 3: Paso por Referencia (By Reference)
- Parte 4: Punteros como Parámetros
- Parte 5: Punteros Pasados por Referencia
- Parte 6: Arrays como Parámetros
- Parte 7: Estructuras como Parámetros
- Parte 8: Funciones que Devuelven Referencias
- Resumen y Reconocimiento en Reversing
Introducción¶
El paso de parámetros a funciones es fundamental en C++ y crítico para reversing. Existen dos formas principales:
- Por valor (by value): Se copia el argumento a una variable local de la función.
- 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:
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:
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:
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:
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:
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,
qes una copia dep. - Las escrituras a través de
[rax]afectan aa. - El incremento de
q(add rax, 4) es local;pno 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:
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 = &pmov rax, [rax]→RAX = p = &amov ecx, [rax]→ECX = a- Las modificaciones a
p(mov [rax], rcx) afectan alpenmain().
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]:
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 movsbo múltiplesmovcopiando 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:
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¶
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 decall? → 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++?¶
- Eficiencia: Evita copiar objetos grandes.
- Claridad: Sintaxis más limpia que punteros (
obj.campovsobj->campo). - 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:
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:
Conclusión:
rax = puntero(dirección de otra variable puntero)rax = *puntero(puntero original)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: