Convenciones de llamadas a función en 64 bits - stefano-sosac/arquitectura-de-computadoras GitHub Wiki
La mayoría de funciones tienen parámetros. Los parámetros nos permiten que una función opere con datos distintos en cada llamada que se realiza. Adicionalmente, una función puede tener un valor de retorno como indicador de éxito o error. Los sistemas operativos Linux x86-64 un protocolo de llamadas a función llamado System V Application Binary Interface, o también System V ABI. El protocolo que se emplea depende del sistema operativo, pero todos los protocolos tienen en común que juntan el uso de registros de propósito general con el uso ocasional de la pila. Los sistemas operativos Linux permiten que se pasen hasta 6 parámetros enteros en registros y 8 parámetros en coma flotante mediante registros, mientras que Windows solo permite 4 enteros y 4 en coma flotante. Un elemento común de ambos protocolos es que emplean rax
como registro para el valor de retorno para enteros y xmm0
para coma flotante.
Es como se le denomina al envío de información a una función y a la obtención adecuada de un resultado de dicha función. La terminología estándar para transmitir valores a una función es call-by-value, mientras que para transmitir direcciones es call-by-reference. Hay varias maneras de pasar argumentos a una función, pero las más usadas son las siguientes:
- Colocar valores o direcciones en un registro.
- Definir variables globales.
- Colocar valores o direcciones en la pila.
Como se mencionó anteriormente los parámetros pueden ser pasados a una función mediante el uso de registros o de la pila. La siguiente tabla muestra cuales son los registros que se usan cuando enteros (char
, short
, int
, long
) o flotantes (float
, double
).
En la tabla se puede apreciar los registros que se corresponden con los argumentos de una función:
Posición del argumento | Entero | Flotante |
---|---|---|
Primero | rdi | xmm0 |
Segundo | rsi | xmm1 |
Tercero | rdx | xmm2 |
Cuarto | rcx | xmm3 |
Quinto | r8 | xmm4 |
Sexto | r9 | xmm5 |
Séptimo | xmm6 | |
Octavo | xmm7 |
En el siguiente ejemplo se muestra una función llamada myfunction
y sus argumentos. En el comentario se indica en que registro va cada parámetro de la función.
extern void my_function(char a, short b, float c, double *d, double e)
// a en rdi, b en rsi, c en xmm0, d en rdx, e en xmm1
En caso la función tenga más de seis parámetros enteros y de 8 parámetros flotantes, los parámetros adicionales serán pasados a la función por de la pila. ↑
Dadas dos funciones foo
y bar
, una situación en que la función foo
llama a la función bar
, se dice que la función foo
es el caller y que la función bar
es el callee. El uso de los registros no estará limitado al paso de argumentos y su modificación deberá tomar en cuenta el rol de las funciones durante la ejecución del programa. Por ejemplo, los registros usados para pasar los primeros 6 argumentos enteros, y para devolver el valor son caller-saved, por esto el callee puede disponer libremente de estos registros sin tomar precausión alguna. Si rax
contiene un valor que el caller desea preservar, el caller debe copiar el valor de rax
a un lugar seguro antes de realizar la llamada a función. En contraste, si el callee desea usar algún registro que sea callee-saved deberá preservar su valor en algún lugar seguro y restaurarlo antes de salir de la llamada a función.
En la siguiente table se muestran los usos convencionales para cada registro según el rol de la función:
Registro | Uso convencional |
---|---|
rax | caller-saved |
rdi | caller-saved |
rsi | caller-saved |
rdx | caller-saved |
rcx | caller-saved |
r8 | caller-saved |
r9 | caller-saved |
r10 | caller-saved |
r11 | caller-saved |
rsp | callee-saved |
rbx | callee-saved |
rbp | callee-saved |
r12 | callee-saved |
r13 | callee-saved |
r14 | callee-saved |
r15 | callee-saved |
Tenga presente que la función main
también puede asumir el rol de caller. ↑
Código en ensamblador para calcular el producto interno de dos vectores.
global asmFloatInnerProd
section .text
asmFloatInnerProd:
xorpd xmm0, xmm0
xorpd xmm1, xmm1
xorpd xmm2, xmm2
cmp rdx, 0
je done
next:
movss xmm0, [rdi]
movss xmm1, [rsi]
mulss xmm0, xmm1
addss xmm2, xmm0
add rdi, 4
add rsi, 4
sub rdx, 1
jnz next
done:
movss [rcx], xmm2
ret
Código en C que calcula el producto interno de dos vectores con una función propia. El programa principal llama a la función hecha en ensamblador y a la función hecha en C. También imprime el tiempo en nanosegundos que toma cada función, y el error relativo del resultado en ensamblador considerando como referencia el valor calculado en C.
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
extern void asmFloatInnerProd(float *v1, float *v2, int N, float *ip);
void initVector(float *v, int N);
void cFloatInnerProd(float *v1, float *v2, int N, float *ip);
float calcRelErr(float ref, float cal);
int main()
{
// semilla para los números aleatorios
srandom(time(NULL));
float *v1, *v2, ipC, ipAsm;
int N = 1024;
v1 = malloc(N * sizeof(float));
v2 = malloc(N * sizeof(float));
int i = 0;
initVector(v1, N);
initVector(v2, N);
struct timespec ti, tf;
double elapsed;
clock_gettime(CLOCK_REALTIME, &ti);
cFloatInnerProd(v1, v2, N, &ipC);
clock_gettime(CLOCK_REALTIME, &tf);
elapsed = (tf.tv_sec - ti.tv_sec) * 1e9 + (tf.tv_nsec - ti.tv_nsec);
printf("el tiempo en nanosegundos que toma la función en C es %lf\n", elapsed);
clock_gettime(CLOCK_REALTIME, &ti);
asmFloatInnerProd(v1, v2, N, &ipAsm);
clock_gettime(CLOCK_REALTIME, &tf);
elapsed = (tf.tv_sec - ti.tv_sec) * 1e9 + (tf.tv_nsec - ti.tv_nsec);
printf("el tiempo en nanosegundos que toma la función en ASM es %lf\n", elapsed);
float relerr = calcRelErr(ipC, ipAsm);
printf("el error relativo es %f\n", relerr);
free(v1);
free(v2);
return 0;
};
void initVector(float *v, int N)
{
for (int i = 0; i < N; i++)
{
float e = random() % 255;
v[i] = (sinf(e) + cosf(e));
}
}
void cFloatInnerProd(float *v1, float *v2, int N, float *ip)
{
int i = 0;
float sum = 0;
for (i = 0; i < N; i++)
{
sum += v1[i] * v2[i];
}
ip[0] = sum;
}
// error relativo de escalares:
// la idea es
// calcular el valor absoluto de la diferencia de las entradas
// calcular el valor absoluto de la referencia
// y dividir el primer valor entre el segundo
// a ese resultado se le llama el error relativo de cal respecto de ref
// mientras menor sea el resultado, mejor
float calcRelErr(float ref, float cal)
{
return fabsf(ref - cal) / fabsf(ref);
}
Para crear el ejecutable:
nasm -f elf64 asmFloatInnerProd.asm -o asmFloatInnerProd.o
gcc asmFloatInnerProd.o floatInnerProd.c -o floatInnerProd -lm
Para probar el programa:
./floatInnerProd
Código en ensamblador que calcula norma-2 de un vector de floats. A esta operación, a veces, le llaman "valor absoluto de un vector", y se suele usar como operación previa para calcular el error relativo entre dos vectores.
global asmFloatNormTwo
section .text
asmFloatNormTwo:
xorpd xmm0, xmm0
xorpd xmm1, xmm1
cmp rsi, 0
je done
next:
movss xmm0, [rdi]
mulss xmm0, xmm0
addss xmm1, xmm0
add rdi, 4
sub rsi, 1
jnz next
done:
sqrtss xmm1, xmm1
movss [rdx], xmm1
ret
Código en C para realizar las comparaciones necesarias.
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
extern void asmFloatNormTwo(float *v1, int N, float *n2);
void cFloatNormTwo(float *v1, int N, float *n2);
int main() {
float *v1, n2C, n2Asm;
int N = 1024;
v1 = malloc(N * sizeof(float));
int i = 0;
for(i = 0; i < N; i++){
v1[i] = (float)i;
}
cFloatNormTwo(v1, N, &n2C);
asmFloatNormTwo(v1, N, &n2Asm);
printf("%f\n%f\n",n2C,n2Asm);
free(v1);
return 0;
};
void cFloatNormTwo(float *v1, int N, float *n2) {
int i = 0;
float sum = 0;
for(i = 0; i < N; i++) {
sum += v1[i] * v1[i];
}
n2[0] = sqrtf(sum);
}
El ejecutable se puede crear con comandos similares a los del ejemplo anterior, solo tendría que usar los nombres correspondientes a los archivos del ejemplo. Como ejercicio, se le sugiere calcular el error relativo de los resultados obtenidos en C y ensamblador, hacer que el vector inicie con valores aleatorios entre -1.0 y 1.0 aprox, y que mostrar los tiempos de ejecución en microsegundos, así como el error calculado.
A continuación, se le invita a resolver la siguiente lista de ejercicios.
Newton propuso la siguiente fórmula que permite aproximar la raíz cuadrada de un número 𝑥:
La fórmula mostrada se simplifica bastante si uno ya conoce la raíz cuadrada de un valor cercano a 𝑥; sin embargo, esto no es tan directo si se desea realizar el cálculo para cualquier valor.
Una forma de garantizar llegar al resultado es partiendo de que 𝐺𝑢𝑒𝑠𝑠 y 𝑛 = 1.0 luego verificar si |𝐺𝑢𝑒𝑠𝑠_{𝑛}-𝐺𝑢𝑒𝑠𝑠_{𝑛+1}| < 𝑝𝑟𝑒𝑐𝑖𝑠𝑖ó𝑛. De no cumplirse lo anterior, se actualizará 𝐺𝑢𝑒𝑠𝑠_{n+1} = 𝐺𝑢𝑒𝑠𝑠_{n}.
Por ejemplo, calcular la raíz cuadrada de 15 con una precisión de 0.15, seguiría los siguientes pasos
Se le solicita realizar un programa en C y una función en lenguaje ensamblador de 64 bits que permita obtener la raiz cuadrada de cualquier número con una precisión dada. Tenga en cuenta las siguientes consideraciones:
- El número 𝑥 (no entero) y la precisión serán predefinidos en el código en C; es decir, serán los argumentos de sus funciones.
- Necesariamente deberá utilizar el Método de Newton.
- Puede utilizar alguna función que calcule el valor de la raíz cuadrada para verificar el resultado.
Se le pide:
a) Implementar la función en ASM que permita calcular la raíz cuadrada por el Método de Newton.
b) Implementar la función en C que permita calcular la raíz cuadrada por el método de Newton.
c) Mostrar para los números 2000 y 15000 con una precisión de 0.001
d) Evaluar los tiempos de ejecución de C y ASM
Se tiene la siguiente ecuación:
La cual es una productoria en donde N, a y b ∈
Se le pide realizar un análisis comparativo entre funciones de C y ASM que implementen la ecuación presentada.
a) Implementar la ecuación en C sin hacer uso de pow
b) Implementar la ecuación en C haciendo uso de la función pow
c) Implementar la ecuación en ASM
d) Medir los tiempos de ejecución para el máximo valor de N. Asignar los valores de a y b que considere conveniente. Comparar los tiempos de ejecución y validar qué implementación presenta mejores resultados.