C nos permite utilizar punteros, mediante estas variables podremos almacenar direcciones de memoria en vez de datos directamente, posteriormente se puede hacer referencia a estas direcciones de memoria para escribir o leer en ellas.
Antes de comenzar es recomendable que leas estos artÃculos anteriores:
- Boot Sector
- Interrupciones
- Memoria
- Pila
- IF-ELSE
- Funciones
- Segmentación de memoria
- Lectura de datos desde disco
- Entrando a modo protegido 32bits
- Compilación, linkado, gestión de la pila y variables en C
Técnicamente hablando todos los punteros son direcciones de memoria de 32 bits, pero según el tipo de dato que vayamos a leer/escribir en esa dirección de memoria tendremos que definir de un modo u otro nuestro puntero.
La definición de un puntero a la dirección de memoria de video serÃa:
char* video_address = 0xb8000;
Si queremos escribir en la dirección de memoria a la que apunta el puntero:
*video_address = 'X';
Veamos como se traduce un puntero a ASM:
void my_function();
int main(int argc, char *argv[])
{
my_function();
}
void my_function() {
int var1 = 9;
int* pointer = &var1;
}
Definimos una variable de tipo entero var1, le asignamos un valor y luego definimos un puntero que apunta a la posición de memoria donde está el valor de var1.
Compilamos:
Cargamos en GDG:
Veamos nuestra función en ASM:
gef➤  disassemble my_function
Dump of assembler code for function my_function:
=> 0x0000000000201300 <+0>: push rbp
0x0000000000201301 <+1>: mov rbp,rsp
0x0000000000201304 <+4>: mov DWORD PTR [rbp-0x4],0x9
0x000000000020130b <+11>: lea rax,[rbp-0x4]
0x000000000020130f <+15>: mov QWORD PTR [rbp-0x10],rax
0x0000000000201313 <+19>: pop rbp
0x0000000000201314 <+20>: ret
End of assembler dump.
Ponemos un breakpoint justo antes de llamar a nuestra función:
gef➤  l
1 void my_function();
2
3 int main(int argc, char *argv[])
4 {
5 my_function();
6 }
7
8 void my_function() {
9 int var1 = 9;
10 int* pointer = &var1;
gef➤  b 5
Corremos el programa:
gef➤  r
Damos unos pasos mas con la orden “si” hasta llegar a:
0x0000000000201304 <+4>: mov DWORD PTR [rbp-0x4],0x9
Con esta instrucción estamos asignando 9 a la dirección de memoria bp-0x4, “creando” la variable var1 en el stackframe.
Seguimos adelante:
gef➤  si
0x000000000020130b <+11>: lea rax,[rbp-0x4]
Con esta instrucción estamos guardando la dirección bp-0x4 en el registro ax, esta dirección es donde se encuentra el valor de var1, justo la definición de puntero.
Seguimos adelante:
gef➤  si
→ 0x20130f <my_function+15> mov QWORD PTR [rbp-0x10], rax
Con esta instrucción estamos “creando” la variable pointer con el valor de ax, si consultamos el valor del puntero(bp-0x10).
gef➤  x/10x $rbp-0x10
0x7fffffffe0e0: 0xffffe0ec 0x00007fff 0x00000001 0x00000001
0x7fffffffe0f0: 0xffffe110 0x00007fff 0x002012f4 0x00000000
0x7fffffffe100: 0xffffe180 0x00007fff
La dirección es: 0x7fffffffe0ec
Si consultamos el contenido de dicha dirección:
gef➤  x/10x 0x7fffffffe0ec-0x10
0x7fffffffe0dc: 0x00000008 0xffffe0ec 0x00007fff 0x00000001
0x7fffffffe0ec: 0x00000009 0xffffe110 0x00007fff 0x002012f4
0x7fffffffe0fc: 0x00000000 0xffffe180
Obtenemos el 9: 0x00000009.
Los punteros se pueden definir directamente a la dirección de memoria donde el compilador guarde la variable, el siguiente ejemplo creará un puntero con el valor de la dirección de memoria donde el compilador ha decidio meter el string.
void my_function();
int main(int argc, char *argv[])
{
my_function();
}
void my_function() {
char* my_string = "Hello";
}
Lo compilamos y lo cargamos en GDB:
gdb -q test3
El código en ASM de nuestra función es el siguiente:
gef➤  disassemble my_function
Dump of assembler code for function my_function:
0x0000000000201300 <+0>: push rbp
0x0000000000201301 <+1>: mov rbp,rsp
0x0000000000201304 <+4>: movabs rax,0x200490
0x000000000020130e <+14>: mov QWORD PTR [rbp-0x8],rax
0x0000000000201312 <+18>: pop rbp
0x0000000000201313 <+19>: ret
End of assembler dump.
La dirección de memoria donde se guarda nuestro string ha sido calculada por el compilador: 0x200490, expliquemos paso a paso como genera el puntero.
0x0000000000201304 <+4>: movabs rax,0x200490
Asigna la dirección de memoria donde se encuentra nuestro string al registro ax.
0x000000000020130e <+14>: mov QWORD PTR [rbp-0x8],rax
Crea la variable en el stackframe y le asigna el valor de ax.
Si ejecutamos paso a paso el programa mediante la orden “si” y paramos en:
0x0000000000201312 <+18>: pop rbp
Podremos consultar el valor de la variable:
gef➤  x/10x $rbp-0x8
0x7fffffffe0e8: 0x00200490 0x00000000 0xffffe110 0x00007fff
0x7fffffffe0f8: 0x002012f4 0x00000000 0xffffe180 0x00007fff
0x7fffffffe108: 0xffffe190 0x00000001
El valor del puntero es: 0x00200490
Consultamos esa dirección de memoria:
gef➤  x/10x 0x00200490-10
0x200486: 0x00000000 0x00000000 0x65480000 0x006f6c6c
0x200496: 0x1b010000 0x00343b03 0x00050000 0x0b680000
0x2004a6: 0x00500000 0x0c880000
Hello en hexadecimal es 48 65 6c 6c 6f, en el dump de memoria podemos verlo: 0x65480000 0x006f6c6c
Otra manera quizás mas rápida de verlo es ya que ax ha sido copiado a bp-0x8 podemos consultar directamente ax obteniendo el valor de la dirección de memoria a la que apunta en ASCII:
gef➤  registers $rax
$rax : 0x0000000000200490 → 0x0000006f6c6c6548 ("Hello"?)
La salida anterior indica que ax tiene como valor  0x00200490 y en esta dirección de memoria encontramos el valor 0x6f6c6c6548 que en ASCII es Hello.
En resumidas cuentas un puntero es una variable que no mantiene un valor en sà misma, si no la dirección de memoria donde está el valor final que realmente nos interesa.