El ensamblador no esta muerto: lenguajes óptimos para ciertas tareas.

El PHP es un buen lenguaje pero es un lenguaje interpretado y para ciertas tareas puede compensar utilizar otro lenguaje de más alto nivel como el C/C++ o incluso programar rutinas en ensamblador. 

Fuente: https://www.flickr.com/photos/llemarie/
Fuente: https://www.flickr.com/photos/llemarie/

Dado que la mayoría de las distribuciones con las que trabajamos son UBUNTU podemos instalar el ensamblador de forma sencilla para crear objetos .o que pueden generar su propio ejecutable o usarse en otras rutinas de C o incluso en PHP mediante el comando “apt-get install nasm“.

Para conocer como programar en ensamblador en Linux por supuesto hay que tener conocimientos sobre lenguaje maquina (assembler) y sobre todo de arquitectura sobre todo en x86 y especialmente sobre las llamadas al núcleo(kernel) del sistema de LINUX mediante syscalls INT 0x80. Más info sobre llamadas al sistema aquí.

Y es que aunque parece un lenguaje que ya no se emplea en ciertos casos podemos acelerar los procesos de nuestro proyecto gracias a programar a un nivel tan bajo. Y es que se puede mediante el NASM generar código objeto ELF tipo .o y enlazar la programación en cualquier lenguaje o incluso crear subrutinas.

En AGENCIA LA NAVE sabemos programar en lenguaje maquina con lo que podemos llegar mucho más lejos que la competencia. En integrar código eficiente esta la diferencia.

A modo de ejemplo vamos a comparar este código en PHP:

<?php

  for($i=4294967295;$i<>0; $i--){
    // no hacer nada
  }

  echo "Hola que tal";

?>

Con esté código programado en ASM.

section .text ; parte definida del código del programa
global _start ; Se necesita poner para que se pueda usar LD
_start: ; Le dice al LD el punto de entrada para linkar

xor edx,edx ; un XOR exclusivo siempre da 0, 1xor1=0 => edx=0 
dec edx ; es un registro de 32 bits => edx=0xFFFFFFFF = máx valor
_bucle: ; definimos el punto del bucle 
dec edx ; decrementa EDX => edx=edx-1 afecta a los FLAGS de la CPUjne _bucle ; Jump if Not Equal to CERO => salta si no es EDX CERO
mov edx, len ;tamaño del mensaje definido area datos abajo  
mov ecx, msg ;mensaje a escribir con la llamada al sistema 
mov ebx, 1 ;file descriptor (stdout) (elegimos la salida) 
mov eax, 4 ;system call number (sys_write)(escribir en descriptor)
int 0x80 ;llamamos al kernel de linux  

mov eax, 1 ;system call number (sys_exit) => solicitamos terminar
int 0x80 ;llamamos al kernel de linux con la función EAX definida 

section .data ; esta es la parte de datos

msg db 'Que tal!',0xa ; una cadena que finaliza con un CR
len equ $ - msg ; tamaño de la cadena= POS-posiniciomensaje

Para general el código una vez ya tenemos el NASM instalado en nuestro ubuntu debemos hacer los siguientes pasos.

nasm -f elf sample.asm 2>&1
ld -m elf_i386 -s -o demo *.o 2>&1

Ahora probamos ambas programaciones para comprobar lo que tarda una y lo que tarda la otra: Ambas deben contar un número desde el máximo posible de 32 bits y al final ir restando hasta CERO y luego ya poner la cadena.

/home/test# ./demo
Que tal!
/home/test# time ./demo
Que tal!

real    0m4.360s
user    0m4.356s
sys     0m0.000s
/home/test# time php sample.php
real    9m36.492s
user    9m36.484s
sys     0m0.028s

[1]+  Hecho                   time php sample.php > tardon

La programación en ASM tarda unos 4,5 segundos mientras que la misma tarea realizada en PHP tarda en ejecutarse 9 minutos y 36 segundos. ¿Seguro que no es útil utilizar ensamblador para ciertas tareas?

El código en lenguaje maquina se puede de hecho integrar en el momento que lo necesitamos en ASM como se presenta en el siguiente ejemplo:

<?php

// cualquier tarea
system("/home/test/demo"); // llamada a un modulo en ASM(=>OPCODE)

?>

/home/test# time php sample2.php
Que tal!

real 0m4.376s
user 0m4.348s
sys 0m0.024s

Podemos ver que apenas retrasa integrar en PHP mediante un system ejecutar el código en lenguaje maquina de la parte que nos interese por ser ejecutada en dicho lenguaje. Puede interesar ejecutar ciertas partes de la programación en C que al ser más cercano a la maquina y crear ficheros objeto también aumenta la velocidad bastante con sistemas de optimización cada vez mejores.

Aún así conocer como funcionan los registros, la redirección de memoria, los flags y los comandos opcodes maquina puede ayudarnos a crear rutinas puntuales que optimicen ciertas partes del código de nuestra programación.

section  .data ;Segmento de datos
    userMsg db 'Introduzca un numero: ' ; Cadena de solicitud de numero
    lenUserMsg equ $-userMsg             ; Tamaño de la cadena
    dispMsg db 'Ha introducido : ' ; La cadena introducida
    lenDispMsg equ $-dispMsg       ; tamaño de la nueva cadena                

section .bss            ;segmento con datos que no se inicializan: Solo reserva memoria 
    num resb 5 ; reservamos 5 bytes para guardar los 5 caracteres que teclee el usuario

section .text           ;segmento de código
       global _start
_start:
       ;usamos la función syscall write (4) para escribir una cadena
       mov eax, 4 ; syscall write
       mov ebx, 1 ; destino => SALIDA stdout 
       mov ecx, userMsg ; indicamos posición de memoria cadena
       mov edx, lenUserMsg ; indicamos su tamaño 
       int 80h ; llamamos al núcleo de linux para hacer el print /echo

       ;Leemos aquí una cadena mediante la llamada READ y guardamos 5 caracteres 
       mov eax, 3 ; syscall read
       mov ebx, 2 ; de donde? stdin (2) entrada
       mov ecx, num  ; memoria dónde guardar el numero (5 bytes)
       mov edx, 5       ;5 bytes (numeric, 1 for sign) max 5 cars. 
       int 80h ; llamamos al núcleo de linux para hacer el READ de la entrada

       ;Presentamos con la misma llamada que arriba lo tecleado por el cliente
       ; cadena programada 
       mov eax, 4 ; syscall write
       mov ebx, 1 ; destino stdout
       mov ecx, dispMsg ; mensaje presentando
       mov edx, lenDispMsg ; donde esta la cadena
       int 80h  

       ;Presentamos los 5 caracteres
       mov eax, 4 ; syscall WRITE
       mov ebx, 1 ; stdout 
       mov ecx, num ; memoria dónde esta la parte leída
       mov edx, 5 ; 5 caracteres 
       int 80h  ; int 0x80 

       ; Hacemos EXIT a nivel de núcleo
       mov eax, 1 ; syscall exit 
       mov ebx, 0 ; sin devolver error 
       int 80h ; solicitamos finalizar

Podemos ver el resultado el ejecutable ocupa menos de 500 bytes. Si lo comparamos con un código en C que ocupa más de 750KB y eso que lo hemos compilado de forma estática. Incluso de forma dinámica ocupa más de 7500 bytes frente a los menos de 500 bytes en ASM. (Y hace la misma tarea).

#include <stdio.h>
int main (int argc,char **argv){
char cad[5];

printf("Introduzca un numero:\n");

fgets(cad, 5, stdin);

printf("La cadena introducida es: %s\n",cad);

return 0;

}

#Compilando de forma estática:

#sample2 es el código anterior enlazado en assembler)

/home/test# cc -o sample2c -static ejemplo.c
/home/test# ls -l sample2
-rwxr-xr-x 1 root root 496 may 13 10:15 sample2
/home/test# ls -l sample2c
-rwxr-xr-x 1 root root 751603 may 13 10:25 sample2c

Compilando ahora sin estatica (en versión dinamica)
cc -o sample2c ejemplo.c
/home/test# ls -l sample2c
-rwxr-xr-x 1 root root 7317 may 13 10:27 sample2c

Lo mejor del lenguaje ensamblador es que controlamos al 100% lo que hacemos si bien el siguiente nivel de optimización pasaría por no llamar al núcleo. En cualquier caso el sistema operativo debe controlar muchas tareas y mediante las llamadas podremos hacerlo absolutamente todo. Es interesante utilizar ensamblador para crear funciones de encriptación de datos o tareas complejas que deban ser optimizadas.

Al crearse objetos se pueden definir funciones en ASM para su uso en C o C++ y así accelerar código de forma eficiente. Os ponemos un ejemplo sencillo para que veáis como se debe crear el fichero .asm y el .c y como fusionarlos.

sumar.asm
; el ELF32 se pasa por PILA los contenidos de las variables
; El resultado se devuelve en elf32 en EAX (seria RAX si fuera de 64 bits)
; -----------------------------------------------------------------------------

        global  sumartres
        section .text
sumartres:
        mov     eax,[esp+4]
        add     eax,[esp+8]
        add     eax,[esp+12]
        ret                             ; the max will be in eax

Creamos el modulo: /home/test# nasm -f elf32 sumar.asm 
Ahora disponemos de sumar.o (con la funcion sumartres)

Creamos ahora codigo llamadasumar.c 
Indicamos en el código que definimos los parametros en 32 bits
#include 
#include 

uint32_t sumartres(uint32_t, uint32_t, uint32_t);

int main() {
    printf("%d\n", sumartres(1, 2, 3));
    printf("%d\n", sumartres(2, 3, 4));
    return 0;
}

Y ahora compilamos el código: 
/home/test# cc -o llamadasumar llamadasumar.c sumar.o

Si ejecutamos ahora el código podemos ver como funciona perfectamente. 
/home/test# ./llamadasumar
6
9

Cuando se emplean procesadores de 64 bits los parámetros de las funciones se pasan de forma diferente. Os recomiendo leer la guía de siguiente que os puede ayudar a entender porque se cambia de convención para pasar todos los parámetros posibles por los registros lo cual es más eficiente.

Esperamos que ahora os quedé más claro que conocer ASM no es una mala cosa y por supuesto puede ayudar a mejorar partes de una programación que no se ejecuta de forma óptima. Se puede mejorar el algoritmo pero también la forma en la se ejecuta en el procesador.

En AGENCIA LA NAVE trabajamos cada día para mejorar su web integrando las últimas tecnologías pero también usando herramientas que aunque llevan años en el sector pueden mejorar el rendimiento de su proyecto y lanzar lo al éxito.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *