Convenciones de llamadas a función en 64 bits - stefano-sosac/arquitectura-de-computadoras GitHub Wiki

Convenciones de llamada a función

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.

Transmisión de Argumentos

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.

Paso de Parámetros

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.

Caller y Callee

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.

Ejemplos

Producto interno

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

Norma-2

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.

Ejercicios

A continuación, se le invita a resolver la siguiente lista de ejercicios.

Ejercicio 1

Newton propuso la siguiente fórmula que permite aproximar la raíz cuadrada de un número 𝑥:

$$ \text{Guess}_{n+1} = \frac{1}{2} \left(\text{Guess}_n + \frac{x}{\text{Guess}_n}\right) $$

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 image

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

Ejercicio 2

Se tiene la siguiente ecuación:

$$ s = \sum_{n=1}^{N}{\frac{(n^{2}+1)}{a^b}} $$

La cual es una productoria en donde N, a y b ∈ $\mathbf{N}$. Además, se sabe que N es un número que como máximo debe ser 4 294 967 295, y que a y b serán restringidos a valores menores que 8 (podrían ser tan grandes como N, pero serán restringidos a un máximo de 8).

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.

⚠️ **GitHub.com Fallback** ⚠️