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.cHasta 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.txtEste 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 amigostrace
. 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 ensambladoras
. Este es el ensamblador de GNU (ASsembler). Este programa convierte programas en lenguaje ensamblador en código objetocollect2
: 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.
cc1
:
$ /usr/lib/gcc/x86_64-linux-gnu/4.6/cc1 hola.c main Analyzing compilation unit Performing interprocedural optimizations <*free_lang_data>Observad que hemos utilizado el mismo path que hemos obtenido de la salida deAssembling 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
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,"",@progbitsAhora 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 propiogcc
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 utilizadcollect2
- Cambiad
collect2
porld
(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 llamea.out
:)
$ 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.oObservad 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-gnueabihfEl 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-gprofSi 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-armEl 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/:$PATHSi 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:$PATHEn 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
■
CLICKS: 4217