Compilación, Compilación Cruzada y Toolchains
PROGRAMACIÓN
Compilación, Compilación Cruzada y Toolchains
2017-03-17
Por
Andrés "Andy" Pajaquer

Estamos seguros de que nuestros ávidos lectores han compilado cientos de programas. En general, no necesitamos tener ni idea de como funciona el proceso. Nuestro querido ./configure && make && make install funciona perfectamente. Pero a veces, necesitamos ir un poquito más allá... por ejemplo cuando necesitamos compilar algo para otra plataforma.
Si sois lectores de esta nuestra humilde publicación, estamos seguros de que en algún momento os vais a ver en la tesitura de compilar algo para alguna plataforma rara. Las cosas han mejorado mogollón en los últimos años, pero aún hoy en día (en pleno 2017 :), de vez en cuando, nos encontramos con alguna plataforma con un soporte limitado. Muchas veces esa plataforma no es muy popular, otras veces es muy nueva y las herramientas habituales no están todavía niqueladas...

En esos casos, creedme, el saber como funciona todo esto del proceso de compilación os puede ahorra muchos quebraderos de cabeza.

Todo el artículo está desarrollado en torno a ejemplos prácticos de forma que podáis ir probando cada una de las cosas que os contamos según vayáis leyendo. Así que sin más... vamos al temita!

Compilación

Vamos a empezar viendo lo que pasa cuando compilamos un programa. Tomemos, por ejemplo, el infame "Hola Mundo". Aquí tenéis nuestra versión:

#include <stdio.h>

int
main ()
{
	printf ("Que pasaaa güorl!\n");
	return 0;
}
El infame Hola Mundo... con un twist

Como todos sabéis podemos compilarlo utilizando gcc con un comando tal que asín:

gcc -o hola hola.c

Hasta aquí todo bien. Tenemos un programa llamado gcc que toma código fuente en C y genera un ejecutable.... Mal!

Veamos que es lo que realmente está pasando. Para ello utilizaremos la utilidad strace. strace nos permite ver que llamadas al sistema realiza un determinado programa. Os adelantamos que gcc no es el único responsable de generar el ejecutable... como todos nosotros, él también necesita ayuda de vez en cuando y nosotros vamos a enterarnos de quienes son sus compinches.

strace -f -e trace=execve gcc -o hola hola.c 2> traza-execve.txt

Este comando va a volcar en un fichero todas las llamadas al sistema execve que se producen cuando compilamos nuestro programa.

En el hipotético caso de que alguno no lo sepa, la forma de ejecutar programas en un sistema UNIX requiere dos llamadas al sistema. Lo primero que hacemos es ejecutar fork, que nos permite crear un nuevo proceso (un nuevo programa) que será una copia exacta del proceso que ejecuta la llamada fork. Luego utilizamos la llamada al sistema execve para cargar el código que queremos que ejecute ese nuevo proceso.

La utilidad strace, por defecto, solo mira en el proceso actual. Si el proceso ejecuta fork y crea un nuevo proceso, éste no aparecerá en la salida de strace, a no ser que utilicemos el flag -f. Con este flag le indicamos a strace que también mire que es lo que hace cualquier nuevo proceso creado durante la monitorización del programa inicial. Si el flag -f no veríamos gran cosa... simplemente probadlo.

Compinches

Es hora de echa un ojo a la salida de nuestro amigo strace. Aún cuando le hemos dicho que solo estamos interesados en las llamadas a execve, strace muestra alguna información adicional (se podría eliminar pero añadiendo unos cuantos flags más). Para facilitar nuestro análisis vamos a filtrar el fichero usando awk de forma que solo el primer parámetro de execve sea mostrado. Por qué awk?... Porque es super alucinante!

tulchein@dev $ grep execve traza-execve.txt
$  awk 'BEGIN{FS=","} /execve/{print $1}' trace-execve.txt
execve("/usr/bin/gcc"
[pid  8726] execve("/usr/lib/gcc/x86_64-linux-gnu/4.6/cc1"
[pid  8727] execve("/usr/lib/lightdm/lightdm/as"
[pid  8727] execve("/usr/local/sbin/as"
[pid  8727] execve("/usr/local/bin/as"
[pid  8727] execve("/usr/sbin/as"
[pid  8727] execve("/usr/bin/as"
[pid  8728] execve("/usr/lib/gcc/x86_64-linux-gnu/4.6/collect2"
[pid  8729] execve("/usr/bin/ld"

Bueno, dejando a un lado los distintos paths que intenta gcc para localizar herramientas como as, en la salida del comando podemos ver las siguientes herramientas:

  • cc1. Este es el compilador C. En realidad es un traductor de lenguaje C a lenguaje ensamblador
  • as. Este es el ensamblador de GNU (ASsembler). Este programa convierte programas en lenguaje ensamblador en código objeto
  • collect2: Este es un programa que gestiona constructores en el código en la fase de enlazado.
  • ld: Este es el enlazador (Linker), el encargado de producir el ejecutable final.

Veamos como funciona todo esto. Primero vamos a convertir nuestro código C en ensamblador usando cc1:

$ /usr/lib/gcc/x86_64-linux-gnu/4.6/cc1 hola.c
 main
Analyzing compilation unit
Performing interprocedural optimizations
 <*free_lang_data>    Assembling functions:
 main
Execution times (seconds)
 preprocessing         :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.01 (50%) wall     130 kB ( 9%) ggc
 lexical analysis      :   0.00 ( 0%) usr   0.01 (100%) sys   0.00 ( 0%) wall       0 kB ( 0%) ggc
 parser                :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.01 (50%) wall     299 kB (20%) ggc
 TOTAL                 :   0.00             0.01             0.02               1521 kB

Observad que hemos utilizado el mismo path que hemos obtenido de la salida de strace. El comando anterior produce un fichero llamado hola.s. La extensión .s es utilizada para los ficheros en ensamblador. El nuestro tiene esta pinta.

	.file	"hola.c"
	.section	.rodata
.LC0:
	.string	"Hello World!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$.LC0, %edi
	call	puts
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
	.section	.note.GNU-stack,"",@progbits

Ahora podemos convertir este fichero ensamblador en código objeto utilizando el ensamblador:

$ as -o hola.o hola.s
$ file hola.o
hola.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

Linkando

Ya solo nos quedaría linkarlo... Bueno, el linkado, es un poquitín complicado, incluso para un programa tan sencillo como este. Lo que pasa es que, cualquier programa que utilice la librería C, tiene que enlazarse con ella. Además, todo programa que utiliza la librería C estándar necesita un cierto código de inicialización que nosotros, los programadores de aplicaciones normales y corrientes no solemos ver.

El comando de linkado depende bastante de donde se hayan instalado todos esos fichero, lo cual puede variar de sistema a sistema, de plataforma a plataforma o de versión del compilador a versión del compilador.

Pero bueno, que no cunda el pánico. El propio gcc nos va a ayudar a entender que es lo que está pasando. Os presentamos el flag -v

$  gcc -v hola.c
(... mogollón de cosas ...)
 /usr/lib/gcc/x86_64-linux-gnu/4.6/collect2 --sysroot=/ --build-id --no-add-needed --as-needed --eh-frame-hdr -m elf_x86_64 --hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/4.6/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/4.6 -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../.. /tmp/ccYlYezt.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/4.6/crtend.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crtn.o
$

Como podéis ver el flag -v nos da un montón de información sobre donde se encuentran los distintos ficheros que usa gcc así como los flags que añade sin decir ni mú. Lo que nos interesa a nosotros en la última linea. Si amigos, ese es el comando de linkado.

Como podéis ver, el linkado se inicia desde el programa collect2 que a su vez ejecuta ld, el enlazador. Para este programa tan sencillo, podemos ejecutar ld directamente en lugar de collect2. De cualquier forma, todos esos parámetros que veis en el comando son esos ficheros necesarios para utilizar la librería C estándar, además de la librería en si misma (puedes encontrar a Wally?. Digo -lc).

Si miráis atentamente veréis que en el medio de todo ese galimatías está el fichero /tmp/ccYlYezt.o. Este es el nombre temporal que gcc asigna a la salida del ensamblador. Nosotros le llamamos hola.o, así que si queréis convertir el hola.o que generamos antes en un ejecutable debéis hacer lo siguiente:

  • Copiad la última línea generado por gcc -v, la que ejecuta la utilizad collect2
  • Cambiad collect2 por ld (esto es totalmente opcional)
  • Buscad el fichero con el nombre raro y cambiadlo por hola.o
  • Añadir -o hola al final si queréis que vuestro programa no se llame a.out :)

Para nuestro caso... el comando quedaría más o menos así:

$ ld  --sysroot=/ --build-id --no-add-needed --as-needed --eh-frame-hdr -m elf_x86_64 --hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/4.6/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/4.6 -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../.. -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/4.6/crtend.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crtn.o hola.o

Observad que hemos eliminado el fichero en /tmp con el nombre raro y hemos añadido hola.o al final. Tras ejecutar esto, un flamante binario con nombre a.out estará listo para ser ejecutado.

Toolchains

Lo que acabamos de describir es lo que se conoce como la ToolChain (Cadena de Hherramientas) del sistema. En realidad la toolchain contiene algunas herramientas más, pero digamos que las que hemos descrito hasta ahora son las mínimas necesarias para poder compilar un programa C (como acabamos de ver).

Bien, pues cuando queremos compilar un programa para otra plataforma, necesitamos un toolchain para esa plataforma. No solo versiones del compilador, ensamblador, enlazador... también los ficheros de inicialización y las librerías estándard que hemos visto cuando linkábamos manualmente nuestro hola mundo.

Para ilustrar esto vamos a instalar un toolchain para máquinas ARM. Si tienes una Raspbery Pi, una BeagleBone, un Olinuxino entonces tienes una máquina ARM con la que probar. Es más, si tienes un teléfono Android, lo más seguro es que sea una máquina ARM. Veremos en un segundo como probar nuestro SW en esos dispositivos.

Para los afortunados usuarios de Debian y derivados, la distribución incluye desde hace ya algún tiempo un toolchain para máquinas ARM. Así que... apt-getealo:

sudo apt-get install gcc-arm-linux-gnueabi
sudo apt-get install gcc-arm-linux-gnueabihf
sudo apt-get install binutils-arm-linux-gnueabihf

El segundo paquete proporciona soporte para Hard Floating point... es decir, para procesadores ARM con soporte hardware para trabajar con números decimales... lo cual, a día de hoy es lo normal. En caso de duda instalad ambos paquetes. El último incluye algunas herramientas adicionales que, si bien no vamos a utilizar en este artículo, os resultarán útiles.

Para los usuarios de otras distribuciones, consultad vuestros repositorios. Si no encontráis un toolchain, descargadlo dela web de Linaro. Solo tenéis que descomprimir el tar.gz en algún sitio y añadir el directorio con los binarios al PATH.

Ahora disponéis del toolchain para ARM. Escribid en el terminal arm-linux-gnueabihf- y pulsad TAB.... Esto es lo que veréis más o menos:

$  arm-linux-gnueabihf-
arm-linux-gnueabihf-addr2line  arm-linux-gnueabihf-ld
arm-linux-gnueabihf-ar         arm-linux-gnueabihf-ld.bfd
arm-linux-gnueabihf-as         arm-linux-gnueabihf-ld.gold
arm-linux-gnueabihf-c++filt    arm-linux-gnueabihf-nm
arm-linux-gnueabihf-cpp        arm-linux-gnueabihf-objcopy
arm-linux-gnueabihf-cpp-4.6    arm-linux-gnueabihf-objdump
arm-linux-gnueabihf-elfedit    arm-linux-gnueabihf-ranlib
arm-linux-gnueabihf-gcc        arm-linux-gnueabihf-readelf
arm-linux-gnueabihf-gcc-4.6    arm-linux-gnueabihf-size
arm-linux-gnueabihf-gcov       arm-linux-gnueabihf-strings
arm-linux-gnueabihf-gcov-4.6   arm-linux-gnueabihf-strip
arm-linux-gnueabihf-gprof

Si habéis instalado otro toolchain, el prefijo de las herramientas puede ser diferente. Comprobad el contenido del directorio de binarios para estar seguros

Ese es el listado completo del toolchain en mi máquina. Podéis ver gcc, as, ld y otras muchas herramientas de las que no vamos a hablar todavía. Como podéis ver cc1 y collect2 no aparecen. Como ya sabemos esos programas son lanzados automáticamente durante el proceso de compilación o linkado y en principio nunca son utilizados por el usuario directamente.

Podéis repetir el proceso anterior con nuestro nuevo toolchain. Todas las herramientas funcionan igual, solo que empiezan por arm-linux-gnueabihf-. Por ejemplo, para producir una versión de hola mundo para ARM solo tenéis que:

$ arm-linux-gnueabihf-gcc hola.c -o hola-arm

Probando Nuestro Programa

Los que tengáis algún dispositivo ARM ejecutando alguna variante reciente de Debian, podéis copiar este binario (hola-arm) en él y ejecutarlo. Debería funcionar sin más. Nosotros lo hemos probado en los dispositivos que tenemos normalmente en línea: Olinuxino, BananaPi y RaspberryPi.

Si tenéis algún problema ejecutando el programa, probad con el otro toolchain (el no hf) en caso de que vuestro dispositivo sea un poco antiguo y no tenga el soporte hardware para los números decimales.

Si nada funciona, probad a compilarlo estáticamente:

$ arm-linux-gnueabihf-gcc -static hola.c -o hola-arm

El binario estático debe funcionar en cualquier ARM, incluyendo vuestro teléfono Android ARM, siempre y cuanto no estéis utilizando un kernel muy viejo en el dispositivo.

Los binarios dinámicos, como el primero que hemos generado, hacen uso del enlazador dinámico (un programa llamado ld-ALGO.so. El algo depende de vuestro sistema. Pues bien, este programa se va a encargar de encontrar la librería C estándar en vuestro dispositivo, cargarla, y modificar la imagen en memoria de nuestro programa (actualizar la tabla PLT) para que utilice el printf que proporciona la librería local.

Mientras el interfaz de la librería C y el del enlazador dinámico no cambien, el programa debería funcionar sin problemas. Esto puede pasar en sistemas muy antiguos, o en sistemas "distintos", como Android.

El caso de Android

Android no es más que otra distribución Linux, en la que la han liado bien. Todavía tenemos un kernel Linux, igualito que el de nuestro ordenador, pero todo el espacio de usuario se ha modificado... desde los scripts de arranque a las aplicaciones de usuario (que normalmente son bytecodes java)... pasando por el enlazador dinámico y la librería estándar C.

En Android, la librería estándar se ha modificado bastante. Incluso su nombre ha cambiado (bionic en lugar de libc :), y su localización. La localización no debería ser un problema ya que es el trabajo del enlazador dinámico encontrarla, pero es que el enlazador dinámico también es diferente, así que nuestro binario no va a funcionar en un teléfono Android.

Si bien es posible "parchear" todo esto, el proceso es complicado y tedioso, así que, en estos casos, lo mejor es hacernos con el toolchain adecuado. Esto pasa también con algunos routers. Utilizar el toolchain correcto va a hacer vuestras vidas mucho más fáciles.

Para poder compilar nuestro programa para Android, debemos instalar el SDK (Software Development Kit) y el NDK (Native Development Kit) de Google, especialmente este último. La instalación es sencilla, solo hay que descomprimir y añadir al PATH los directorios con los programas que queremos utilizar. Algo tal que así:

~/android $ export PATH=${HOME}/android/ndk/build/tools:$PATH
~/android $ export PATH=${HOME}/android/sdk/platform-tools/:$PATH

Si tenéis problemas instalando el SDK/NDK, simplemente echadle un ojo a las páginas de Google... es muy fácil.

Bueno, ahora que tenemos las herramientas instaladas, podemos generar nuestro toolchain. Y esto se hace con un comando como este:

~/android $ make-standalone-toolchain.sh --platform=android-19 --install-dir=${HOME}/android/android-19 --arch=arm
~/android $ PATH=${HOME}/android/android-19/bin:$PATH

En este caso hemos elegido el API 19 que se corresponde con Android 4.4 o superior. Podéis consultar los APIs disponibles en esta página:

http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels

Si no os molesta leer en Inglés, todo esto está explicado en más detalle en esta serie de artículos http://papermint-designs.com/dmo-blog/2016-04-awesome-android-extreme-hacking--part-i

Pues, ya está listo. Compilamos, copiamos al teléfono (o emulador) y ejecutamos:

$ arm-linux-androideabi-gcc -o hola-arm hola.c 
$ adb push hola-arm /data/local/tmp
$ adb shell "chmod 777 /data/local/tmp/hola-arm; /data/local/tmp/hola-arm"

Y así concluye esta primera parte de nuestro alucinante viaje hacia los entresijos más íntimos del antiguo arte de la compilación... Qué va! Es coña!. En el próximo número seguiremos explorando los detalles del proceso de compilación. Esperamos que os haya resultado interesante.

Header Image Credits: MAROQUOTIDIEN PLUS

SOBRE Andrés "Andy" Pajaquer
En lo que a seguridad se refiere Andy es un crack. Conocedor de los secretos más oscuros de tu sistema operativo, es capaz de extorsionarlo para conseguir de el lo que quiera. Andy es licenciado por la Universidad del Humor y recientemente a defendido su tesis: "Origen Epistemológico de los Chiste Paramétricos en Entornos en bebidos", en la que contó con la ayuda inestimable de Jim "Tonys".