Identificar promociones de enteros y conversiones entre enteros y coma flotante.
Notas de la práctica
movss transfiere un valor escalar de 32 bits. cvttss2si convierte un valor de coma flotante a entero con truncamiento. El uso de XMM describe la clase de operación; no significa que un float ocupe más que cualquier entero.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 5 --example float-castEstoy siguiendo «Tipos y conversiones» del curso de Lobera. Identificar promociones de enteros y conversiones entre enteros y coma flotante. 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.
Una conversión de tipos puede cambiar la representación de un dato o la forma de extenderlo a un registro mayor. En esta lección seguiremos conversiones entre float, int y char para reconocer sus instrucciones y comprobar el resultado.
De coma flotante a entero
Un cast expresa una conversión explícita mediante (tipo) expresión. El resultado depende de los tipos de origen y destino: convertir un número de coma flotante a entero, por ejemplo, puede descartar su parte fraccionaria.
Veamos un ejemplo sencillo:
# include <stdio.h>
main(){
func();
getchar();
}
func(){
int val = 2;
float a = 5.25;
int b = (int)a;
printf ("%d\n", b);
}
El desensamblado tiene este aspecto:
[0x55630491a6a4]> pdf
/ (fcn) sym.func 65
| sym.func ();
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; var int local_4h @ rbp-0x4
| ; CALL XREF from 0x55630491a693 (sym.main)
| 0x55630491a6a4 55 push rbp
| 0x55630491a6a5 4889e5 mov rbp, rsp
| 0x55630491a6a8 4883ec10 sub rsp, 0x10
| 0x55630491a6ac c745f4020000. mov dword [local_ch], 2
| 0x55630491a6b3 f30f1005bd00. movss xmm0, dword [0x55630491a778] ; [0x55630491a778:4]=0x40a80000
| 0x55630491a6bb f30f1145f8 movss dword [local_8h], xmm0
| 0x55630491a6c0 f30f1045f8 movss xmm0, dword [local_8h]
| 0x55630491a6c5 f30f2cc0 cvttss2si eax, xmm0
| 0x55630491a6c9 8945fc mov dword [local_4h], eax
| 0x55630491a6cc 8b45fc mov eax, dword [local_4h]
| 0x55630491a6cf 89c6 mov esi, eax
| 0x55630491a6d1 488d3d9c0000. lea rdi, qword [0x55630491a774] ; "%d\n"
| 0x55630491a6d8 b800000000 mov eax, 0
| 0x55630491a6dd e86efeffff call sym.imp.printf ; int printf(const char *format)
| 0x55630491a6e2 90 nop
| 0x55630491a6e3 c9 leave
\ 0x55630491a6e4 c3 ret
[0x55630491a6a4]>
Aparecen movss, cvttss2si y el registro xmm0. Los registros XMM tienen 128 bits; movss utiliza un valor escalar de precisión simple de 32 bits. En esta compilación, tanto float como int ocupan 4 bytes. La diferencia está en la representación y en la operación que realiza cada instrucción.
En una situación como esta tenemos dos opciones: interpretar las instrucciones una a una para deducir qué hace el programa, o establecer un breakpoint —por ejemplo, tras almacenar xmm0 en local_8h— e inspeccionar el contenido:
[0x5634635196a4]> pdf
/ (fcn) sym.func 65
| sym.func ();
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; var int local_4h @ rbp-0x4
| ; CALL XREF from 0x563463519693 (sym.main)
| 0x5634635196a4 55 push rbp
| 0x5634635196a5 4889e5 mov rbp, rsp
| 0x5634635196a8 4883ec10 sub rsp, 0x10
| 0x5634635196ac c745f4020000. mov dword [local_ch], 2
| 0x5634635196b3 f30f1005bd00. movss xmm0, dword [0x563463519778] ; [0x563463519778:4]=0x40a80000
| 0x5634635196bb f30f1145f8 movss dword [local_8h], xmm0
| 0x5634635196c0 f30f1045f8 movss xmm0, dword [local_8h]
| ;-- rip:
| 0x5634635196c5 b f30f2cc0 cvttss2si eax, xmm0
| 0x5634635196c9 8945fc mov dword [local_4h], eax
| 0x5634635196cc 8b45fc mov eax, dword [local_4h]
| 0x5634635196cf 89c6 mov esi, eax
| 0x5634635196d1 488d3d9c0000. lea rdi, qword [0x563463519774] ; "%d\n"
| 0x5634635196d8 b800000000 mov eax, 0
| 0x5634635196dd e86efeffff call sym.imp.printf ; int printf(const char *format)
| 0x5634635196e2 90 nop
| 0x5634635196e3 c9 leave
\ 0x5634635196e4 c3 ret
[0x5634635196a4]>
[0x5634635196a4]> pxw @ 0x563463519778
0x563463519778 0x40a80000 0x3b031b01 0x00000040 0x00000007 ...@...;@.......
[0x5634635196a4]> drt fpu
frip = 0x00000000
frdp = 0x00000000
st0 = 0x00000000
st1 = 0x00000000
st2 = 0x00000000
st3 = 0x00000000
st4 = 0x00000000
st5 = 0x00000000
st6 = 0x00000000
st7 = 0x00000000
xmm0h = 0x40a80000
xmm0l = 0x00000000
xmm1h = 0x31747361632f2e
xmm1l = 0x524f4c4f435f534c
xmm2h = 0xff00000000000000
xmm2l = 0x00000000
xmm3h = 0x0000ff00
xmm3l = 0x00000000
xmm4h = 0x2f2f2f2f2f2f2f2f
xmm4l = 0x2f2f2f2f2f2f2f2f
xmm5h = 0x00000000
xmm5l = 0x00000000
xmm6h = 0x00000000
xmm6l = 0x00000000
xmm7h = 0x00000000
xmm7l = 0x00000000
x64 = 0x00000000
[0x5634635196a4]>
Como se puede ver, primero local_ch se inicializa con 2; eso es sencillo: debe ser una variable int simple. Después, el contenido de 0x563463519778 se carga en el registro xmm0 y pasa a la variable local_8h. Si inspeccionamos el contenido del registro xmm0 con drt, veremos 0x40a80000, que corresponde a 5.25 en float.
rax2 Fx40a80000
5.250000f
Radare2 puede inferir tipos a partir del análisis. afta actualiza esa información; contrasta sus propuestas con el ancho de los accesos y las instrucciones que utilizan cada variable:
[0x558e5889c6a4]> afvt local_8h float
[0x558e5889c6a4]> pdf
/ (fcn) sym.func 65
| sym.func ();
| ; var int local_ch @ rbp-0xc
| ; var float local_8h @ rbp-0x8
| ; var int local_4h @ rbp-0x4
| ; CALL XREF from 0x558e5889c693 (sym.main)
| 0x558e5889c6a4 55 push rbp
| 0x558e5889c6a5 4889e5 mov rbp, rsp
[...]
[0x558e5889c6a4]>
Y con afvt var type podemos establecer el tipo de la variable manualmente. Respecto a las instrucciones utilizadas:
| 0x5634635196b3 f30f1005bd00. movss xmm0, dword [0x563463519778] ; [0x563463519778:4]=0x40a80000
| 0x5634635196bb f30f1145f8 movss dword [local_8h], xmm0
| 0x5634635196c0 f30f1045f8 movss xmm0, dword [local_8h]
| ;-- rip:
| 0x5634635196c5 b f30f2cc0 cvttss2si eax, xmm0
movss mueve un valor escalar de precisión simple entre registros XMM y operandos de memoria. cvttss2si convierte ese valor a entero truncando hacia cero: en este ejemplo, 5.25 se convierte en 5. El tamaño de transferencia y la conversión se deducen de la instrucción concreta.
Extensión de signo
Veamos ahora otro tipo de casting: esta vez iremos de carácter a entero.
# include <stdio.h>
main(){
func();
getchar();
}
func(){
char b = 'a';
int x = (int)b;
printf ("%d", x);
}
Si miramos el desensamblado...
[0x55b8bc8c96a4]> pdf
/ (fcn) sym.func 44
| sym.func ();
| ; var int local_5h @ rbp-0x5
| ; var int local_4h @ rbp-0x4
| ; CALL XREF from 0x55b8bc8c9693 (sym.main)
| 0x55b8bc8c96a4 55 push rbp
| 0x55b8bc8c96a5 4889e5 mov rbp, rsp
| 0x55b8bc8c96a8 4883ec10 sub rsp, 0x10
| 0x55b8bc8c96ac c645fb61 mov byte [local_5h], 0x61 ; 'a' ; 97
| 0x55b8bc8c96b0 0fbe45fb movsx eax, byte [local_5h]
| 0x55b8bc8c96b4 8945fc mov dword [local_4h], eax
| 0x55b8bc8c96b7 8b45fc mov eax, dword [local_4h]
| 0x55b8bc8c96ba 89c6 mov esi, eax
| 0x55b8bc8c96bc 488d3d910000. lea rdi, qword [0x55b8bc8c9754] ; "%d"
| 0x55b8bc8c96c3 b800000000 mov eax, 0
| 0x55b8bc8c96c8 e883feffff call sym.imp.printf ; int printf(const char *format)
| 0x55b8bc8c96cd 90 nop
| 0x55b8bc8c96ce c9 leave
\ 0x55b8bc8c96cf c3 ret
[0x55b8bc8c96a4]>
Este es muy sencillo; lo relevante ocurre aquí:
| 0x55b8bc8c96ac c645fb61 mov byte [local_5h], 0x61 ; 'a' ; 97
| 0x55b8bc8c96b0 0fbe45fb movsx eax, byte [local_5h]
| 0x55b8bc8c96b4 8945fc mov dword [local_4h], eax
| 0x55b8bc8c96b7 8b45fc mov eax, dword [local_4h]
El byte 0x61, correspondiente a a en ASCII, se guarda en local_5h. movsx extiende su signo al cargarlo en un registro de 32 bits y el resultado se almacena en la variable entera. Para este valor positivo se obtiene 97. El ancho de eax corresponde al destino de la conversión.
Representación y formato de salida
Terminemos con un último ejemplo.
# include <stdio.h>
main(){
func();
getchar();
}
func(){
char b = 'a';
int x = (int)b;
printf ("%d\n", x);
printf ("%d\n", b);
}
El último programa almacena a como char y como int, e imprime ambos con %d. El carácter se promociona a entero al pasarlo a printf, por lo que ambos muestran 97. En esta compilación char ocupa 1 byte e int ocupa 4; el formato de salida no cambia su almacenamiento.
Veamos.
[0x5570cc50c6a4]> pdf
/ (fcn) sym.func 67
| sym.func ();
| ; var int local_5h @ rbp-0x5
| ; var int local_4h @ rbp-0x4
| ; CALL XREF from 0x5570cc50c693 (sym.main)
| 0x5570cc50c6a4 55 push rbp
| 0x5570cc50c6a5 4889e5 mov rbp, rsp
| 0x5570cc50c6a8 4883ec10 sub rsp, 0x10
| 0x5570cc50c6ac c645fb61 mov byte [local_5h], 0x61 ; 'a' ; 97
| 0x5570cc50c6b0 0fbe45fb movsx eax, byte [local_5h]
| 0x5570cc50c6b4 8945fc mov dword [local_4h], eax
| 0x5570cc50c6b7 8b45fc mov eax, dword [local_4h]
| 0x5570cc50c6ba 89c6 mov esi, eax
| 0x5570cc50c6bc 488d3db10000. lea rdi, qword [0x5570cc50c774] ; "%d\n"
| 0x5570cc50c6c3 b800000000 mov eax, 0
| 0x5570cc50c6c8 e883feffff call sym.imp.printf ; int printf(const char *format)
| 0x5570cc50c6cd 0fbe45fb movsx eax, byte [local_5h]
| 0x5570cc50c6d1 89c6 mov esi, eax
| 0x5570cc50c6d3 488d3d9a0000. lea rdi, qword [0x5570cc50c774] ; "%d\n"
| 0x5570cc50c6da b800000000 mov eax, 0
| 0x5570cc50c6df e86cfeffff call sym.imp.printf ; int printf(const char *format)
| 0x5570cc50c6e4 90 nop
| 0x5570cc50c6e5 c9 leave
\ 0x5570cc50c6e6 c3 ret
[0x5570cc50c6a4]>
[0x5570cc50c6a4]> afvd
var local_5h = 0x7ffce6d687eb 0xd688000000006161 aa......
var local_4h = 0x7ffce6d687ec 0xe6d6880000000061 a.......
[0x5570cc50c6a4]> dc
97
97
Vuelve al binario
Identificar promociones de enteros y conversiones entre enteros y coma flotante. 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: float-cast.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 5 --example float-cast
./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.