Compilación, Compilación Cruzada y Toolchains. Parte II
PROGRAMACIÓN
Compilación, Compilación Cruzada y Toolchains. Parte II
2017-04-18
Por
Andrés "Andy" Pajaquer

En la entrega anterior de esta mini-serie hemos visto como utilizar un toolchain para compilar pequeños programas que hacen uso de la librería C estándar... Pero como podemos proceder cuando nuestro programa utiliza otras librerías?
Pues bien, eso es de lo que vamos a hablar en esta ocasión. Para poder desvelar todos los secretos de la compilación cruzada en entornos complejos (vamos cuando tenemos que usar muchas librerías), lo primero que vamos a hacer es crear una librería. Menuda sorpresa no?

Una librería de Ejemplo

Vamos a generar una sencilla librería con dos funciones. Crearemos un fichero llamado milib.c y añadiremos el siguiente código.

#include <stdio.h>

int func1 ()
{
  printf ("Esta el la funcion1 de mi librería\n");
  return 1;
}

int func2 ()
{
  printf ("Esta el la funcion2 de mi librería\n");
  return 2;
}
  

También necesitaremos un pequeño fichero de cabecera para poder utilizar esta librería en nuestros programas. Aquí lo tenéis:

#ifndef MILIB_H
#define MILIB_H

#ifdef __cplusplus
extern "C" {
#endif

int func1 ();
int func2 ();

#ifdef __cplusplus
}
#endif

#endif

  

Lo típico verdad?. Nada que comentar. Circulen, circulen.

Ahora solo necesitamos un pequeño Makefile para poder generar nuestra librería. Lo hemos llamado Makefile.manual. Algo como esto:

all: libmilib.so

libmilib.so: milib.c
	${CC} -shared -fPIC -o $@ $<

.PHONY:
install:
	cp libmilib.so /usr/lib
	cp milib.h /usr/include

  

Como podéis ver, hemos utilizado una variable para llamar al compilador de forma que nos resulte sencillo compilar la librería para otras plataformas.

$ CC=arm-linux-gnueabihf-gcc make -f Makefile.manual
$ file libmilib.so
libmilib.so: ELF 32-bit LSB  shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=d3d93d5b8d512882c52f6fe131a242e0550d9d91, not stripped

  

Et voilá!... Nuestra librería está lista para ser utilizada en nuestro cacharro ARM preferido.

Probando la Librería

Para poder probar si nuestra librería ha sido correctamente generada, vamos a escribir un pequeño programa que la utilice.

#include <stdio.h>
#include <milib.h>

int
main ()
{
  printf ("Programa con librerías\n");

  func1 ();
  func2 ();

  return 0;
}
  

Bien. Ahora solo tenemos que compilar este programa y enlazarlo con la librería que acabamos de generar. Veremos un par de formas de hacerlo, y la primera es utilizando los flags de compilación.

$ arm-linux-gnueabihf-gcc -o main main.c -I. -L. -lmilib
 

Puesto que nuestra librería no se encuentra en ninguno de los directorios estándar donde busca el compilador (/usr/lib, /usr/local/lib,...), lo primero que tenemos que hacer es indicar el directorio en el que debe buscar nuestra librería. El parámetro -L. le indicará al compilador que debe utilizar el directorio actual para ese menester.

Lo mismo sucede con los ficheros .h. Cuando estos no se encuentran en uno de los directorios del sistema (/usr/include,...), tenemos que decirle al compilador donde puede encontrarlos. En el programa podríamos haber entrecomillado el #include para milib.h, para que el compilador busque el fichero en el directorio actual. En este caso estamos intentando cubrir el caso general, en el que la librería ya está en el sistema y estamos intentando escribir un programa que la utiliza. Vamos el .h es un API público.

Una vez que el compilador sabe donde buscar, ya solo tenemos que indicarle que librería queremos añadir. Esto lo conseguimos con el parámetro -lmilib.

Ahora ya podemos probar nuestro programa. Copiamos los ficheros binarios (la librería y el programa principal) a nuestro dispositivo ARM (o lo que sea que estéis usando), y ejecutamos el programa con un comando como este:

LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH ./main2
 

Como ya sabréis la variable LD_LIBRARY_PATH, nos permite añadir directorios a la lista que se utiliza para localizar las librerías dinámicas que utiliza un determinado programa. En este caso, estamos añadiendo el directorio actual antes de ejecutar el programa para indicarle al enlazador dinámico que queremos que busque las librerías en este directorio.

Tal y como lo hemos ejecutado, solo actualizamos el valor de LD_LIBRARY_PATH para la ejecución del programa. Podemos modificarla permanentemente utilizando export (en shells tipo bash) o setenv.

Finalmente, también podríamos copiar la librería en uno de los directorios del sistema (por ejemplo, /usr/local/lib) y de esta forma no tener que especificar un valor especial para LD_LIBRARY_PATH. En este caso, debéis ejecutar ldconfig como root después de copiar el la librería en el directorio del sistema. De esta forma actualizamos la cache del enlazador dinámico.

Si obtenéis un mensaje como este:

./prog: error while loading shared libraries: libmilib.so: cannot open shared object file: No such file or directory

Eso significa que el path es incorrecto o la cache no ha sido actualizada (dependiendo del método que utilicéis).

Un Paso Intermedio

Lo que os acabamos de contar funcionan bien para proyectos pequeños y con pocas dependencias. En cuanto la cosa comienza a crecer, este método manual se hace tedioso y propenso a errores... Tenemos que hacerlo mejor. Pero antes de entrar en modo PRO, vamos a dar un pasito intermedio para introducir un concepto que nos hará más sencillo comprender la última parte de este artículo.

Lo primero que vamos a hacer es modificar el makefile de nuestra librería. Realmente solo vamos a cambiar la regla de instalación, para que copie los ficheros en un directorio en concreto y no directamente en los directorios del sistema. Algo como esto:

install:
        mkdir -p /tmp/sysroot/usr/lib
        mkdir -p /tmp/sysroot/usr/include
	cp libmilib.so /tmp/sysroot/usr/lib
	cp milib.h /tmp/sysroot/usr/include

En este caso estamos utilizando un directorio llamado /tmp/sysroot. La verdad es que no importa donde pongáis este directorio, en seguida vamos a ver como hacer todo esto de una forma más elegante. Por ahora, dejémoslo ahí. Cuando ejecutemos make install, la librería que hemos generado y el fichero de cabecera asociado se copiarán en /tmp/sysroot, siguiendo la estructura típica del sistema de ficheros raíz de una máquina GNU/Linux.

Lo que esto nos va a permitir, es compilar nuestro programa con un comando como este:

$ arm-linux-gnueabihf-gcc --sysroot /tmp/sysroot/  -o main1 main.c -lmilib

El parámetro --sysroot, nos permite indicarle al compilador que, en lugar de utilizar el sistema de ficheros raíz actual, considere que el sistema de ficheros raíz está en el directorio indicado. En este caso /tmp/sysroot.

De esta forma, podemos compilar todas las librerías que queramos, instalarlas en nuestro SYSROOT y luego utilizarlas para compilar nuestros programas, prácticamente como si estuviéramos compilando nativamente en la máquina destino.

De la misma forma, si necesitamos compilar un programa que hace uso de alguna librería dependiente de la plataforma, simplemente podemos copiarlas en nuestro SYSROOT, y usarlas normalmente en nuestros programas. Un ejemplo típico de este último caso son las liberías OpenGL que suelen ser cerradas en muchas plataformas, o los ficheros de desarrollo de VideoCore de la Rpi que se suelen encontrar bajo /opt/vc.

En plan PRO

Ahora que ya sabemos lo que tenemos que hacer vamos a ponernos en plan POFESIONAL. Para ello vamos a utilizar las Autotools de GNU. No, no son herramientas para arreglar el coche... ni son las herramientas que utilizan los Transformers.... Se trata de un conjunto de utilidades que nos permiten automatizar el proceso de construcción de programas. Ya hemos hablado de ellas en otras ocasiones, pero bueno.... vamos a hacer este artículo autocontenido y repetirnos un poco. :)

Lo que vamos a hacer es adaptar nuestra librería y nuestro programa para que se compilen utilizando autotoolsi> y luego mostraros como este conjunto de herramientas nos va a simplificar todo el proceso que hemos descrito más arriba.

Comenzaremos adaptando la librería. Creamos un directorio llamado milib. Lo podéis crear donde queráis, y de hecho podéis llamarlo como queráis... esto no es Java :). En este directorio tendremos que añadir dos ficheros: configure.ac y Makefile.am. El primero debería parecerse a este:

AC_INIT([milib], 0.1, [roor@papermint-designs.com])
AM_INIT_AUTOMAKE([-Wall -Werror])

AC_PROG_CC
AC_HEADER_STDC()
AM_PROG_AR
AC_PROG_LIBTOOL

AC_OUTPUT(Makefile)

Y el segundo, el Makefile.am, debería parecerse a esto:

lib_LTLIBRARIES = libmilib.la

include_HEADERS=milib.h
libmilib_la_SOURCES=milib.c

Listo. Ahora solo tenemos que añadir unos cuantos ficheros (que pueden estar vacios) y generar el famoso configure.

$ touch NEWS README AUTHORS ChangeLog
$ autoreconf -if

Y listo. Ya podemos cross-compilar nuestra librería para lo que nos de la gana. Para una plataforma ARM con soporte hardware de coma flotante (buff!) sería algo así:

$ ./configure --host=arm-linux-gnueabihf --prefix=/tmp/sysroot/usr
$ make && make install

Mola.... Veamos lo que hemos ganado. Ahora podemos indicar el toolchain a utilizar en el configure. Para un proyecto tan chorras como el que hemos utilizado en este ejemplo, esto no parece una gran ventaja... pero bueno, cuando tengáis que crear un proyecto que incluya varios programas y librerías....lo agradeceréis. Lo segundo que hemos ganado, es que podemos instalar nuestro paquete en cualquier sitio utilizando el flag --prefix. De nuevo, esto parece algo muy fácil de hacer en un Makefile, normal y corriente... cierto, lo es. Pero con autotools es bastante más fácil, sobre todo cuando tienes ficheros de configuración que va a un directorio, páginas del manual, que van a otro, unos binarios que se instalan y que no (tests unitarios),...

Así que ahora tenemos una forma fácil de compilar nuestra librería y añadirla a un SYSROOT. Observad que todo esto lo podéis utilizar para compilar nativamente en la plataforma... sólo tendréis que ejecutar configure sin parámetros. Como hacéis normalmente en vuestra máquina GNU/Linux.

Compilando El programa

Ahora ya solo nos queda compilar nuestro binario. Para ello crearemos otro directorio para contener todos los ficheros necesarios para compilar el programa usando autotools y, una vez más, crearemos los ficheros configure.ac y Makefile.am.

El Makefile.am es el más sencillo. Aquí lo tenéis.

bin_PROGRAMS = main

main_SOURCES=main.c
main_LDADD=-lmilib

El configure es un poco más complicado. Al parecer, autotools no ofrece soporte directo para utilizar sysroot, o al menos, el que escribe y subscribe no ha sido capaz de encontrarlo. Así que, lo que hemos hecho es añadir ese soporte al configurecode>. Esto hace que el fichero sea un poco más complejo. Veamos como ha quedao.

AC_INIT([milib-test], 0.1, [roor@papermint-designs.com])
AM_INIT_AUTOMAKE([-Wall -Werror])

AC_PROG_CC
AC_HEADER_STDC()

AC_ARG_WITH(sysroot,
  AS_HELP_STRING([--with-sysroot],
    [Directory with the target libraries.]),,
    withval="")

if test "x$withval" != "x" ; then
	CFLAGS=" --sysroot=$withval ${CFLAGS} "
	CXXFLAGS=" --sysroot=$withval ${CFLAGS} "
fi

AC_OUTPUT(Makefile)
AC_MSG_RESULT(CFLAGS   : ${CFLAGS} )
AC_MSG_RESULT(CXXFLAGS : ${CXXFLAGS} )

Lo que hemos hecho es añadir un argumento a configure, al que hemos llamado --with-sysroot. Como podéis ver, autools proporciona macros para manejar este tipo de parámetros e integrarlos en las herramientas (si ejecutáis configure --help veréis los textos de ayuda que hemos añadido a nuestro configure.ac).

Aparte de eso, que es totalmente estándar, podéis ver un pequeño script en medio de nuestro fichero. Si el if hacia el final del fichero, es código shell script corriente y moliente. Simplemente comprueba si la variable $withval no está vacía. En ese caso, añade el flag --sysroot a las variables CFLAGS (código C) y CXXFLAGS (código C++) que se utilizará durante la compilación, junto con el directorio que se haya pasado como parámetro con el argumento --with-sysroot.

Al final del fichero, mostramos el valor de los flags de compilación para que podáis comprobar que todo ha funcionado correctamente.

Con todo esto, ahora podemos compilar nuestro programa de la siguiente forma:

$  ./configure --host=arm-linux-gnueabihf --with-sysroot=/tmp/sysroot/ --prefix=/tmp/sysroot
$ make && make install

De esta forma, nuestro programa se compilará con el toolchain indicado, utilizando el sistema que hayamos replicado bajo /tmp/sysroot y se instalará en ese mismo directorio. Al final solo tendremos que copiar todo lo que se encuentre en nuestro directorio SYSROOT en la plataforma destino y todo listo.

Por cierto, ahora que tenéis vuestra librería y aplicación adaptadas para autotools, podéis ejecutar make dist para generar un paquete con el código fuente!!!. Sí, así de fácil.

Conclusiones

Bueno, esperamos que esta mini-serie os haya resultado interesante y sobre todo que pueda ser útil en el futuro. En estos dos artículos hemos repasado los conceptos generales tras el proceso de compilación y esperamos que ahora tengáis una visión más clara de su funcionamiento.


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".