Distinguir una dirección de su contenido y reconocer las reservas de pila y heap.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 9 --example pointer-writeEstoy siguiendo «Punteros y memoria dinámica» del curso de Lobera. Distinguir una dirección de su contenido y reconocer las reservas de pila y 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.
Un puntero almacena una dirección. Para seguirlo en el desensamblado hay que distinguir la lectura de esa dirección, su cálculo y el acceso al objeto apuntado. El primer ejemplo permite observar las tres operaciones; después las aplicaremos a un array reservado en el heap.
Dirección, puntero y contenido
Empecemos con este código
#include <stdio.h>
#include <stdlib.h>
int main(){
int i = 2;
char c = 'c';
char* pc = &c;
printf("Value of i: %d \n",i);
printf("Address of i: %p \n",&i);
printf("Value of c: %c \n", c);
printf("Address of c: %p \n",&c);
printf("Updating the content of the mem address pointed by pc \n");
*pc = 'b';
printf("Value of c: %c \n", c);
return 0;
}
En C, &c indica la dirección de la variable c. *p declara un puntero. Pueden existir múltiples punteros apuntando a la misma dirección de memoria, de modo que si esa dirección se actualiza, el cambio afecta a todos los punteros. Si no tienes ningún conocimiento previo sobre punteros, lee primero esto: https://www.programiz.com/c-programming/c-pointers y luego continúa con este artículo.
Al compilar, el programa produce la siguiente salida:
Value of i: 2
Address of i: 0x7ffe948d662c
Value of c: c
Address of c: 0x7ffe948d662b
Updating the content of the mem address pointed by pc
Value of c: b
Y ahora el desensamblado:
[0x556609210155]> pdf
; DATA XREF from entry0 @ 0x55660921008d
┌ 210: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_15h @ rbp-0x15
│ ; var int64_t var_14h @ rbp-0x14
│ ; var int64_t var_10h @ rbp-0x10
│ ; var int64_t var_8h @ rbp-0x8
│ 0x556609210155 55 push rbp
│ 0x556609210156 4889e5 mov rbp, rsp
│ 0x556609210159 4883ec20 sub rsp, 0x20
│ 0x55660921015d 64488b042528. mov rax, qword fs:[0x28]
│ 0x556609210166 488945f8 mov qword [var_8h], rax
│ 0x55660921016a 31c0 xor eax, eax
│ 0x55660921016c c745ec020000. mov dword [var_14h], 2
│ 0x556609210173 c645eb63 mov byte [var_15h], 0x63 ; 'c' ; 99
│ 0x556609210177 488d45eb lea rax, [var_15h]
│ 0x55660921017b 488945f0 mov qword [var_10h], rax
│ 0x55660921017f 8b45ec mov eax, dword [var_14h]
│ 0x556609210182 89c6 mov esi, eax
│ 0x556609210184 488d3d7d0e00. lea rdi, str.Value_of_i:__d ; 0x556609211008 ; "Value of i: %d \n"
│ 0x55660921018b b800000000 mov eax, 0
│ 0x556609210190 e8bbfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x556609210195 488d45ec lea rax, [var_14h]
│ 0x556609210199 4889c6 mov rsi, rax
│ 0x55660921019c 488d3d760e00. lea rdi, str.Address_of_i:__p ; 0x556609211019 ; "Address of i: %p \n"
│ 0x5566092101a3 b800000000 mov eax, 0
│ 0x5566092101a8 e8a3feffff call sym.imp.printf ; int printf(const char *format)
│ 0x5566092101ad 0fb645eb movzx eax, byte [var_15h]
│ 0x5566092101b1 0fbec0 movsx eax, al
│ 0x5566092101b4 89c6 mov esi, eax
│ 0x5566092101b6 488d3d6f0e00. lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
│ 0x5566092101bd b800000000 mov eax, 0
│ 0x5566092101c2 e889feffff call sym.imp.printf ; int printf(const char *format)
│ 0x5566092101c7 488d45eb lea rax, [var_15h]
│ 0x5566092101cb 4889c6 mov rsi, rax
│ 0x5566092101ce 488d3d680e00. lea rdi, str.Address_of_c:__p ; 0x55660921103d ; "Address of c: %p \n"
│ 0x5566092101d5 b800000000 mov eax, 0
│ 0x5566092101da e871feffff call sym.imp.printf ; int printf(const char *format)
│ 0x5566092101df 488d3d6a0e00. lea rdi, str.Updating_the_content_of_the_mem_address_pointed_by_pc ; 0x556609211050 ; "Updating the content of the mem address pointed by pc "
│ 0x5566092101e6 e845feffff call sym.imp.puts ; int puts(const char *s)
│ 0x5566092101eb 488b45f0 mov rax, qword [var_10h]
│ 0x5566092101ef c60062 mov byte [rax], 0x62 ; 'b'
│ ; [0x62:1]=255 ; 98
│ 0x5566092101f2 0fb645eb movzx eax, byte [var_15h]
│ 0x5566092101f6 0fbec0 movsx eax, al
│ 0x5566092101f9 89c6 mov esi, eax
│ 0x5566092101fb 488d3d2a0e00. lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
│ 0x556609210202 b800000000 mov eax, 0
│ 0x556609210207 e844feffff call sym.imp.printf ; int printf(const char *format)
│ 0x55660921020c b800000000 mov eax, 0
│ 0x556609210211 488b55f8 mov rdx, qword [var_8h]
│ 0x556609210215 644833142528. xor rdx, qword fs:[0x28]
│ ┌─< 0x55660921021e 7405 je 0x556609210225
│ │ 0x556609210220 e81bfeffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x556609210225 c9 leave
└ 0x556609210226 c3 ret
[0x556609210155]>
Vamos a diseccionarlo. Al principio el programa inicializa estas variables:
│ 0x55660921016c c745ec020000. mov dword [var_14h], 2
│ 0x556609210173 c645eb63 mov byte [var_15h], 0x63 ; 'c' ; 99
│ 0x556609210177 488d45eb lea rax, [var_15h]
│ 0x55660921017b 488945f0 mov qword [var_10h], rax
La primera variable se inicializa con 2, la segunda con 0x63, es decir, el carácter C, y a continuación la dirección de memoria de esa variable (lea = load effective address) —no su contenido— se almacena en var_10h. Como podemos deducir con bastante seguridad que esas primeras variables corresponden al entero i y al carácter c, las renombramos con afvn, con lo que el resultado queda así:
[0x556609210155]> afv
var int64_t char_c @ rbp-0x15
var int64_t int_i @ rbp-0x14
var int64_t var_10h @ rbp-0x10
var int64_t var_8h @ rbp-0x8 = stack cookie
Y como var_10h almacenará la DIRECCIÓN de c, la renombramos como pointer_c
[0x556609210155]> afvn pointer_c var_10h
[0x556609210155]> afv
var int64_t char_c @ rbp-0x15
var int64_t int_i @ rbp-0x14
var int64_t pointer_c @ rbp-0x10
var int64_t var_8h @ rbp-0x8
Inspeccionemos el primer printf
│ 0x55660921017f 8b45ec mov eax, dword [int_i]
│ 0x556609210182 89c6 mov esi, eax
│ 0x556609210184 488d3d7d0e00. lea rdi, str.Value_of_i:__d ; 0x556609211008 ; "Value of i: %d \n"
│ 0x55660921018b b800000000 mov eax, 0
│ 0x556609210190 e8bbfeffff call sym.imp.printf ; int printf(const char *format)
Nada nuevo aquí. Con [ref] indicamos que queremos el contenido de lo apuntado por int_i, es decir, el valor de la variable i; lo cargamos en eax (usamos eax en lugar de rax porque el espacio disponible es suficiente) y después cargamos la dirección base de la cadena "Value of..." como parámetro. La función printf toma esos dos parámetros y hace su trabajo, como ya deberíamos saber. Nótese que en este primer caso pasamos el valor de la variable, no su dirección. Continuemos:
│ 0x556609210195 488d45ec lea rax, [int_i]
│ 0x556609210199 4889c6 mov rsi, rax
│ 0x55660921019c 488d3d760e00. lea rdi, str.Address_of_i:__p ; 0x556609211019 ; "Address of i: %p \n"
│ 0x5566092101a3 b800000000 mov eax, 0
│ 0x5566092101a8 e8a3feffff call sym.imp.printf ; int printf(const char *format)
Observa cómo en el siguiente bloque el programa usa lea en lugar de mov. Esta vez carga la DIRECCIÓN de int_i en lugar del valor almacenado en lo que apunta int_i. En términos generales, usar &var se traduce internamente en lea reg, [var]. Este es un concepto clave en reversing, desarrollo de exploits y, en general, en cualquier análisis de programas a bajo nivel; conviene tenerlo muy claro. Sigamos
│ 0x5566092101ad 0fb645eb movzx eax, byte [char_c]
│ 0x5566092101b1 0fbec0 movsx eax, al
│ 0x5566092101b4 89c6 mov esi, eax
│ 0x5566092101b6 488d3d6f0e00. lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
│ 0x5566092101bd b800000000 mov eax, 0
│ 0x5566092101c2 e889feffff call sym.imp.printf ; int printf(const char *format)
La instrucción carga un byte y lo extiende con ceros al ancho del registro de destino. movzx expresa esa extensión; su presencia no demuestra una optimización del consumo de memoria.
│ 0x5566092101c7 488d45eb lea rax, [char_c]
│ 0x5566092101cb 4889c6 mov rsi, rax
│ 0x5566092101ce 488d3d680e00. lea rdi, str.Address_of_c:__p ; 0x55660921103d ; "Address of c: %p \n"
│ 0x5566092101d5 b800000000 mov eax, 0
│ 0x5566092101da e871feffff call sym.imp.printf ; int printf(const char *format)
De nuevo, esta vez se carga la DIRECCIÓN.
Al final del programa tenemos esto:
│ 0x5566092101eb 488b45f0 mov rax, qword [pointer_c]
│ 0x5566092101ef c60062 mov byte [rax], 0x62 ; 'b'
│ ; [0x62:1]=255 ; 98
│ 0x5566092101f2 0fb645eb movzx eax, byte [char_c]
│ 0x5566092101f6 0fbec0 movsx eax, al
│ 0x5566092101f9 89c6 mov esi, eax
│ 0x5566092101fb 488d3d2a0e00. lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
│ 0x556609210202 b800000000 mov eax, 0
│ 0x556609210207 e844feffff call sym.imp.printf ; int printf(const char *format)
La primera instrucción carga en rax el puntero guardado en pointer_c. La segunda escribe el byte 0x62, el carácter b, en la dirección que contiene rax. Así se modifica el objeto char_c.
Empezamos así:
│ 0x5566092101eb b 488b45f0 mov rax, qword [pointer_c]
│ 0x5566092101ef c60062 mov byte [rax], 0x62 ; 'b'
│ ; [0x62:1]=255 ; 98
│ 0x5566092101f2 0fb645eb movzx eax, byte [char_c]
│ 0x5566092101f6 0fbec0 movsx eax, al
│ 0x5566092101f9 89c6 mov esi, eax
│ 0x5566092101fb 488d3d2a0e00. lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
│ 0x556609210202 b800000000 mov eax, 0
│ 0x556609210207 e844feffff call sym.imp.printf ; int printf(const char *format)
[...]
[0x5566092101eb]> afvd
var var_8h = 0x7ffd71635228 = (qword)0xa0af6487b4e4ef00
var int_i = 0x7ffd7163521c = (qword)0x7163521b00000002
var char_c = 0x7ffd7163521b = (qword)0x63521b0000000263
var pointer_c = 0x7ffd71635220 = (qword)0x00007ffd7163521b
Como se puede ver, el contenido de pointer_c (0x00007ffd7163521b) es la dirección de memoria de char_c (0x7ffd7163521b). Así funcionan los punteros: almacenan direcciones de memoria.
Tras ejecutar el primer mov, rax tendrá este aspecto:
[0x5566092101eb]> ds
[0x5566092101ef]> dr rax
0x7ffd7163521b
Ahora rax contiene el CONTENIDO de pointer_c, y ese contenido es la DIRECCIÓN de char_c, por lo que hacer [rax] equivale a hacer [char_c]. Y char_c contiene el carácter 'c':
[0x5566092101ef]> afvd
var var_8h = 0x7ffd71635228 = (qword)0xa0af6487b4e4ef00
var int_i = 0x7ffd7163521c = (qword)0x7163521b00000002
var char_c = 0x7ffd7163521b = (qword)0x63521b0000000263
var pointer_c = 0x7ffd71635220 = (qword)0x00007ffd7163521b
[0x5566092101ef]> pxw @ 0x7ffd7163521b
0x7ffd7163521b 0x00000263 0x63521b00 0x007ffd71 0xe4ef0000 c.....Rcq.......
Y tras ejecutar el segundo mov
[0x5566092101ef]> ds
[0x5566092101f2]> pxw @ 0x7ffd7163521b
0x7ffd7163521b 0x00000262 0x63521b00 0x007ffd71 0xe4ef0000 b.....Rcq.......
Ahora contiene el carácter 'b'.
La carga posterior de char_c obtiene el byte modificado. pointer_c conserva su dirección; [rax] designa el contenido situado en ella.
Con esto terminamos el primer ejemplo. Pasemos a algo más complejo, aunque igualmente accesible.
Un array local en la pila
¿Recuerdas el programa que vimos en uno de los tutoriales anteriores? El que usaba un array que el usuario debía rellenar en un bucle while. Inspeccionemos este:
#include <stdio.h>
main() {
int data[100];
int entered;
int i;
long sum=0;
do {
printf("How many numbers? ");
scanf("%d", &entered);
if (entered>100)
printf("Limit is 100");
} while (entered>100);
for (i=0; i<entered; i++) {
printf("Enter number %d: ", i+1);
scanf("%d", &data[i]);
}
for (i=0; i<entered; i++)
sum += data[i];
printf("SUM: %ld\n", sum);
}
Este ejemplo reserva el array como variable local y utiliza un índice para recorrerlo:
[0x55c2a9231155]> pdf
; DATA XREF from entry0 @ 0x55c2a923108d
┌ 335: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_1b0h @ rbp-0x1b0
│ ; var int64_t var_1ach @ rbp-0x1ac
│ ; var int64_t var_1a8h @ rbp-0x1a8
│ ; var int64_t var_1a0h @ rbp-0x1a0
│ ; var int64_t var_8h @ rbp-0x8
│ 0x55c2a9231155 55 push rbp
│ 0x55c2a9231156 4889e5 mov rbp, rsp
│ 0x55c2a9231159 4881ecb00100. sub rsp, 0x1b0
│ 0x55c2a9231160 64488b042528. mov rax, qword fs:[0x28]
│ 0x55c2a9231169 488945f8 mov qword [var_8h], rax
│ 0x55c2a923116d 31c0 xor eax, eax
│ 0x55c2a923116f 48c78558feff. mov qword [var_1a8h], 0
│ ┌─> 0x55c2a923117a 488d3d830e00. lea rdi, str.How_many_numbers ; 0x55c2a9232004 ; "How many numbers? "
│ ╎ 0x55c2a9231181 b800000000 mov eax, 0
│ ╎ 0x55c2a9231186 e8b5feffff call sym.imp.printf ; int printf(const char *format)
│ ╎ 0x55c2a923118b 488d8550feff. lea rax, [var_1b0h]
│ ╎ 0x55c2a9231192 4889c6 mov rsi, rax
│ ╎ 0x55c2a9231195 488d3d7b0e00. lea rdi, [0x55c2a9232017] ; "%d"
│ ╎ 0x55c2a923119c b800000000 mov eax, 0
│ ╎ 0x55c2a92311a1 e8aafeffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ ╎ 0x55c2a92311a6 8b8550feffff mov eax, dword [var_1b0h]
│ ╎ 0x55c2a92311ac 83f864 cmp eax, 0x64 ; 100
│ ┌──< 0x55c2a92311af 7e11 jle 0x55c2a92311c2
│ │╎ 0x55c2a92311b1 488d3d620e00. lea rdi, str.Limit_is_100 ; 0x55c2a923201a ; "Limit is 100"
│ │╎ 0x55c2a92311b8 b800000000 mov eax, 0
│ │╎ 0x55c2a92311bd e87efeffff call sym.imp.printf ; int printf(const char *format)
│ └──> 0x55c2a92311c2 8b8550feffff mov eax, dword [var_1b0h]
│ ╎ 0x55c2a92311c8 83f864 cmp eax, 0x64 ; 100
│ └─< 0x55c2a92311cb 7fad jg 0x55c2a923117a
│ 0x55c2a92311cd c78554feffff. mov dword [var_1ach], 0
│ ┌─< 0x55c2a92311d7 eb4e jmp 0x55c2a9231227
│ ┌──> 0x55c2a92311d9 8b8554feffff mov eax, dword [var_1ach]
│ ╎│ 0x55c2a92311df 83c001 add eax, 1
│ ╎│ 0x55c2a92311e2 89c6 mov esi, eax
│ ╎│ 0x55c2a92311e4 488d3d3c0e00. lea rdi, str.Enter_number__d: ; 0x55c2a9232027 ; "Enter number %d: "
│ ╎│ 0x55c2a92311eb b800000000 mov eax, 0
│ ╎│ 0x55c2a92311f0 e84bfeffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x55c2a92311f5 488d8560feff. lea rax, [var_1a0h]
│ ╎│ 0x55c2a92311fc 8b9554feffff mov edx, dword [var_1ach]
│ ╎│ 0x55c2a9231202 4863d2 movsxd rdx, edx
│ ╎│ 0x55c2a9231205 48c1e202 shl rdx, 2
│ ╎│ 0x55c2a9231209 4801d0 add rax, rdx
│ ╎│ 0x55c2a923120c 4889c6 mov rsi, rax
│ ╎│ 0x55c2a923120f 488d3d010e00. lea rdi, [0x55c2a9232017] ; "%d"
│ ╎│ 0x55c2a9231216 b800000000 mov eax, 0
│ ╎│ 0x55c2a923121b e830feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ ╎│ 0x55c2a9231220 838554feffff. add dword [var_1ach], 1
│ ╎│ ; CODE XREF from main @ 0x55c2a92311d7
│ ╎└─> 0x55c2a9231227 8b8550feffff mov eax, dword [var_1b0h]
│ ╎ 0x55c2a923122d 398554feffff cmp dword [var_1ach], eax
│ └──< 0x55c2a9231233 7ca4 jl 0x55c2a92311d9
│ 0x55c2a9231235 c78554feffff. mov dword [var_1ach], 0
│ ┌─< 0x55c2a923123f eb1f jmp 0x55c2a9231260
│ ┌──> 0x55c2a9231241 8b8554feffff mov eax, dword [var_1ach]
│ ╎│ 0x55c2a9231247 4898 cdqe
│ ╎│ 0x55c2a9231249 8b848560feff. mov eax, dword [rbp + rax*4 - 0x1a0]
│ ╎│ 0x55c2a9231250 4898 cdqe
│ ╎│ 0x55c2a9231252 48018558feff. add qword [var_1a8h], rax
│ ╎│ 0x55c2a9231259 838554feffff. add dword [var_1ach], 1
│ ╎│ ; CODE XREF from main @ 0x55c2a923123f
│ ╎└─> 0x55c2a9231260 8b8550feffff mov eax, dword [var_1b0h]
│ ╎ 0x55c2a9231266 398554feffff cmp dword [var_1ach], eax
│ └──< 0x55c2a923126c 7cd3 jl 0x55c2a9231241
│ 0x55c2a923126e 488b8558feff. mov rax, qword [var_1a8h]
│ 0x55c2a9231275 4889c6 mov rsi, rax
│ 0x55c2a9231278 488d3dba0d00. lea rdi, str.SUM:__ld ; 0x55c2a9232039 ; "SUM: %ld\n"
│ 0x55c2a923127f b800000000 mov eax, 0
│ 0x55c2a9231284 e8b7fdffff call sym.imp.printf ; int printf(const char *format)
│ 0x55c2a9231289 b800000000 mov eax, 0
│ 0x55c2a923128e 488b4df8 mov rcx, qword [var_8h]
│ 0x55c2a9231292 6448330c2528. xor rcx, qword fs:[0x28]
│ ┌─< 0x55c2a923129b 7405 je 0x55c2a92312a2
│ │ 0x55c2a923129d e88efdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x55c2a92312a2 c9 leave
└ 0x55c2a92312a3 c3 ret
[0x55c2a9231155]>
Lo que quiero que notes es lo siguiente: al comienzo del código aparece esto = 432:
│ 0x55c2a9231159 4881ecb00100. sub rsp,
Habrás visto instrucciones como esta en muchos de los binarios que hemos analizado. Lo que hace es reservar espacio en la memoria local de la función (usando la pila) para almacenar variables locales. (consulta https://en.wikibooks.org/wiki/X86_Disassembly/Functions_and_Stack_Frames)
En este caso reserva algo más de 400 bytes. Tiene sentido: según el código, declaramos un array de 100 enteros y, asumiendo 4 bytes por entero, esos 400 bytes y algo más son coherentes. Es habitual que el programa reserve un poco más de espacio del estrictamente necesario. Observar este sub rbp al inicio de la función aporta información relevante.
No voy a entrar en muchos detalles sobre el código, ya que no hay prácticamente nada nuevo; deberías saber analizarlo sin problemas.
Lo que quiero destacar esta vez es lo siguiente. En este programa usamos un do-while en lugar de un while, y podemos verlo aquí:
│ ┌─> 0x55c2a923117a 488d3d830e00. lea rdi, str.How_many_numbers ; 0x55c2a9232004 ; "How many numbers? "
│ ╎ 0x55c2a9231181 b800000000 mov eax, 0
│ ╎ 0x55c2a9231186 e8b5feffff call sym.imp.printf ; int printf(const char *format)
│ ╎ 0x55c2a923118b 488d8550feff. lea rax, [var_1b0h]
│ ╎ 0x55c2a9231192 4889c6 mov rsi, rax
│ ╎ 0x55c2a9231195 488d3d7b0e00. lea rdi, [0x55c2a9232017] ; "%d"
│ ╎ 0x55c2a923119c b800000000 mov eax, 0
│ ╎ 0x55c2a92311a1 e8aafeffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ ╎ 0x55c2a92311a6 8b8550feffff mov eax, dword [var_1b0h]
│ ╎ 0x55c2a92311ac 83f864 cmp eax, 0x64 ; 100
│ ┌──< 0x55c2a92311af 7e11 jle 0x55c2a92311c2
│ │╎ 0x55c2a92311b1 488d3d620e00. lea rdi, str.Limit_is_100 ; 0x55c2a923201a ; "Limit is 100"
│ │╎ 0x55c2a92311b8 b800000000 mov eax, 0
│ │╎ 0x55c2a92311bd e87efeffff call sym.imp.printf ; int printf(const char *format)
│ └──> 0x55c2a92311c2 8b8550feffff mov eax, dword [var_1b0h]
│ ╎ 0x55c2a92311c8 83f864 cmp eax, 0x64 ; 100
│ └─< 0x55c2a92311cb 7fad jg 0x55c2a923117a
El bloque de código se ejecuta primero y, después, se comprueba la condición (cmp 100); el programa salta de vuelta al inicio del bloque o continúa, pero el bloque se ejecuta al menos una vez. Con un while en lugar de un do-while, el programa salta primero a la comprobación y, en función del resultado, entra o no en el bloque.
Reserva dinámica del array
Pasemos a inspeccionar el mismo programa usando memoria dinámica:
#include <stdio.h>
#include <stdlib.h>
main() {
int* data;
int valnum;
int i;
long sum=0;
do {
printf("How many vals you need to add? ");
scanf("%d", &valnum);
data = (int *) malloc (valnum * sizeof(int));
if (data == NULL)
printf("NO SPACE AVAILABLE.");
} while (data == NULL);
for (i=0; i<valnum; i++) {
printf("ENTER NUM %d ", i+1);
scanf("%d", data+i);
}
for (i=0; i<valnum; i++)
sum += *(data+i);
printf("SUM: %ld\n", sum);
free(data);
}
La versión con memoria dinámica pregunta cuántos valores se van a almacenar y calcula el tamaño de la reserva:
data = (int *) malloc (valnum * sizeof(int));
Podemos desglosarlo para entenderlo mejor. Primero se calcula valnum * sizeof(int): sizeof(int) devuelve el tamaño en bytes de un entero en nuestro sistema (por ejemplo, 4), que se multiplica por valnum, la entrada del usuario (cuántos valores quiere añadir). Si el usuario quiere 4 valores, el cálculo sería algo como 4*4 = 16 bytes, el espacio necesario para alojar 4 enteros. Ese resultado se pasa a malloc. Esta función se usa para reservar espacio en memoria. void *malloc(size_t size) reserva la memoria solicitada (en el heap) y devuelve un puntero a ella (en la pila), o NULL si la solicitud falla; por eso el resultado de malloc se almacena en un puntero a entero.
A continuación el programa hace lo siguiente:
scanf("%d", data+i);
Nótese que normalmente al usar scanf pasamos &var; aquí, en cambio, pasamos data+i, siendo i un entero (1, 2, 3...). Por eso data es en sí mismo un puntero, una dirección de memoria, y usar & no tendría sentido. El +i indica la siguiente dirección de memoria disponible, ya que data apunta a la dirección base de ese array dinámico.
La misma lógica se aplica al calcular la suma
sum += *(data+i);
Desensamblemos ahora este programa y analicémoslo:
[0x558471c62175]> pdf
; DATA XREF from entry0 @ 0x558471c620ad
┌ 322: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_20h @ rbp-0x20
│ ; var int64_t var_1ch @ rbp-0x1c
│ ; var int64_t var_18h @ rbp-0x18
│ ; var int64_t var_10h @ rbp-0x10
│ ; var int64_t var_8h @ rbp-0x8
│ 0x558471c62175 55 push rbp
│ 0x558471c62176 4889e5 mov rbp, rsp
│ 0x558471c62179 4883ec20 sub rsp, 0x20
│ 0x558471c6217d 64488b042528. mov rax, qword fs:[0x28]
│ 0x558471c62186 488945f8 mov qword [var_8h], rax
│ 0x558471c6218a 31c0 xor eax, eax
│ 0x558471c6218c 48c745e80000. mov qword [var_18h], 0
│ ┌─> 0x558471c62194 488d3d6d0e00. lea rdi, str.How_many_vals_you_need_to_add ; 0x558471c63008 ; "How many vals you need to add? "
│ ╎ 0x558471c6219b b800000000 mov eax, 0
│ ╎ 0x558471c621a0 e8abfeffff call sym.imp.printf ; int printf(const char *format)
│ ╎ 0x558471c621a5 488d45e0 lea rax, [var_20h]
│ ╎ 0x558471c621a9 4889c6 mov rsi, rax
│ ╎ 0x558471c621ac 488d3d750e00. lea rdi, [0x558471c63028] ; "%d"
│ ╎ 0x558471c621b3 b800000000 mov eax, 0
│ ╎ 0x558471c621b8 e8b3feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ ╎ 0x558471c621bd 8b45e0 mov eax, dword [var_20h]
│ ╎ 0x558471c621c0 4898 cdqe
│ ╎ 0x558471c621c2 48c1e002 shl rax, 2
│ ╎ 0x558471c621c6 4889c7 mov rdi, rax
│ ╎ 0x558471c621c9 e892feffff call sym.imp.malloc ; void *malloc(size_t size)
│ ╎ 0x558471c621ce 488945f0 mov qword [var_10h], rax
│ ╎ 0x558471c621d2 48837df000 cmp qword [var_10h], 0
│ ┌──< 0x558471c621d7 7511 jne 0x558471c621ea
│ │╎ 0x558471c621d9 488d3d4b0e00. lea rdi, str.NO_SPACE_AVAILABLE. ; 0x558471c6302b ; "NO SPACE AVAILABLE."
│ │╎ 0x558471c621e0 b800000000 mov eax, 0
│ │╎ 0x558471c621e5 e866feffff call sym.imp.printf ; int printf(const char *format)
│ └──> 0x558471c621ea 48837df000 cmp qword [var_10h], 0
│ └─< 0x558471c621ef 74a3 je 0x558471c62194
│ 0x558471c621f1 c745e4000000. mov dword [var_1ch], 0
│ ┌─< 0x558471c621f8 eb45 jmp 0x558471c6223f
│ ┌──> 0x558471c621fa 8b45e4 mov eax, dword [var_1ch]
│ ╎│ 0x558471c621fd 83c001 add eax, 1
│ ╎│ 0x558471c62200 89c6 mov esi, eax
│ ╎│ 0x558471c62202 488d3d360e00. lea rdi, str.ENTER_NUM__d ; 0x558471c6303f ; "ENTER NUM %d "
│ ╎│ 0x558471c62209 b800000000 mov eax, 0
│ ╎│ 0x558471c6220e e83dfeffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x558471c62213 8b45e4 mov eax, dword [var_1ch]
│ ╎│ 0x558471c62216 4898 cdqe
│ ╎│ 0x558471c62218 488d14850000. lea rdx, [rax*4]
│ ╎│ 0x558471c62220 488b45f0 mov rax, qword [var_10h]
│ ╎│ 0x558471c62224 4801d0 add rax, rdx
│ ╎│ 0x558471c62227 4889c6 mov rsi, rax
│ ╎│ 0x558471c6222a 488d3df70d00. lea rdi, [0x558471c63028] ; "%d"
│ ╎│ 0x558471c62231 b800000000 mov eax, 0
│ ╎│ 0x558471c62236 e835feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ ╎│ 0x558471c6223b 8345e401 add dword [var_1ch], 1
│ ╎│ ; CODE XREF from main @ 0x558471c621f8
│ ╎└─> 0x558471c6223f 8b45e0 mov eax, dword [var_20h]
│ ╎ 0x558471c62242 3945e4 cmp dword [var_1ch], eax
│ └──< 0x558471c62245 7cb3 jl 0x558471c621fa
│ 0x558471c62247 c745e4000000. mov dword [var_1ch], 0
│ ┌─< 0x558471c6224e eb20 jmp 0x558471c62270
│ ┌──> 0x558471c62250 8b45e4 mov eax, dword [var_1ch]
│ ╎│ 0x558471c62253 4898 cdqe
│ ╎│ 0x558471c62255 488d14850000. lea rdx, [rax*4]
│ ╎│ 0x558471c6225d 488b45f0 mov rax, qword [var_10h]
│ ╎│ 0x558471c62261 4801d0 add rax, rdx
│ ╎│ 0x558471c62264 8b00 mov eax, dword [rax]
│ ╎│ 0x558471c62266 4898 cdqe
│ ╎│ 0x558471c62268 480145e8 add qword [var_18h], rax
│ ╎│ 0x558471c6226c 8345e401 add dword [var_1ch], 1
│ ╎│ ; CODE XREF from main @ 0x558471c6224e
│ ╎└─> 0x558471c62270 8b45e0 mov eax, dword [var_20h]
│ ╎ 0x558471c62273 3945e4 cmp dword [var_1ch], eax
│ └──< 0x558471c62276 7cd8 jl 0x558471c62250
│ 0x558471c62278 488b45e8 mov rax, qword [var_18h]
│ 0x558471c6227c 4889c6 mov rsi, rax
│ 0x558471c6227f 488d3dc70d00. lea rdi, str.SUM:__ld ; 0x558471c6304d ; "SUM: %ld\n"
│ 0x558471c62286 b800000000 mov eax, 0
│ 0x558471c6228b e8c0fdffff call sym.imp.printf ; int printf(const char *format)
│ 0x558471c62290 488b45f0 mov rax, qword [var_10h]
│ 0x558471c62294 4889c7 mov rdi, rax
│ 0x558471c62297 e894fdffff call sym.imp.free ; void free(void *ptr)
│ 0x558471c6229c b800000000 mov eax, 0
│ 0x558471c622a1 488b4df8 mov rcx, qword [var_8h]
│ 0x558471c622a5 6448330c2528. xor rcx, qword fs:[0x28]
│ ┌─< 0x558471c622ae 7405 je 0x558471c622b5
│ │ 0x558471c622b0 e88bfdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x558471c622b5 c9 leave
└ 0x558471c622b6 c3 ret
[0x558471c62175]>
El malloc se realiza aquí:
│ ╎ 0x558471c621b8 e8b3feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ ╎ 0x558471c621bd 8b45e0 mov eax, dword [var_20h]
│ ╎ 0x558471c621c0 4898 cdqe
│ ╎ 0x558471c621c2 48c1e002 shl rax, 2
│ ╎ 0x558471c621c6 4889c7 mov rdi, rax
│ ╎ 0x558471c621c9 e892feffff call sym.imp.malloc ; void *malloc(size_t size)
Cargamos la entrada del usuario (var_20h) en eax y la multiplicamos por 4 con shl rax, 2. Como se puede ver, sizeof no se llama aquí porque el compilador ya conoce el tamaño de un entero. Así, la entrada del usuario multiplicada por 4 se pasa a malloc. Ejecutemos esto.
│ ╎ 0x558471c621ce b 488945f0 mov qword [var_10h], rax
│ ╎ 0x558471c621d2 48837df000 cmp qword [var_10h], 0
│ ┌──< 0x558471c621d7 7511 jne 0x558471c621ea
│ │╎ 0x558471c621d9 488d3d4b0e00. lea rdi, str.NO_SPACE_AVAILABLE. ; 0x558471c6302b ; "NO SPACE AVAILABLE."
│ │╎ 0x558471c621e0 b800000000 mov eax, 0
│ │╎ 0x558471c621e5 e866feffff call sym.imp.printf ; int printf(const char *format)
[0x558471c621ce]> dr rax
0x5584735cfa80
[0x558471c621ce]> dmhg
Heap Layout
┌────────────────────────────────────┐
│ Malloc chunk @ 0x5584735cf250 │
│ size: 0x410 status: allocated │
└────────────────────────────────────┘
v
│
│
┌────────────────────────────────────┐
│ Malloc chunk @ 0x5584735cf660 │
│ size: 0x410 status: allocated │
└────────────────────────────────────┘
v
│
│
┌────────────────────────────────────┐
│ Malloc chunk @ 0x5584735cfa70 │
│ size: 0x20 status: allocated │
└────────────────────────────────────┘
v
│
└──┐
│
┌───────────────────────────────┐
│ Top chunk @ 0x5584735cfa90 │
└───────────────────────────────┘
[0x558471c621ce]>
La función devuelve un puntero y se ha creado espacio en el heap. El puntero se guarda en var_10h.
│ ╎ 0x558471c621ce b 488945f0 mov qword [var_10h], rax
│ ╎ 0x558471c621d2 48837df000 cmp qword [var_10h], 0
Veamos ahora cómo se gestiona la memoria para la entrada del usuario:
│ ╎│ 0x558471c62213 8b45e4 mov eax, dword [var_1ch]
│ ╎│ 0x558471c62216 4898 cdqe
│ ╎│ 0x558471c62218 488d14850000. lea rdx, [rax*4]
│ ╎│ 0x558471c62220 488b45f0 mov rax, qword [var_10h]
│ ╎│ 0x558471c62224 4801d0 add rax, rdx
│ ╎│ 0x558471c62227 4889c6 mov rsi, rax
│ ╎│ 0x558471c6222a 488d3df70d00. lea rdi, [0x558471c63028] ; "%d"
│ ╎│ 0x558471c62231 b800000000 mov eax, 0
│ ╎│ 0x558471c62236 e835feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
Asumiendo que var_1ch es el contador del bucle, vemos cómo se mueve a eax y luego el CONTENIDO de rax*4 se carga en rdx y se suma a la dirección base del array dinámico. Siendo 4 el tamaño del entero, tiene todo el sentido: el contador avanzará de 4 en 4 por el array dinámico y asignará los nuevos valores en cada posición. La dirección correspondiente se pasa a scanf.
El siguiente bloque calcula la suma con las operaciones que ya hemos estudiado:
│ ┌──> 0x558471c62250 8b45e4 mov eax, dword [var_1ch]
│ ╎│ 0x558471c62253 4898 cdqe
│ ╎│ 0x558471c62255 488d14850000. lea rdx, [rax*4]
│ ╎│ 0x558471c6225d 488b45f0 mov rax, qword [var_10h]
│ ╎│ 0x558471c62261 4801d0 add rax, rdx
│ ╎│ 0x558471c62264 8b00 mov eax, dword [rax]
│ ╎│ 0x558471c62266 4898 cdqe
│ ╎│ 0x558471c62268 480145e8 add qword [var_18h], rax
│ ╎│ 0x558471c6226c 8345e401 add dword [var_1ch], 1
Se hace lo mismo pero esta vez var_18h almacena la suma.
Final de la vida del objeto
Al final del programa se llama a free. Me gusta esta respuesta de Stack Overflow: https://stackoverflow.com/questions/14986543/calling-free-in-c
free devuelve al asignador un bloque obtenido mediante una reserva compatible. A partir de esa llamada termina la vida del objeto: conservar el valor del puntero no permite seguir accediendo a él. La liberación tampoco garantiza que sus bytes se borren o que la región se devuelva inmediatamente al sistema operativo.
│ 0x558471c62290 488b45f0 mov rax, qword [var_10h]
│ 0x558471c62294 4889c7 mov rdi, rax
│ 0x558471c62297 e894fdffff call sym.imp.free ; void free(void *ptr)
Nótese que pasamos la dirección base de nuestro array dinámico a free.
Observar una escritura en memoria.
La sesión empieza en un breakpoint preparado antes de *pc = 'b'. Hermes lee el byte, ejecuta una sola instrucción y comprueba el cambio en la memoria del proceso.
Leer la explicación y los comandos
La consulta
Hemos detenido pointer-write justo antes de la asignación *pc = 'b'. Examina la instrucción actual y el byte al que apunta el registro. Ejecuta únicamente esa instrucción con ds y vuelve a leer el byte. Explica el cambio observado y conserva el proceso detenido.
Qué muestra la sesión
La instrucción mov byte [rax], 0x62 escribe en la dirección que contiene RAX. Antes del paso, esa posición contiene 0x63, el código ASCII de c.
Tras ejecutar ds, la misma dirección contiene 0x62, el código de b. RIP avanza a la instrucción siguiente y el proceso permanece detenido.
Al inicio, Hermes distingue la posición de análisis de radare2 del registro RIP del proceso. La comprobación utiliza los registros del depurador y la dirección numérica de RAX para consultar el byte correcto.
Comprobación desde la consola
Desde la consola del companion se revisan estas instrucciones y datos:
dr rip
dr rax
px 1 @ 0x7ffff0807fdb
pd 3 @ 0x40120a
El breakpoint lo prepara el docente antes de grabar. Para repetir la lectura, consulta dr rax y utiliza la dirección de tu proceso; la dirección de pila del vídeo pertenece a esa ejecución. Descargar el fuente del vídeo ↙ · Preparar el companion ↗
Vuelve al binario
Distinguir una dirección de su contenido y reconocer las reservas de pila y 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-write.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 9 --example pointer-write
./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.