lunes, 2 de octubre de 2017

Compilando Programas

En este capítulo, veremos cómo construir programas compilando código fuente. La disponibilidad de código fuente es la libertad fundamental que hace que Linux sea posible. El ecosistema completo de desarrollo Linux se basa en el libre intercambio entre desarrolladores. Para muchos usuarios de escritorio, compilar es un arte perdido. Solía ser bastante común, pero hoy, los proveedores e distribuciones mantienen amplios repositorios de binarios precompilados, listos para descargar y usar. En el momento de la escritura de este libro, el repositorio Debian (uno de los más grandes de todas las distribuciones) contiene casi 23.000 paquetes.

Entonces ¿por qué compilar software? Hay dos razones:

  • Disponibilidad. A pesar del número de programas precompilados en los repositorios de las distribuciones, alguno distribuidores pueden no incluir todas las aplicaciones deseadas. En este caso, la única forma de obtener el programa deseado es compilarlo de su fuente.
  • Oportunidad. Mientras que algunos distribuciones se especializan en las últimas versiones de los programas, muchas no lo hacen. Esto significa que para tener la última versión de un programa es necesario compilarlo.
Compilar software desde código fuente puede llegar a ser muy complejo y técnico; mucho más allá del alcance de muchos usuarios. Sin embargo, muchas tareas de compilación son bastante fáciles y sólo necesitan unos pocos pasos. Todo depende del paquete. Veremos un caso muy simple para hacer un repaso del proceso y como punto de partida para aquellos que quieran emprender un estudio más a fondo.

Presentaremos un nuevo comando:

  • make - Utilidad para mantener programas
¿Qué es compilar?

Digamos que, compilar es el proceso de traducir código fuente (la descripción legible por humanos de un programa escrito por un programador) al lenguaje nativo del procesador del ordenador.

El procesador del ordenador (o CPU) trabaja a un nivel muy elemental, ejecutando programas en lo que se llama lenguaje máquina. Es un código numérico que describe operaciones muy pequeñas, como "añade este byte", "apunta a esta localización en la memoria" o "copia este byte."

Cada una de esas instrucciones se expresan en binario (unos y ceros). Los primeros programas de ordenador se escribieron usando este código numérico, que explicaría porque los que los escribieron se dice que fumaban mucho, bebían litros de café y usaban gafas con cristales gordos.

El problema se solucionó con la llegada del lenguaje ensamblador, que reemplazaba los códigos numéricos con caracteres mnemotécnicos (ligeramente) más fáciles de usar como CPY (para copiar) y MOV (para mover). Los programas escritos en lenguaje ensamblador se procesan a lenguaje máquina por un programa llamado ensamblador. El lenguaje ensamblador se usa aún hoy para ciertas tareas especializadas de programación, como controladores de dispositivos y sistemas embebidos.

A continuación llegamos a lo que se llama lenguajes de programación de alto nivel. Se llaman así porque permiten que el programador esté menos preocupado con los detalles de lo que está haciendo el procesador y más con resolver los problemas que tiene entre manos. Los primeros (desarrollados durante los años cincuenta) incluyen FORTRAN (diseñado para tareas científicas y técnicas) y COBOL (diseñado para aplicaciones comerciales). Ambos tiene un uso limitado todavía hoy.

Aunque hay muchos lenguajes de programación populares, dos predominan. La mayoría de programas escritos para sistemas modernos están escritos en C o C++. En los ejemplos que siguen, compilaremos un programa en C.

Los programas escritos en lenguajes de programación de alto nivel son convertidos a lenguaje máquina procesándolos en otro programa, llamado compilador. Algunos compiladores traducen las instrucciones de alto nivel en lenguaje ensamblador y luego usan un ensamblador para realizar el último paso de traducirlo a lenguaje máquina.

Un proceso usado a menudo junto con el compilado es el llamado enlazado. Hay muchas tareas comunes realizadas por programas. Tomemos, por ejemplo, abrir un archivo. Muchos programas realizan esta tarea, pero sería un despilfarro que cada programa implemente su propia rutina para abrir archivos. Tiene más sentido tener una única pieza de programación que sepa como abrir archivos y permitir a todos los programas que lo necesiten compartirla. Dar soporte a tareas comunes es realizado por las llamadas librerías. Contienen múltiples rutinas, cada una realiza alguna tarea común que puede ser compartida por múltiples programas. Si miramos en los directorios /lib y/usr/lib, podemos ver donde están muchas de ellas. Un programa llamado enlazador se usa para realizar las conexiones entre la salida del compilador y las librerías que el programa compilado requiere. El resultado final de este proceso es el archivo ejecutable del programa, listo para ser usado.

¿Todos los programas se compilan?

No. Como hemos visto, hay programas como los scripts de shell que no requieren compilación. Se ejecutan directamente. Están escritos en lo que se conoce como lenguajes de script o interpretados. Estos lenguajes han ganado popularidad en los últimos años e incluyen Perl, Python, PHP, Ruby y muchos otros.

Los lenguajes de script se ejecutan por un programa especial llamado intérprete. Un intérprete toma el archivo del programa y lee y ejecuta cada instrucción contenida dentro de él. En general, los programas interpretados se ejecutan mucho más lentamente que los programas compilados. Ésto es porque cada instrucción de código en un programa interpretado se traduce cada vez que se ejecuta, mientras que en un programa compilado, una instrucción de código fuente se traduce sólo una vez, y esta traducción se graba permanentemente en el archivo ejecutable final.

Entonces ¿por qué son tan populares los programas interpretados? Para muchas tareas de programación, los resultados son "suficientemente rápidos", pero la ventaja real es que es generalmente más rápido y fácil desarrollar programas interpretados que programas compilados. Los programas se desarrollan a menudo en un ciclo repetitivo de código, compilación, prueba. A medida que un programa crece en tamaño, la fase de compilación del ciclo puede llegar a ser muy larga. Los lenguajes interpretados eliminan el paso de la compilación y por lo tanto aumentan la velocidad de desarrollo del programa.

Compilando un programa en C

Compilemos algo. Antes de hacerlo sin embargo, vamos a necesitar algunas herramientas como el compilador, el enlazador y make. El compilador C usado casi universalmente en el entorno Linux se llama gcc (GNU C Compiler - Compilador C GNU), originalmente escrito por Richard Stallman. La mayoría de las distribuciones no instalan gcc por defecto. Podemos comprobar si el compilador esta presente así:

 [me@linuxbox ~]$ which gcc

 /usr/bin/gcc

El resultado de este ejemplo indica que el compilador está instalado.

Consejo: Tu distribución puede tener un meta-paquete (una colección de paquetes) para el desarrollo de software. Si es así, considera instalarlo si quieres compilar programas en tu sistema. Si tu sistema no ofrece un meta-paquete, prueba a instalar los paquetes gcc y make. En muchas distribuciones, es suficiente para realizar el ejercicio siguiente.


Obteniendo el código fuente

Para nuestro ejercicio de compilación, vamos a compilar un programa del Proyecto GNU llamado diction. Es un pequeño pero práctico programa que comprueba la calidad de escritura y edición de los archivos de texto. Como programa, es bastante pequeño y fácil de construir.

Siguiendo la norma, primero vamos a crear un directorio para nuestro código fuente llamado src y luego descargaremos el código fuente en él usando ftp:

[me@linuxbox ~]$ mkdir src
[me@linuxbox ~]$ cd src
[me@linuxbox src]$ ftp ftp.gnu.org
Connected to ftp.gnu.org.
220 GNU FTP server ready.
Name (ftp.gnu.org:me): anonymous
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> cd gnu/diction
250 Directory successfully changed.
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
-rw-r--r-- 1 1003 65534 68940 Aug 28 1998 diction-0.7.tar.gz
-rw-r--r-- 1 1003 65534 90957 Mar 04 2002 diction-1.02.tar.gz
-rw-r--r-- 1 1003 65534 141062 Sep 17 2007 diction-1.11.tar.gz
226 Directory send OK.
ftp> get diction-1.11.tar.gz
local: diction-1.11.tar.gz remote: diction-1.11.tar.gz
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for diction-1.11.tar.gz
(141062 bytes).
226 File send OK.
141062 bytes received in 0.16 secs (847.4 kB/s)
ftp> bye
221 Goodbye.
[me@linuxbox src]$ ls
diction-1.11.tar.gz

Nota: Como somos los "mantenedores" de este código fuente mientras lo compilamos, lo guardaremos en ~/src. El código fuente instalado por nuestra distribución se instalará en /usr/src, mientras que nuestro código fuente destinado al uso de múltiples usuarios se instala a menudo en /usr/local/src.

Como podemos ver, el código fuente se proporciona normalmente en forma de archivo tar comprimido. Algunas veces se llama tarball, este archivo contiene el source tree(árbol de código), o jerarquía de directorios y archivos que abarca el código fuente. Tras llegar al sitio ftp, examinamos la lista de archivos tar disponibles y seleccionamos la versión más nueva para descargar. Usando el comando get contenido en ftp, copiamos el archivo del servidor ftp a la máquina local.

Una vez que se ha descargado el archivo tar, tiene que ser desempaquetado. Esto se hace con el programa tar:

 [me@linuxbox src]$ tar xzf diction-1.11.tar.gz
 [me@linuxbox src]$ ls
 diction-1.11        diction-1.11.tar.gz

Consejo: El programa diction, como todo el software del Proyecto GNU, sigue ciertos estándares para el empaquetado de código fuente. La mayoría del resto de código fuente disponible en el ecosistema Linux también sigue este estándar. Un elemento del estándar es que cuando el código fuente del archivo tar es desempaquetado, se creará un directorio que contiene el árbol fuente, y que este directorio se llamará project-x.xx, conteniendo así tanto el nombre del proyecto como el número de versión. Este esquema también permite la instalación fácil de múltiples versiones del mismo programa. Sin embargo, a menudo es una buena idea examinar la disposición del árbol antes de desempaquetarlo. Algunos proyectos no crearán el directorio, pero en su lugar colocarán los archivos directamente en el directorio actual. Esto provocará un desorden en tu, de otra forma, bien organizado directorio src. Para evitar esto, usa el siguiente comando para examinar el contenido del archivo tar:

 tar tzvf archivotar | head


Examinando el árbol fuente

Desempaquetar el archivo tar da como resultado la creación de un nuevo directorio, llamado diction-1.11. Este directorio contiene el árbol fuente. Miremos dentro:

[me@linuxbox src]$ cd diction-1.11
[me@linuxbox diction-1.11]$ ls
config.guess  diction.c        getopt.c       nl
config.h.in   diction.pot      getopt.h       nl.po
config.sub    diction.spec     getopt_int.h   README
configure     diction.spec.in  INSTALL        sentence.c
configure.in  diction.texi.in  install-sh     sentence.h
COPYING       en               Makefile.in    style.1.in
de            en_GB            misc.c         style.c
de.po         en_GB.po         misc.h         test
diction.1.in  getopt1.c        NEWS

En él, vemos un número de archivos. Los programas pertenecientes al Proyecto GNU, así como muchos otros, proporcionarán los archivos de documentación README,INSTALL, NEWS y COPYNG. Estos archivos contienen la descripción del programa, información de cómo construirlo e instalarlo, y sus términos de licencia. Es siempre una buena idea leer los archivos README e INSTALL antes de intentar construir el programa.

Los otros archivos interesantes en este directorio son los que terminan en .c y .h:

[me@linuxbox diction-1.11]$ ls *.c
diction.c  getopt1.c  getopt.c  misc.c  sentence.c  style.c
[me@linuxbox diction-1.11]$ ls *.h
getopt.h   getopt_int.h misc.h  sentence.h

Los archivos .c contienen los dos programas C proporcionados por el paquete (styley diction), divididos en módulos. Es una práctica común para programas grandes dividirlos en trozos más pequeños y fáciles de manejar. Los archivos de código fuente son de texto ordinario y pueden examinarse con less:

 [me@linuxbox diction-1.11]$ less diction.c

Los archivos .h se conocen como archivos de cabecera (header files). Estos, también, son texto ordinario. Los archivos cabecera contienen descripciones de las rutinas incluidas en un archivo de código fuente o librería. Para que el compilador pueda conectar los módulos, debe recibir una descripción de los módulos necesarios para completar todo el programa. Por el principio del archivo diction.c, vemos esta línea:

 #include "getopt.h"

Ésto ordena al compilador a leer el archivo getopt.h como si leyera el código fuente de diction.c para poder "saber" que hay en getopt.c. El archivo getopt.cproporciona rutinas que son compartidas por los programas style y diction.

Encima de la declaración include de getopt.h, vemos otras declaraciones include como estas:

 #include <regex.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>

Estas se refieren también a archivos cabecera, pero se refieren a archivos cabecera que se encuentran fuera del árbol de código actual. Son proporcionados por el sistema para soportar la compilación de cada programa. Si miramos en /usr/include, podemos verlos:

 [me@linuxbox diction-1.11]$ ls /usr/include

Los archivos cabecera en este directorio se instalaron cuando instalamos el compilador.


Construyendo el programa

La mayoría de los programas se construyen con una secuencia simple de dos comandos:

 ./configure
 make

El programa configure es un script de shell que es proporcionado por el árbol fuente. Su trabajo es analizar el entorno de construcción. La mayoría del código fuente se diseña par ser portable. O sea, se diseña para construirse en más de un tipo de sistema como-Unix. Pero para hacer eso, el código fuente puede necesitar someterse a leves ajustes durante la construcción para acomodarse a las diferencias entre sistemas. configure también comprueba qué herramientas externas necesarias y componentes están instalados. Ejecutemos configure. Como configure no está localizado donde el shell espera normalmente que estén almacenados los programas, tenemos que decirle al shell explícitamente su localización precediendo el comando con ./ para indicar que el programa se localiza en el directorio de trabajo actual:

 [me@linuxbox diction-1.11]$ ./configure

configure producirá un montón de mensajes a medida que prueba y configura la construcción. Cuando termina, tendrá un aspecto como éste:

 checking libintl.h presence... yes
 checking for libintl.h... yes
 checking for library containing gettext... none required
 configure: creating ./config.status
 config.status: creating Makefile
 config.status: creating diction.1
 config.status: creating diction.texi
 config.status: creating diction.spec
 config.status: creating style.1
 config.status: creating test/rundiction
 config.status: creating config.h
 [me@linuxbox diction-1.11]$

Lo importante aquí es que no hay mensajes de error. Si los hubiera, la configuración fallaría, y el programa no se construiría hasta que se corrigieran los errores.

Vemos que configure ha creado varios archivos nuevos en nuestro directorio fuente. El más importante es Makefile. Makefile es un archivo de configuración que indica al programa make como construir exactamente el programa. Sin él, make no funcionará. Makefile es un archivo de texto ordinario, así que podemos verlo:

 [me@linuxbox diction-1.11]$ less Makefile


El programa make toma como entrada un makefile (que normalmente se llama Makefile), que describe las relaciones y dependencias entre los componentes que componen el programa finalizado.
La primera parte de makefile define variables que son sustituidas en secciones posteriores del makefile. Por ejemplo vemos la línea:

 CC=         gcc

que define que el compilador C será gcc. Más adelante en el makefile, vemos una instancia donde se usa:

 diction:    diction.o sentence.o misc.o  getopt.o getopt1.o
             $(CC) -o $@ $(LDFLAGS) diction.o sentence.o misc.o \
             getopt.o getopt1.o $(LIBS)

Aquí se realiza una sustitución, y el valor $(CC) se reemplaza por gcc en el momento de la ejecución.

La mayoría del makefile consiste en dos líneas, que definen un objetivo, en este caso el archivo ejecutable diction, y los archivos de los que depende. Las líneas restantes describen lo que el/los comando/s necesitan para crear el objetivo desde sus componentes. Vemos en este ejemplo que el archivo ejecutable diction (uno de los productos finales) depende de la existencia de diction.o, sentence.o, misc.o,getop.o y gestopt1.o. Mas adelante aún, en el makefile, vemos las definiciones de cada uno de estos objetivos:

 diction.o:   diction.c config.h getopt.h misc.h sentence.h
 getopt.o:    getopt.c getopt.h getopt_int.h
 getopt1.o:   getopt1.c getopt.h getopt_int.h
 misc.o:      misc.c config.h misc.h
 sentence.o:  sentence.c config.h misc.h sentence.h
 style.o:     style.c config.h getopt.h misc.h sentence.h

Sin embargo, no vemos ningún comando especificado para ellos. Esto es gestionado por un objetivo general, anteriormente en el archivo, que describe el comando usado para compilar cualquier archivo .c en un archivo .o:

 .c.o:
              $(CC) -c $(CPPFLAGS) $(CFLAGS) $<

Todo esto parece muy complicado. ¿Por qué no listamos simplemente todos los pasos para compilar las partes y terminamos? La respuesta a ésto se aclarará en un momento. Mientras tanto, ejecutemos make y construyamos nuestros programas:

 [me@linuxbox diction-1.11]$ make

El programa make se ejecutará, usando los contenidos de Makefile para guiar sus acciones. Producirá un montón de mensajes.

Cuando termine, veremos que todos los objetivos están presentes ahora en nuestro directorio:

 [me@linuxbox diction-1.11]$ ls
 config.guess   de.po           en           install-sh   sentence.c
 config.h       diction         en_GB        Makefile     sentence.h
 config.h.in    diction.1       en_GB.mo     Makefile.in  sentence.o
 config.log     diction.1.in    en_GB.po     misc.c       style
 config.status  diction.c       getopt1.c    misc.h       style.1
 config.sub     diction.o       getopt1.o    misc.o       style.1.in
 configure      diction.pot     getopt.c     NEWS         style.c
 configure.in   diction.spec    getopt.h     nl           style.o
 COPYING        diction.spec.in getopt_int.h nl.mo        test
 de             diction.texi    getopt.o     nl.po
 de.mo          diction.texi.in INSTALL      README

Entre los archivos, vemos diction y style, los programas que elegimos construir. ¡Felicidades están en orden! ¡Acabamos de compilar nuestros primeros programas desde código fuente!

Pero sólo por curiosidad, ejecutemos make de nuevo:

 [me@linuxbox diction-1.11]$ make
 make: Nothing to be done for `all'.

Sólo produce un extraño mensaje. ¿Qué está pasando? ¿Por qué no ha construido el programa de nuevo? Ah, esta es la magia de make. En lugar de simplemente construirlo todo de nuevo, make sólo construye lo que necesita construirse. Con todos los objetivos presentes, make ha determinado que no hay nada que hacer. Podemos demostrar ésto eliminando uno de los objetivos y ejecutando make de nuevo para ver qué hace. Deshagámonos de uno de los objetivos intermedios:


 [me@linuxbox diction-1.11]$ rm getopt.o
 [me@linuxbox diction-1.11]$ make


Vemos que make reconstruye y reenlaza los programas diction y style, ya que dependen del módulo perdido. Este comportamiento también indica otra característica importante de make: mantiene los objetivos actualizados. make insiste en que los objetivos sean más nuevos que sus dependencias. Esto tiene todo el sentido, como programador a menudo actualizaras algo de código fuente y luego usarás make para construir una nueva versión del producto finalizado. make se asegura de que todo los que necesite construirse en el código actualizado sea construido. Si usamos el programa touch para "actualizar" uno de los archivos de código fuente, podemos ver lo que ocurre:

[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me      me      37164 2009-03-05 06:14 diction
-rw-r--r-- 1 me      me      33125 2007-03-30 17:45 getopt.c
[me@linuxbox diction-1.11]$ touch getopt.c
[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me      me      37164 2009-03-05 06:14 diction
-rw-r--r-- 1 me      me      33125 2009-03-05 06:23 getopt.c
[me@linuxbox diction-1.11]$ make


Después de que make se ejecute, vemos que ha restaurado el objetivo para que sea más nuevo que la dependencia:

[me@linuxbox diction-1.11]$ ls -l diction getopt.c
-rwxr-xr-x 1 me      me      37164 2009-03-05 06:24 diction
-rw-r--r-- 1 me      me      33125 2009-03-05 06:23 getopt.c

La capacidad de make de construir inteligentemente sólo lo que necesita ser construido es un gran beneficio para los programadores. Aunque los ahorros de tiempo no son muy aparentes en nuestro pequeño proyecto, es muy significante para proyectos más grandes. Recuerda, el kernel Linux (un programa sometido a modificaciones y mejoras constantes) contiene varios millones de líneas de código.


Instalando el programa

El código fuente bien empaquetado ofrece a menudo un objetivo especial de makellamado install. Este objetivo instalará el producto final en un directorio de sistema para su uso. Usualmente, este directorio es /usr/local/bin, la localización tradicional para construir software localmente. Sin embargo, este directorio no es normalmente modificable por los usuarios normales, así que tenemos que ser superusuario para realizar la instalación:

 [me@linuxbox diction-1.11]$ sudo make install

Después de realizar la instalación, podemos comprobar que el programa está listo:

 [me@linuxbox diction-1.11]$ which diction
 /usr/local/bin/diction
 [me@linuxbox diction-1.11]$ man diction

¡Y ahí lo tenemos!

Resumiendo

En este capítulo, hemos visto como tres simples comandos:

./configure
make
make install

pueden usarse para construir muchos paquetes de código fuente. También hemos visto el importante rol que make juega en el mantenimiento de programas. El programa makepuede usarse para cualquier tarea que se necesite para mantener una relación objetivo/dependencia, no sólo para compilar código fuente.

Para saber más


No hay comentarios:

Publicar un comentario

Nota: solo los miembros de este blog pueden publicar comentarios.