Gcc Inline Assembly

Con lo que vimos anteriormente no logramos ning�n tipo de interacci�n con los operadores definidos por el resto del c�digo. Es esta versi�n "extendida" la que nos permitir� pasar valores inmediatos a registros, cargar registros con el contenido de variables, cargar variables con el contenido de registros, etc.

La sintaxis gen�rica es:

__asm__ ( "instrucciones" : lista_salida : lista_entrada : lista_destruida);

Donde:

lista_salida(o output list): contiene los registros, variables donde se guardara un dato.

lista_entrada (o input list): contiene los registros, variables o datos inmediatos que se utilizaran como entradas en una instrucci�n.

lista_destruida (o clobber list): contiene los registros que se ven modificados por las "instrucciones". Esta lista es necesaria para que el gcc sepa que registros debe utilizar, cuales debera resguardar, etc.

Si bien esto puede parecer un poco confuso, espero ir evacuando las dudas a trav�s de ejemplos.

.�Ejemplo 1

int main(void)
{
    int i=100;
    __asm__ ("movl %0, %%eax" : : "g" (i));
}

En ese fragmento lo que hacemos no es m�s que cargar el valor de i en el registro eax. N�tese que al utilizarse la versi�n "extendida" es necesario agregar otro operador "%" a los registros (Como ya en la versi�n basica del Inline Assembly anteponiamos el operador "%" a los registros, ahora deberemos agregar otro m�s, con lo que cada registro tendr� un "%%" antes de su nombre.

Volviendo al ejemplo anterior, si observamos, el modificador "g" (i) se encuentra en la lista de entrada, mientras que las lista de salida esta vac�a al igual que la clobber list.

Podriamos utilizar otro ejemplo levemente mas avanzado, para quien todav�a se encuentra perdido.

.�Ejemplo 2

#define MAX_VALOR       200
int main(void)
{
    int i=100;
    __asm__ ("movl %0, %%eax; movl %1, %%ebx" : :  "g" (i) , "g" (MAX_VALOR));
}

Aca nuevamente no utilizamos ni la lista de salida, ni la lista de registros destruidos. Simplemente cargamos en eax y en ebx el valor de i y 200 respectivamente.

�Que funci�n cumple el %0 y el %1 ? Estos son los valores que ser�n reemplazados din�micamente por Gcc. En caso de que agregaramos m�s instrucciones con mas par�metros a cargar, continuar�n con %2, %3, etc.

�Que representa el "g" ? Este modificador le indica a Gcc que el parametro que se pasa entre par�ntesis puede ser tanto una variable en memoria como un valor inmediato. Si bien el modificador "g" es uno de los m�s utilizados (al menos por mi) hay muchos otros. S�lo intentare explicar algunos, pueden consultar el resto en el manual.

.�Modificadores

A continuaci�n se listan los modificadores m�s usados en el ensamblado extendido. Existen otros m�s, incluyendo algunos propios de ciertas arquitecturas.

  • "a" eax
  • "b" ebx
  • "c" ecx
  • "d" edx
  • "S" esi
  • "D" edi
  • "q" o "r" cualquier registro de prop�sito general (a conveniencia de gcc)
  • "I" valor inmediato (entre 0 y 31)

Voy a intentar clarificar un poco esto a trav�s de unos ejemplos. Empecemos por uno sin mucha utilidad pr�ctica, pero que a nuestros fines nos sirve:

.�Ejemplo 3

int main(void)
{
    int i=100;
    __asm__ ("mov %0, %%ebx" : : "c" (i));
}

Aqui el modificador "c" indica al Gcc que el valor de i deber� ser cargado en el registro ECX, el cual ser� colocado en el lugar de %0. En pocas palabras, en este caso, cargamos el valor de i en EBX, utilizando a ECX como registro de paso.

Pasemos a un ejemplo m�s real y �til.

.�Ejemplo 4

La siguiente es una implementaci�n de la funci�n memcpy utilizando inline assembly (Sacada de Routix).

void *memcpy( void *dest, const void *src, unsigned int n)
{
    __asm__("cld ; rep ; movsb":  : "c" ((unsigned int) n), "S" (src), "D" (dest));
    
    return dest;
}

Recordemos que la instrucci�n movsb mueve el contenido de esi a edi, e incrementa o decrementa los valores de ESI y EDI (seg�n el flag de direcci�n, modificado en este caso por CLD). La instrucci�n REP hace que se repita esto mientras ECX no sea 0. Tampoco en esta ocasi�n utilizamos la lista de salida, ni la de registros destruidos. El modificador "c" indica que el registro de destino ser� ECX, mientras que "S" y "D" lo hacen con ESI y EDI respectivamente.

.�Modificador "r"

El modificador "r" como ya comentamos con anterioridad le indica a Gcc que puede utilizar cualquier registro. Esto es �til ya que en muchas oportunidades no es necesario usar un registro en particular, y cualquiera que se utilice puede realizar bien la tarea. Quiz� para alguien con no mucha experiencia mezclando c�digo C y assembly le venga bien la siguiente explicaci�n. Como vimos antes, podemos ver el c�digo ensamblador generado por Gcc utilizando la opci�n -S. All� pudimos observar que los accesos a variables se realizan utilizando registros de prop�sito general. �Que pasar�a si el c�digo en C guardara un valor en EAX para un futuro acceso a una variable y nosotros, muy despreocupadamente, utilizamos un

__asm__ ("movl %0, %%cr3" : : "a" (i) )

? Tal cual lo imaginamos, vamos a estar pisando el valor que pensaba utilizar Gcc con lo cual el comportamiento del programa es ahora totalmente impredecible. Es en este (y en otros) casos donde el modificador "r" nos ayuda a evitar esos problemas, ya que deja que Gcc elija un registro que no esta utilizando. Otra forma de evitar este tipo de inconvenientes es usar la denominada "clobber list" (la cual nombre anteriormente como lista de registros destruidos).

Veamos otro ejemplo sacado del c�digo fuente de Routix, el cual muestra el uso del modificador "r"

#define load_cr3(x) __asm__ ("movl %0, %%cr3" : : "r" (x) )

Esta macro carga un nuevo directorio de paginas. No importa realmente si usted no sabe que es la paginaci�n, simplemente piense en CR3 como un registro m�s, el cual no puede ser cargado de forma inmediata (es decir, requiere ser cargado a trav�s de otro registro). Este caso es un perfecto ejemplo el cu�l deja a Gcc utilizar el registro de prop�sito general que tenga disponible.

.�Clobber List

Ya hemos dado una explicaci�n de porque es importante incluir los registros que modifica nuestro c�digo en la clobber list, por lo que ahora vamos a apoyar esa teor�a con algunos ejemplos:

El siguiente ejemplo esta sacado tambi�n del c�digo fuente de Routix. Es la interfaz en modo usuario de la llamada al sistema exec (su comportamiento es ligeramente diferente a la del estandar Unix). Eleg� este fragmento de c�digo porque aporta informaci�n sobre el uso no solo de la clobber list (o lista de registros destruidos) si no tambi�n de la lista de salida.

int exec (char *tarea)
{
    __asm__ __volatile__ ("movl %0, %%eax ; movl %1, %%ebx" :  : "g" \
                (SYS_PROCESS | SYS_EXEC) , "g" (tarea) : "eax" , "ebx");
    __asm__ __volatile__ ("int $0x50");

     int retorno;
    __asm__ __volatile__ ("movl %%eax, %0" : "=g" (retorno)  );
 
    return retorno;
}

La primera linea no hace m�s que cargar en el registro EAX el n�mero de llamada al sistema correspondiente, y en EBX la direcci�n de un string que posee el path del programa a ejecutar. Lo que aporta de nuevo esta funci�n es que al final incluye un: "eax", "ebx" que corresponde a la clobber list. Con este agregado le estoy diciendo expl�citamente a gcc que los registros EAX y EBX estan siendo modificados para que �l pueda tomar los recaudos necesarios. (Tanto SYS_PROCESS como SYS_EXEC son macros, las cuales son consideradas como valores inmediatos).

La segunda l�nea hace pasar al modo kernel v�a la interrupci�n 0x50.

El �ltimo fragmento agrega el modificador "=g" en la lista de salida. Hasta ahora hab�amos pasado valores de variables a registros, pero nunca en sentido inverso, es en este �ltimo caso donde la output list (o lista de salida) entra en juego. La linea

__asm__ __volatile__ ("movl %%eax, %0" : "=g" (retorno)  )

mueve el valor del registro EAX a lo que representa el %0, que en este caso es la variable retorno. Es evidente el agregado del signo "=" antes de la "g", el cual es necesario en el modificador usado en la lista de salida.

Veamos alg�n ejemplo m�s de la lista de salida.

Este ejemplo es la implementaci�n de la funci�n inportb en Routix:

unsigned char inportb(word puerto )
{
    unsigned char valor;

    __asm__ __volatile__("inb %w1,%b0" : "=a" (valor) : "d" (puerto) );
    return valor;
}

Aca podemos ver algunos agregados m�s. No se si habr�n notado que siempre nos manejamos con datos de 4 bytes, es decir con el registro completo. No siempre es necesario esto, en algunos casos alcanza con un word (AX) y en otro incluso con un byte (AL). El caso de la instrucci�n INB (o IN) es un claro ejemplo. El n�mero de puerto es un entero de 2 bytes, mientras que el valor le�do de �l es de tan solo 1. El operador %w le dice al gcc que el dato que recibe es un word (2 bytes), mientras que el %b hace lo respectivo a 1 byte.

Si compilamos este ejemplo con gcc -S obtenemos

_inportb:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $4, %esp
        movl    8(%ebp), %eax
        movw    %ax, -2(%ebp)
        movw    -2(%ebp), %dx
/APP
        inb %dx,%al
/NO_APP
        movb    %al, -3(%ebp)
        movl    $0, %eax
        movb    -3(%ebp), %al
        leave
        ret

donde podemos comprobar que realmente el gcc utiliza los registros parciales DX y AL.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
SIGUIENTE ARTÍCULO