Seguir el paso de direcciones a funciones y la construcción de estructuras en el heap.
Notas de la práctica
Avanzar un puntero sobre un objeto escalar no autoriza a acceder a otro objeto contiguo. Para practicar aritmética de punteros usa los ejemplos de arrays. El laboratorio pointer-argument permite seguir una modificación por referencia sin depender de accesos fuera del objeto.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 10 --example pointer-argumentEstoy siguiendo «Aritmética de punteros y estructuras dinámicas» del curso de Lobera. Seguir el paso de direcciones a funciones y la construcción de estructuras en el heap. Dame primero una pista. Después contrasta la explicación con las instrucciones y guarda las evidencias que hayamos comprobado.
El agente y la consola comparten el estado de radare2. Preparación y funcionamiento.
Los punteros y la memoria dinámica en general son temas muy relevantes que desempeñan un papel fundamental en los programas modernos y merecen más de una entrada. Si quieres adentrarte en la investigación de vulnerabilidades o en la escritura de exploits, entender a la perfección cómo se gestiona la memoria en el software moderno es imprescindible. Si tu objetivo es la investigación en seguridad o el reversing, la capacidad de interpretar qué está haciendo el programa (o qué está haciendo la CPU) en un momento dado y en un contexto concreto es fundamental.
Avanzar un puntero
Empecemos con el siguiente programa:
#include <stdio.h>
#include <stdlib.h>
main() {
int * ipoint;
ipoint = (int *) malloc (sizeof(int));
*ipoint = 3;
printf ("%p \n",ipoint);
printf ("%i\n",*ipoint);
ipoint ++;
printf ("%p\n",ipoint);
printf ("%i\n",*ipoint);
getchar();
}
El programa comienza reservando en el heap un espacio de memoria del tamaño de un int; se declara un puntero a ese espacio. Dicho espacio se inicializa con el valor 3 en decimal. Observa que * se usa para referenciar el espacio apuntado por el puntero: si no se usa *, ipoint = dirección. A continuación se imprime la dirección de memoria y luego el valor. Después el programa ejecuta ipoint++. ¿Qué ocurrirá? ¿3+1? ¿O dirección de memoria +1?
Ya deberías saberlo... pero veámoslo dentro de radare2:
En este ejemplo ya sabemos qué buscamos, pero radare2 ofrece formas muy útiles de presentar información interesante que permiten llegar antes al punto clave. Probemos con agc:
[0x55a93cbe56da]> agc
┌────────────────────┐
│ main │
└────────────────────┘
v
│
┌─────────────────────────│
│ └─────────────────────────┐
│ │ │
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ sym.imp.malloc │ │ sym.imp.printf │ │ sym.imp.getchar │
└────────────────────┘ └────────────────────┘ └────────────────────┘
agc analiza el grafo de llamadas, por lo que en este caso podemos identificar rápidamente que dentro de main se llaman malloc, printf y getchar. Esto resulta útil cuando existen «circuitos» o «caminos» interesantes entre funciones que queremos analizar.
Continuemos con pdf:
[0x55a93cbe56da]> pdf
; DATA XREF from entry0 @ 0x55a93cbe55ed
┌ 147: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_8h @ rbp-0x8
│ 0x55a93cbe56da 55 push rbp
│ 0x55a93cbe56db 4889e5 mov rbp, rsp
│ 0x55a93cbe56de 4883ec10 sub rsp, 0x10
│ 0x55a93cbe56e2 bf04000000 mov edi, 4
│ 0x55a93cbe56e7 e8c4feffff call sym.imp.malloc ; void *malloc(size_t size)
│ 0x55a93cbe56ec 488945f8 mov qword [var_8h], rax
│ 0x55a93cbe56f0 488b45f8 mov rax, qword [var_8h]
│ 0x55a93cbe56f4 c70003000000 mov dword [rax], 3
│ 0x55a93cbe56fa 488b45f8 mov rax, qword [var_8h]
│ 0x55a93cbe56fe 4889c6 mov rsi, rax
│ 0x55a93cbe5701 488d3dec0000. lea rdi, str.p ; 0x55a93cbe57f4 ; "%p \n"
│ 0x55a93cbe5708 b800000000 mov eax, 0
│ 0x55a93cbe570d e87efeffff call sym.imp.printf ; int printf(const char *format)
│ 0x55a93cbe5712 488b45f8 mov rax, qword [var_8h]
│ 0x55a93cbe5716 8b00 mov eax, dword [rax]
│ 0x55a93cbe5718 89c6 mov esi, eax
│ 0x55a93cbe571a 488d3dd80000. lea rdi, [0x55a93cbe57f9] ; "%i\n"
│ 0x55a93cbe5721 b800000000 mov eax, 0
│ 0x55a93cbe5726 e865feffff call sym.imp.printf ; int printf(const char *format)
│ 0x55a93cbe572b 488345f804 add qword [var_8h], 4
│ 0x55a93cbe5730 488b45f8 mov rax, qword [var_8h]
│ 0x55a93cbe5734 4889c6 mov rsi, rax
│ 0x55a93cbe5737 488d3dbf0000. lea rdi, [0x55a93cbe57fd] ; "%p\n"
│ 0x55a93cbe573e b800000000 mov eax, 0
│ 0x55a93cbe5743 e848feffff call sym.imp.printf ; int printf(const char *format)
│ 0x55a93cbe5748 488b45f8 mov rax, qword [var_8h]
│ 0x55a93cbe574c 8b00 mov eax, dword [rax]
│ 0x55a93cbe574e 89c6 mov esi, eax
│ 0x55a93cbe5750 488d3da20000. lea rdi, [0x55a93cbe57f9] ; "%i\n"
│ 0x55a93cbe5757 b800000000 mov eax, 0
│ 0x55a93cbe575c e82ffeffff call sym.imp.printf ; int printf(const char *format)
│ 0x55a93cbe5761 e83afeffff call sym.imp.getchar ; int getchar(void)
│ 0x55a93cbe5766 b800000000 mov eax, 0
│ 0x55a93cbe576b c9 leave
└ 0x55a93cbe576c c3 ret
[0x55a93cbe56da]>
Como de costumbre, diseccionemos la función en varias partes; la primera llama a malloc:
│ 0x55a93cbe56de 4883ec10 sub rsp, 0x10
│ 0x55a93cbe56e2 bf04000000 mov edi, 4
│ 0x55a93cbe56e7 e8c4feffff call sym.imp.malloc ; void *malloc(size_t size)
│ 0x55a93cbe56ec 488945f8 mov qword [var_8h], rax
Como vemos, el programa reserva espacio en la pila para variables estáticas y reserva 4 bytes mediante malloc, almacenando la dirección resultante (el puntero) en var_8h.
│ 0x55a93cbe56fe 4889c6 mov rsi, rax
│ 0x55a93cbe5701 488d3dec0000. lea rdi, str.p ; 0x55a93cbe57f4 ; "%p \n"
│ 0x55a93cbe5708 b800000000 mov eax, 0
│ 0x55a93cbe570d e87efeffff call sym.imp.printf ; int printf(const char *format)
A continuación se imprime el contenido de rax. Como rax contiene el puntero devuelto por malloc, lo que se imprimirá es un puntero:
│ 0x55a93cbe5712 488b45f8 mov rax, qword [var_8h]
│ 0x55a93cbe5716 8b00 mov eax, dword [rax]
│ 0x55a93cbe5718 89c6 mov esi, eax
│ 0x55a93cbe571a 488d3dd80000. lea rdi, [0x55a93cbe57f9] ; "%i\n"
│ 0x55a93cbe5721 b800000000 mov eax, 0
│ 0x55a93cbe5726 e865feffff call sym.imp.printf ; int printf(const char *format)
Después se llama de nuevo a printf, pero esta vez, fíjate en la segunda línea: se pasa el CONTENIDO en lugar de una dirección. Veamos qué ocurre ahora:
│ 0x55a93cbe572b 488345f804 add qword [var_8h], 4
│ 0x55a93cbe5730 488b45f8 mov rax, qword [var_8h]
│ 0x55a93cbe5734 4889c6 mov rsi, rax
│ 0x55a93cbe5737 488d3dbf0000. lea rdi, [0x55a93cbe57fd] ; "%p\n"
│ 0x55a93cbe573e b800000000 mov eax, 0
│ 0x55a93cbe5743 e848feffff call sym.imp.printf ; int printf(const char *format)
Se suma 4 al contenido de var_8h. Como el contenido de var_8h es un puntero, el 3 no se convierte en 4 ni en 7 ni en ningún otro valor. Ten en cuenta que al hacer pointer++ con un puntero a int, ++ equivale a +4, ya que el tamaño de un int es 4 bytes en este caso. Tras eso, se imprime el puntero. El último bloque de código es el siguiente:
│ 0x55a93cbe5748 488b45f8 mov rax, qword [var_8h]
│ 0x55a93cbe574c 8b00 mov eax, dword [rax]
│ 0x55a93cbe574e 89c6 mov esi, eax
│ 0x55a93cbe5750 488d3da20000. lea rdi, [0x55a93cbe57f9] ; "%i\n"
│ 0x55a93cbe5757 b800000000 mov eax, 0
│ 0x55a93cbe575c e82ffeffff call sym.imp.printf ; int printf(const char *format)
Por último, el programa imprime el contenido de lo apuntado por var_8h, pero como var_8h fue actualizado, ahora apunta a una dirección de memoria diferente, 4 bytes más adelante respecto a la original, por lo que se imprimirá cualquier valor aleatorio en lugar de nuestro 3. Depura el programa por tu cuenta y compruébalo como ejercicio.
Modificar el valor apuntado
Ese ejemplo era muy básico. Pasemos a otro igualmente sencillo para asegurarnos de que no se nos escapa nada:
#include <stdio.h>
#include <stdlib.h>
main() {
int * spoint;
spoint = (int *) malloc (sizeof(int));
*spoint = 3;
printf ("%p \n",spoint);
printf ("%d\n",*spoint);
(*spoint) ++;
printf ("%d\n",*spoint);
getchar();
}
La diferencia está aquí: (*spoint)++. Acostúmbrate a esta notación, ya que la verás con frecuencia al revisar código C/C++ o al trabajar con código de bajo nivel en general. (*pointer) se usa para referenciar el contenido real de un puntero, por lo que hacerlo aquí actualizará efectivamente spoint y convertirá ese 3 en un 4.
Es hora de depurarlo, pero espera: este programa es casi idéntico al anterior; parece una versión «parcheada». El parcheo es muy habitual en el software moderno: los desarrolladores parchean y redistribuyen su software, y en ocasiones esos parches corrigen vulnerabilidades de seguridad.
Podemos usar binary diffing para comparar los dos ejecutables así:
red@blue:~/c/chapter10$ radiff2 -A -a x86 -C ipoint secondpointer
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
sym._init 23 0x560 | MATCH (1.000000) | 0x560 23 sym._init
sym.imp.printf 6 0x590 | MATCH (1.000000) | 0x590 6 sym.imp.printf
sym.imp.getchar 6 0x5a0 | MATCH (1.000000) | 0x5a0 6 sym.imp.getchar
sym.imp.malloc 6 0x5b0 | MATCH (1.000000) | 0x5b0 6 sym.imp.malloc
entry0 42 0x5d0 | MATCH (0.952381) | 0x5d0 42 entry0
sym.deregister_tm_clones 50 0x600 | MATCH (1.000000) | 0x600 50 sym.deregister_tm_clones
sym.register_tm_clones 66 0x640 | MATCH (1.000000) | 0x640 66 sym.register_tm_clones
sym.__do_global_dtors_aux 58 0x690 | MATCH (1.000000) | 0x690 58 sym.__do_global_dtors_aux
entry.init0 10 0x6d0 | MATCH (1.000000) | 0x6d0 10 entry.init0
main 147 0x6da | MATCH (0.802721) | 0x6da 133 main
sym.__libc_csu_init 101 0x770 | UNMATCH (0.970297) | 0x760 101 sym.__libc_csu_init
sym.__libc_csu_fini 2 0x7e0 | MATCH (1.000000) | 0x7d0 2 sym.__libc_csu_fini
sym._fini 9 0x7e4 | MATCH (1.000000) | 0x7d4 9 sym._fini
loc.imp._ITM_deregisterTMCloneTable 292 0x0 | UNMATCH (0.993711) | 0x0 292 loc.imp._ITM_deregisterTMCloneTable
red@blue:~/c/chapter10$
Hay muchas formas de usar radiff. Sin argumentos simplemente vuelca las diferencias, pero estas pueden ser enormes. La salida que acabamos de generar con -a nos muestra que las principales diferencias se encuentran en la función main. Esto es muy útil, especialmente en programas grandes con muchas funciones, donde el parche puede haberse aplicado a una única función; de este modo se ahorra mucho tiempo.
En este caso, la diferencia se encuentra aquí:
│ 0x0000072f 8b00 mov eax, dword [rax]
│ 0x00000731 8d5001 lea edx, [rax + 1]
│ 0x00000734 488b45f8 mov rax, qword [var_8h]
│ 0x00000738 8910 mov dword [rax], edx
│ 0x0000073a 488b45f8 mov rax, qword [var_8h]
│ 0x0000073e 8b00 mov eax, dword [rax]
│ 0x00000740 89c6 mov esi, eax
│ 0x00000742 488d3da00000. lea rdi, [0x000007e9] ; "%d\n" ; const char *format
│ 0x00000749 b800000000 mov eax, 0
Aquí se mueve a eax el contenido de lo apuntado por RAX —un puntero en este caso—, y luego se carga en edx el contenido de lo apuntado por ese puntero (3) más 1. Como puedes ver, se usa un «juego de punteros» para evitar usar add directamente. El resultado final es el mismo.
Un puntero como argumento
Pasemos a un ejemplo más complejo. Como puedes deducir, los punteros pueden usarse junto con funciones. Cuando se pasa un array como argumento a una función, se pasa una referencia (dirección base) al array en lugar del array completo, por lo que si el array se modifica dentro de la función, esos cambios serán permanentes y efectivos tras el ret. Lo mismo puede hacerse con ints, chars o cualquier tipo de variable: basta con declarar esos parámetros como punteros con * (o declararlos como direcciones con & y operar dentro de la función).
#include <stdio.h>
void x2(int *x) {
*x = *x * 2;
}
main() {
int n = 5;
printf("value= %d\n", n);
x2(&n);
printf("updated_value= %d\n", n);
getchar();
}
Como esto es algo más complejo, podemos usar agc para generar el grafo de llamadas en main:
[0x562a3106b714]> agc
┌────────────────────┐
│ main │
└────────────────────┘
v
│
┌───────────────────────────────────────│
│ ┌─────────────│
│ │ │───────────┐
│ │ └─────────────────────────────────────┐
│ │ │ │
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────────────┐
│ sym.imp.printf │ │ sym.x2 │ │ sym.imp.getchar │ │ sym.imp.__stack_chk_fail │
└────────────────────┘ └────────────────────┘ └────────────────────┘ └────────────────────────────┘
[0x562a3106b714]>
Aquí vemos que el programa llama a sym.x2.
[0x562a3106b714]> pdf
; DATA XREF from entry0 @ 0x562a3106b60d
┌ 118: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_ch @ rbp-0xc
│ ; var int64_t var_8h @ rbp-0x8
│ 0x562a3106b714 55 push rbp
│ 0x562a3106b715 4889e5 mov rbp, rsp
│ 0x562a3106b718 4883ec10 sub rsp, 0x10
│ 0x562a3106b71c 64488b042528. mov rax, qword fs:[0x28]
│ 0x562a3106b725 488945f8 mov qword [var_8h], rax
│ 0x562a3106b729 31c0 xor eax, eax
│ 0x562a3106b72b c745f4050000. mov dword [var_ch], 5
│ 0x562a3106b732 8b45f4 mov eax, dword [var_ch]
│ 0x562a3106b735 89c6 mov esi, eax
│ 0x562a3106b737 488d3dd60000. lea rdi, str.value___d ; 0x562a3106b814 ; "value= %d\n"
│ 0x562a3106b73e b800000000 mov eax, 0
│ 0x562a3106b743 e878feffff call sym.imp.printf ; int printf(const char *format)
│ 0x562a3106b748 488d45f4 lea rax, [var_ch]
│ 0x562a3106b74c 4889c7 mov rdi, rax
│ 0x562a3106b74f e8a6ffffff call sym.x2
│ 0x562a3106b754 8b45f4 mov eax, dword [var_ch]
│ 0x562a3106b757 89c6 mov esi, eax
│ 0x562a3106b759 488d3dbf0000. lea rdi, str.updated_value___d ; 0x562a3106b81f ; "updated_value= %d\n"
│ 0x562a3106b760 b800000000 mov eax, 0
│ 0x562a3106b765 e856feffff call sym.imp.printf ; int printf(const char *format)
│ 0x562a3106b76a e861feffff call sym.imp.getchar ; int getchar(void)
│ 0x562a3106b76f b800000000 mov eax, 0
│ 0x562a3106b774 488b55f8 mov rdx, qword [var_8h]
│ 0x562a3106b778 644833142528. xor rdx, qword fs:[0x28]
│ ┌─< 0x562a3106b781 7405 je 0x562a3106b788
│ │ 0x562a3106b783 e828feffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x562a3106b788 c9 leave
└ 0x562a3106b789 c3 ret
[0x562a3106b714]>
Primero, var_ch se inicializa con 0x5:
│ 0x562a3106b72b c745f4050000. mov dword [var_ch], 5
│ 0x562a3106b732 8b45f4 mov eax, dword [var_ch]
Luego se llama a x2 así:
│ 0x562a3106b748 488d45f4 lea rax, [var_ch]
│ 0x562a3106b74c 4889c7 mov rdi, rax
│ 0x562a3106b74f e8a6ffffff call sym.x2
En lugar del valor (mov), se usa LEA (load effective address) para cargar var_ch en rdi, de modo que la dirección (un puntero) del valor almacenado en var_ch se pasa a x2.
Veamos ahora el interior de x2:
[0x562a3106b714]> s sym.x2
[0x562a3106b6fa]> pdf
; CALL XREF from main @ 0x562a3106b74f
┌ 26: sym.x2 (int64_t arg1);
│ ; var int64_t var_8h @ rbp-0x8
│ ; arg int64_t arg1 @ rdi
│ 0x562a3106b6fa 55 push rbp
│ 0x562a3106b6fb 4889e5 mov rbp, rsp
│ 0x562a3106b6fe 48897df8 mov qword [var_8h], rdi ; arg1
│ 0x562a3106b702 488b45f8 mov rax, qword [var_8h]
│ 0x562a3106b706 8b00 mov eax, dword [rax]
│ 0x562a3106b708 8d1400 lea edx, [rax + rax]
│ 0x562a3106b70b 488b45f8 mov rax, qword [var_8h]
│ 0x562a3106b70f 8910 mov dword [rax], edx
│ 0x562a3106b711 90 nop
│ 0x562a3106b712 5d pop rbp
└ 0x562a3106b713 c3 ret
[0x562a3106b6fa]>
Primero se carga desde rdi el valor, que queda almacenado en var_8h, un espacio local reservado dentro de la función.
│ 0x562a3106b6fe 48897df8 mov qword [var_8h], rdi ; arg1
│ 0x562a3106b702 488b45f8 mov rax, qword [var_8h]
Luego el programa multiplica por dos, del mismo modo que en el ejemplo anterior.
│ 0x562a3106b706 8b00 mov eax, dword [rax]
│ 0x562a3106b708 8d1400 lea edx, [rax + rax]
El resultado entero calculado en edx se escribe en la dirección recibida por la función. El puntero permite modificar el objeto del llamador; el valor almacenado es el resultado de la operación. Comprueba ambos accesos en pointer-argument.
Dirección base e índice
Con arrays se puede operar de la misma manera. Cuando declaramos un array estático, estamos declarando una dirección base y un espacio de tipo t (int, char, float...) en memoria.
Eso puede interpretarse usando punteros, como en este programa:
#include <stdio.h>
main() {
int data[10];
int i;
printf ("%p\n", data);
*(data)= 20;
printf ("%d ", *(data));
*(data+1)= 40;
printf ("%d ", data[1]);
getchar();
}
Aquí está:
[0x560d6a0d06fa]> pdf
; DATA XREF from entry0 @ 0x560d6a0d060d
┌ 137: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_30h @ rbp-0x30
│ ; var int64_t var_2ch @ rbp-0x2c
│ ; var int64_t var_8h @ rbp-0x8
│ 0x560d6a0d06fa 55 push rbp
│ 0x560d6a0d06fb 4889e5 mov rbp, rsp
│ 0x560d6a0d06fe 4883ec30 sub rsp, 0x30
│ 0x560d6a0d0702 64488b042528. mov rax, qword fs:[0x28]
│ 0x560d6a0d070b 488945f8 mov qword [var_8h], rax
│ 0x560d6a0d070f 31c0 xor eax, eax
│ 0x560d6a0d0711 488d45d0 lea rax, [var_30h]
│ 0x560d6a0d0715 4889c6 mov rsi, rax
│ 0x560d6a0d0718 488d3df50000. lea rdi, [0x560d6a0d0814] ; "%p\n"
│ 0x560d6a0d071f b800000000 mov eax, 0
│ 0x560d6a0d0724 e897feffff call sym.imp.printf ; int printf(const char *format)
│ 0x560d6a0d0729 c745d0140000. mov dword [var_30h], 0x14 ; 20
│ 0x560d6a0d0730 8b45d0 mov eax, dword [var_30h]
│ 0x560d6a0d0733 89c6 mov esi, eax
│ 0x560d6a0d0735 488d3ddc0000. lea rdi, [0x560d6a0d0818] ; "%d "
│ 0x560d6a0d073c b800000000 mov eax, 0
│ 0x560d6a0d0741 e87afeffff call sym.imp.printf ; int printf(const char *format)
│ 0x560d6a0d0746 c745d4280000. mov dword [var_2ch], 0x28 ; '(' ; 40
│ 0x560d6a0d074d 8b45d4 mov eax, dword [var_2ch]
│ 0x560d6a0d0750 89c6 mov esi, eax
│ 0x560d6a0d0752 488d3dbf0000. lea rdi, [0x560d6a0d0818] ; "%d "
│ 0x560d6a0d0759 b800000000 mov eax, 0
│ 0x560d6a0d075e e85dfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x560d6a0d0763 e868feffff call sym.imp.getchar ; int getchar(void)
│ 0x560d6a0d0768 b800000000 mov eax, 0
│ 0x560d6a0d076d 488b55f8 mov rdx, qword [var_8h]
│ 0x560d6a0d0771 644833142528. xor rdx, qword fs:[0x28]
│ ┌─< 0x560d6a0d077a 7405 je 0x560d6a0d0781
│ │ 0x560d6a0d077c e82ffeffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x560d6a0d0781 c9 leave
└ 0x560d6a0d0782 c3 ret
[0x560d6a0d06fa]>
En este programa, en lugar de una dirección base y demás, el compilador trabaja con dos variables, ya que solo se inicializan dos posiciones. La primera posición se inicializa con 20:
│ 0x560d6a0d0729 b c745d0140000. mov dword [var_30h], 0x14 ; 20
│ 0x560d6a0d0730 b 8b45d0 mov eax, dword [var_30h]
Podemos comprobarlo depurando el programa así:
[0x560d6a0d0729]> afvd
var var_8h = 0x7ffef51d96a8 = (qword)0x073629ee5f9d1100
var var_30h = 0x7ffef51d9680 = (qword)0x00007fed746809a0
var var_2ch = 0x7ffef51d9684 = (qword)0x0000000000007fed
[0x560d6a0d0729]>
Esas son las «variables»; tras avanzar más allá de la inicialización vemos:
[0x560d6a0d0729]> ds
[0x560d6a0d0730]> afvd
var var_8h = 0x7ffef51d96a8 = (qword)0x073629ee5f9d1100
var var_30h = 0x7ffef51d9680 = (qword)0x00007fed00000014
var var_2ch = 0x7ffef51d9684 = (qword)0x0000000000007fed
[0x560d6a0d0730]>
Debido a la lógica de los arrays: 0x7ffef51d9680 será la dirección base del array; var_2ch irá justo a continuación, como puedes observar:
│ 0x560d6a0d0746 c745d4280000. mov dword [var_2ch], 0x28 ; '(' ; 40
│ 0x560d6a0d074d 8b45d4 mov eax, dword [var_2ch]
Pasamos a la siguiente posición, donde se inicializa var_2ch. Como veremos, esas variables se suceden una tras otra así:
[0x560d6a0d0750]> pf 2i @ 0x7ffef51d9680
0x7ffef51d9680 [0] {
0x7ffef51d9680 = 20
}
0x7ffef51d9684 [1] {
0x7ffef51d9684 = 40
}
Una forma habitual de identificar arrays en memoria es observar muchas variables del mismo tipo que se suceden consecutivamente, como acabamos de ver.
Estructuras reservadas en el heap
Por último, trabajemos con este ejemplo final:
#include <stdio.h>
main() {
struct person {
char name[30];
char email[25];
int age;
};
struct person *person1;
person1 = (struct person*)
malloc (sizeof(struct person));
strcpy(person1->name, "Peter");
strcpy(person1->email, "p@p.p");
person1->age = 21;
printf("Person data= %s, %s, and the age is: %d\n",
person1->name, person1->email, person1->age);
free(person1);
getchar();
}
El último ejemplo reserva una estructura mediante malloc. sizeof incluye el espacio de sus miembros y el padding necesario para respetar su alineación. Para reconstruirla, compara el tamaño solicitado con los offsets de cada acceso.
p->campo accede a un miembro a través de un puntero y equivale a (*p).campo. Esta sintaxis también se utiliza con punteros a estructuras alojadas en la pila; no depende de que la reserva sea dinámica.
El desensamblado muestra la reserva y la inicialización de sus miembros:
[0x7fead046c090]> s main
[0x559db307071a]> pdf
; DATA XREF from entry0 @ 0x559db307062d
┌ 137: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_8h @ rbp-0x8
│ 0x559db307071a 55 push rbp
│ 0x559db307071b 4889e5 mov rbp, rsp
│ 0x559db307071e 4883ec10 sub rsp, 0x10
│ 0x559db3070722 bf3c000000 mov edi, 0x3c ; '<' ; 60
│ 0x559db3070727 e8c4feffff call sym.imp.malloc ; void *malloc(size_t size)
│ 0x559db307072c 488945f8 mov qword [var_8h], rax
│ 0x559db3070730 488b45f8 mov rax, qword [var_8h]
│ 0x559db3070734 c70050657465 mov dword [rax], 0x65746550 ; 'Pete'
│ ; [0x65746550:4]=-1
│ 0x559db307073a 66c740047200 mov word [rax + 4], 0x72 ; 'r'
│ ; [0x72:2]=0xffff ; 114
│ 0x559db3070740 488b45f8 mov rax, qword [var_8h]
│ 0x559db3070744 4883c01e add rax, 0x1e ; 30
│ 0x559db3070748 c7007040702e mov dword [rax], 0x2e704070 ; 'p@p.'
│ ; [0x2e704070:4]=-1
│ 0x559db307074e 66c740047000 mov word [rax + 4], 0x70 ; 'p'
│ ; [0x70:2]=0xffff ; 112
│ 0x559db3070754 488b45f8 mov rax, qword [var_8h]
│ 0x559db3070758 c74038150000. mov dword [rax + 0x38], 0x15 ; [0x15:4]=-1 ; 21
│ 0x559db307075f 488b45f8 mov rax, qword [var_8h]
│ 0x559db3070763 8b5038 mov edx, dword [rax + 0x38]
│ 0x559db3070766 488b45f8 mov rax, qword [var_8h]
│ 0x559db307076a 488d701e lea rsi, [rax + 0x1e]
│ 0x559db307076e 488b45f8 mov rax, qword [var_8h]
│ 0x559db3070772 89d1 mov ecx, edx
│ 0x559db3070774 4889f2 mov rdx, rsi
│ 0x559db3070777 4889c6 mov rsi, rax
│ 0x559db307077a 488d3db70000. lea rdi, str.Person_data___s___s__and_the_age_is:__d ; 0x559db3070838 ; "Person data= %s, %s, and the age is: %d\n"
│ 0x559db3070781 b800000000 mov eax, 0
│ 0x559db3070786 e845feffff call sym.imp.printf ; int printf(const char *format)
│ 0x559db307078b 488b45f8 mov rax, qword [var_8h]
│ 0x559db307078f 4889c7 mov rdi, rax
│ 0x559db3070792 e829feffff call sym.imp.free ; void free(void *ptr)
│ 0x559db3070797 e844feffff call sym.imp.getchar ; int getchar(void)
│ 0x559db307079c b800000000 mov eax, 0
│ 0x559db30707a1 c9 leave
└ 0x559db30707a2 c3 ret
[0x559db307071a]>
Como podemos ver en primer lugar, el programa declara var_8h como puntero a la dirección base de la estructura y reserva espacio en la pila (0x10):
│ ; var int64_t var_8h @ rbp-0x8
│ 0x559db307071a 55 push rbp
│ 0x559db307071b 4889e5 mov rbp, rsp
│ 0x559db307071e 4883ec10 sub rsp, 0x10
Luego reserva 60 bytes en el heap para la estructura con malloc:
│ 0x559db3070722 bf3c000000 mov edi, 0x3c ; '<' ; 60
│ 0x559db3070727 e8c4feffff call sym.imp.malloc ; void *malloc(size_t size)
A continuación carga la dirección base en rax y copia «Peter» dentro.
│ 0x559db3070730 488b45f8 mov rax, qword [var_8h]
│ 0x559db3070734 c70050657465 mov dword [rax], 0x65746550 ; 'Pete'
│ ; [0x65746550:4]=-1
│ 0x559db307073a 66c740047200 mov word [rax + 4], 0x72 ; 'r'
│ ; [0x72:2]=0xffff ; 114
Observa cómo la «r» final se carga después de «Pete» (rax+4), ya que «Pete» = 4 caracteres = 4 bytes.
│ 0x559db3070740 488b45f8 mov rax, qword [var_8h]
│ 0x559db3070744 4883c01e add rax, 0x1e ; 30
│ 0x559db3070748 c7007040702e mov dword [rax], 0x2e704070 ; 'p@p.'
│ ; [0x2e704070:4]=-1
│ 0x559db307074e 66c740047000 mov word [rax + 4], 0x70 ; 'p'
Observa que add se suma a 0x1e, lo cual es importante: como rax contiene la dirección base de esa estructura de datos y el primer campo de la estructura es un array de 30 posiciones de tipo byte, hay que avanzar sobre él, ya que el siguiente array comenzará justo después.
│ 0x559db3070754 488b45f8 mov rax, qword [var_8h]
│ 0x559db3070758 c74038150000. mov dword [rax + 0x38], 0x15 ; [0x15:4]=-1 ; 21
│ 0x559db307075f 488b45f8 mov rax, qword [var_8h]
A continuación tenemos el int de la edad, que sigue la misma estrategia de inicialización.
Como práctica, compila estos programas y reviértelos por tu cuenta. La siguiente lección tratará sobre listas enlazadas y será la última sobre memoria dinámica; después abordaremos aspectos básicos como defines, uniones y operaciones a nivel de bit.
Vuelve al binario
Seguir el paso de direcciones a funciones y la construcción de estructuras en el heap. Identifica al menos una instrucción y el dato que utiliza; conserva la compilación con la que lo has observado.
Fuente de esta práctica: pointer-argument.c ↓.
Descargar los 13 ejemplos de fundamentos ↙
Puedes utilizar la consola del companion sin conversar con el agente, o abrir el ejecutable en tu desensamblador habitual.
./coursectl start 10 --example pointer-argument
./coursectl consoleCierra la sesión anterior con ./coursectl stop antes de cambiar de laboratorio. Ver preparación.
Antes de continuar
Explica con tus palabras la operación estudiada y señala las instrucciones o capturas que la justifican. Distingue los datos observados de los nombres y tipos que has reconstruido.
El progreso se guarda en este navegador. Puedes recorrer las lecciones en cualquier orden.