Únete al equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Contacta con el equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Fundamentos de reversing / 06

Tipos y conversiones

LoberaFormación técnica06 / 33 · 4 min de lectura · Linux · x86 / x86-64
06 / 33

Identificar promociones de enteros y conversiones entre enteros y coma flotante.

Consultar conceptos ↗
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.

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:

C
# 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:

ENSAMBLADOR / REFERENCIA
[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:

ENSAMBLADOR / REFERENCIA
[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.

CÓDIGO / SALIDA
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:

ENSAMBLADOR / REFERENCIA
[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:

CÓDIGO / SALIDA
|           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.

C
# include <stdio.h>

main(){

 func();    
 getchar();
}

func(){

char b = 'a';

int x = (int)b;
printf ("%d", x);
}

Si miramos el desensamblado...

ENSAMBLADOR / REFERENCIA
[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í:

ENSAMBLADOR / REFERENCIA
|           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.

C
# 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.

ENSAMBLADOR / REFERENCIA
[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
Práctica manual

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.

TERMINAL / COMPANION
./coursectl start 5 --example float-cast
./coursectl console

Cierra la sesión anterior con ./coursectl stop antes de cambiar de laboratorio. Ver preparación.

Comprobación personal

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.

Código original: AGPLv3. Textos y diagramas originales: CC BY-SA 4.0. Reutilización y alcance ↗.

Lobera

Conceptos de reversing

Consulta una definición y continúa donde estabas.