Curso de programación en C para GNU/Linux (II)

En la pasada entrega; de este curso de programación en C para GNU/Linux hablamos de procesos, tareas, hilos, planificación, y demás. En esta ocasión, hablaremos del GCC (antiguamente conocido como GNU C Compiler y actualmente como GNU Compiler Collection), de los Makefiles y de cómo escribir y ejecutar nuestro primer programa en C para GNU/Linux.

 

El GCC

Las siglas GCC significan actualmente “GNU Compiler Collection“ (“Colección de compiladores GNU”). Antes estas mismas siglas significaban “GNU C Compiler” (“Compilador C de GNU”), si bien ahora se utilizan para denominar a toda una colección de compiladores de diversos lenguajes como C, C++, Objetive C, Chill, Fortran, y Java. Esta colección de compiladores está disponible para prácticamente todos los Sistemas Operativos, si bien es característica de entornos UNIX libres y se incluye en la práctica totalidad de distribuciones de GNU/Linux. En su desarrollo participan voluntarios de todas las partes del mundo y se distribuye bajo la licencia GPL (“General Public License”) lo que lo hace de libre distribución: está permitido hacer copias de él y regalarlas o venderlas siempre que se incluya su código fuente y se mantenga la licencia. Nosotros nos referiremos al GCC únicamente como el compilador de C estándar en GNU/Linux.

Compilación básica

GCC es un compilador de línea de comandos, aunque existen numerosos IDE o entornos de desarrollo que incluyen a GCC como su motor de compilación. La manera más simple de llamar a GCC es esta:

gcc codigo.c –o ejecutable

Así el GCC compilará el código fuente que haya en “codigo.c” y generará un fichero ejecutable en “ejecutable”. Si todo el proceso se ha desarrollado correctamente, el GCC no devuelve ningún mensaje de confirmación. En realidad la opción “-o” para indicar el fichero de salida no es necesaria, y si no se indica se guarda el resultado de la compilación en el fichero “a.out”.

Muchos proyectos de software están formados por más de un fichero fuente, por lo que habrá que compilar varios ficheros para generar un único ejecutable. Esto se puede hacer de forma sencilla llamando a GCC con varios ficheros fuente y un ejecutable:

gcc menu.c bd.c motor.c –o juego

Sin embargo es bastante probable que todos los ficheros fuente de un mismo proyecto no se encuentren en el mismo directorio, y que conforme el proyecto crezca, existan muchos ficheros de cabeceras (los típicos “.h”) y se alojen en directorios diferentes. Para evitar problemas a la hora de tratar con proyectos semejantes, podemos hacer uso de la opción “-I” e incluir los ficheros que sean necesario. Imaginemos que tenemos un proyecto en el que todos los ficheros fuente están dentro del directorio “src” y todos los ficheros de cabeceras están en el directorio “include”. Podríamos compilar el proyecto de la siguiente manera:

gcc ./src/*.c –Iinclude –o juego

Paso a paso

Hasta ahora hemos dado por hecho que es normal que un compilador realice todos los pasos necesarios para obtener un ejecutable partiendo del código fuente, si bien esto no tiene por qué ser así. A la hora de generar un ejecutable hay una serie de procesos implicados:

  1. Edición del código fuente → código fuente.
  2. Preprocesado → código fuente preprocesado.
  3. Compilación → código ensamblador.
  4. Ensamblado → código objeto.
  5. Enlazado → ejecutable.

Mediante el GCC pueden realizarse todos ellos secuencialmente hasta conseguir el ejecutable. Eso es lo que hemos estado haciendo en los ejemplos anteriores, pero en ocasiones es conveniente parar el proceso en un paso intermedio para evaluar los resultados:

  • Con la opción “-E” detenemos el proceso en la etapa de preprocesado, obteniendo código fuente preprocesado.
  • Con la opción “-S” se detiene en la etapa de compilación, pero no ensambla el código.
  • Con la opción “-c”, compila y ensambla el código, pero no lo enlaza, obteniendo código objeto.

Si no indicamos ninguna de estas opciones, se realizarán las cuatro fases de las que se encarga el GCC: preprocesado, compilación, ensamblado y enlazado.

Ahora ya controlamos más el proceso. Cuando un proyecto involucra muchos ficheros es bastante normal que no todas sus partes tengan las mismas opciones de compilación. Por ello es muy útil generar separadamente los respectivos códigos objeto, y cuando ya estén todos generados, enlazarlos para obtener el ejecutable:

gcc –c bd.c –o bd.o
gcc –c motor.c –lgraphics –o motor.o
gcc –c menu.c –lcurses –o menu.o
gcc bd.o motor.o menu.o –o juego

Librerías

Conforme un proyecto va ganando entidad se hace casi irremediable el uso de librerías (realmente son “bibliotecas”) de funciones, que permiten reutilizar código de manera cómoda y eficiente.

Para utilizar librerías estándar en el sistema es necesario emplear la opción “-l” a la hora de llamar a GCC:

gcc –c menu.c –lcurses –o menu.o

La compilación de este fichero (“menu.c”) requiere que esté instalada la librería curses o ncurses en el sistema, por ejemplo (la librería se llamará casi con seguridad “libncurses”). Si la librería no es una librería estándar en el sistema, sino que pertenece únicamente a nuestro proyecto, podremos indicar la ruta empleando la opción “-L”:

gcc –c motor.c –L./libs/librería-motor –o motor.o

Optimizaciones

El GCC incluye opciones de optimización en cuanto al código generado. Existen 3 niveles de optimización de código:

  1. Con “-O1” conseguimos optimizaciones en bloques repetitivos, operaciones con coma flotante, reducción de saltos, optimización de manejo de parámetros en pila, etc.
  2. Con “-O2” conseguimos todas las optimizaciones de “-O1” más mejoras en el abastecimiento de instrucciones al procesador, optimizaciones con respecto al retardo ocasionado al obtener datos del “heap” o de la memoria, etc.
  3. Con “-O3” conseguimos todas las optimizaciones de “-O2” más el desenrollado de bucles y otras prestaciones muy vinculadas con el tipo de procesador.

Si queremos tener un control total acerca de las opciones de optimización empleadas podremos utilizar la opción “-f”:

  • “-ffastmath”: genera optimizaciones sobre las operaciones de coma flotante, en ocasiones saltándose restricciones de estándares IEEE o ANSI.
  • “-finline-functions”: expande todas las funciones “inline” durante la compilación.
  • “-funroll-loops”: desenrolla todos los bucles, convirtiéndolos en una secuencia de instrucciones. Se gana en velocidad a costa de aumentar el tamaño del código.

Debugging

Los errores de programación o “bugs” son nuestros compañeros de viaje a la hora de programar cualquier cosa. Es muy común programar cualquier aplicación sencillísima y que por alguna mágica razón no funcione correctamente, o lo haga sólo en determinadas ocasiones (esto desespera aún más). Por ello, muchas veces tenemos que hacer “debugging”, ir a la caza y captura de nuestros “bugs”. La manera más ruin de buscar fallos todos la conocemos (aunque muchas veces nos dé vergüenza reconocerlo), en lugar de pelearnos con “debuggers”, llenar el código de llamadas a printf() sacando resultados intermedios es lo más divertido. En muchas ocasiones hacemos de ello un arte y utilizamos variables de preprocesado para indicar qué parte del código es de “debug” y cuál no. Para indicar una variable de preprocesado en GCC se utiliza la opción “-D”:

gcc –DDEBUG prueba.c –o prueba

Si queremos optar por una alternativa más profesional, quizá convenga utilizar las opciones “-g” o “-ggdb” para generar información extra de “debug” en nuestro ejecutable y poder seguir de forma más cómoda su ejecución mediante el GDB (“GNU Debugger”).

Si deseamos obtener todas las posibles advertencias en cuanto a generación del ejecutable partiendo de nuestro código fuente, emplearemos “-Wall”, para solicitar todos los “warnings” en los que incurra nuestro código. Así mismo, podríamos utilizar la opción “-ansi” o “-pedantic” para tratar de acercar nuestros programas al estándar ANSI C.

make world

Hemos visto en el anterior apartado cómo el desarrollo de un programa puede involucrar muchos ficheros diferentes, con opciones de compilación muy diversas y complejas. Esto podría convertir la programación de herramientas que involucren varios ficheros en un verdadero infierno. Sin embargo, make permite gestionar la compilación y creación de ejecutables, aliviando a los programadores de éstas tareas.

Con make deberemos definir solamente una vez las opciones de compilación de cada módulo o programa. El resto de llamadas serán sencillas gracias a su funcionamiento mediante reglas de compilación. Además, make es capaz de llevar un control de los cambios que ha habido en los ficheros fuente y ejecutables y optimiza el proceso de edición-compilación-depuración evitando recompilar los módulos o programas que no han sido modificados.

Makefile, el guión de make

Los Makefiles son los ficheros de texto que utiliza make para llevar la gestión de la compilación de programas. Se podrían entender como los guiones de la película que quiere hacer make, o la base de datos que informa sobre las dependencias entre las diferentes partes de un proyecto.

Todos los Makefiles están ordenados en forma de reglas, especificando qué es lo que hay que hacer para obtener un módulo en concreto. El formato de cada una de esas reglas es el siguiente:

objetivo : dependencias
	comandos

En “objetivo” definimos el módulo o programa que queremos crear, después de los dos puntos y en la misma línea podemos definir qué otros módulos o programas son necesarios para conseguir el “objetivo”. Por último, en la línea siguiente y sucesivas indicamos los comandos necesarios para llevar esto a cabo. Es muy importante que los comandos estén separados por un tabulador de el comienzo de línea. Algunos editores como el mcedit cambian los tabuladores por 8 espacios en blanco, y esto hace que los Makefiles generados así no funcionen. Un ejemplo de regla podría ser el siguiente:

juego : ventana.o motor.o bd.o
	gcc –O2 –c juego.c –o juego.o
	gcc –O2 juego.o ventana.o motor.o bd.o –o juego

Para crear “juego” es necesario que se hayan creado “ventana.o”, “motor.o” y “bd.o” (típicamente habrá una regla para cada uno de esos ficheros objeto en ese mismo Makefile).

En los siguientes apartados analizaremos un poco más a fondo la sintaxis de los Makefiles.

Comentarios en Makefiles

Los ficheros Makefile pueden facilitar su comprensión mediante comentarios. Todo lo que esté escrito desde el carácter “#” hasta el final de la línea será ignorado por make. Las líneas que comiencen por el carácter “#” serán tomadas a todos los efectos como líneas en blanco.

Es bastante recomendable hacer uso de comentarios para dotar de mayor claridad a nuestros Makefiles. Podemos incluso añadir siempre una cabecera con la fecha, autor y número de versión del fichero, para llevar un control de versiones más eficiente.

Variables

Es muy habitual que existan variables en los ficheros Makefile, para facilitar su portabilidad a diferentes plataformas y entornos. La forma de definir una variable es muy sencilla, basta con indicar el nombre de la variable (típicamente en mayúsculas) y su valor, de la siguiente forma:

CC = gcc –O2

Cuando queramos acceder al contenido de esa variable, lo haremos así:

$(CC) juego.c –o juego

Es necesario tener en cuenta que la expansión de variables puede dar lugar a problemas de expansiones recursivas infinitas, por lo que a veces se emplea esta sintaxis:

CC := gcc
CC := $(CC) –O2

Empleando “:=” en lugar de “=” evitamos la expansión recursiva y por lo tanto todos los problemas que pudiera acarrear.

Además de las variables definidas en el propio Makefile, es posible hacer uso de las variables de entorno, accesibles desde el intérprete de comandos. Esto puede dar pie a formulaciones de este estilo:

SRC = $(HOME)/src

juego :
	gcc $(SCR)/*.c –o juego

Un tipo especial de variables lo constituyen las variables automáticas, aquellas que se evalúan en cada regla. A mí, personalmente, me recuerdan a los parámetros de un script. En la siguiente tabla tenemos una lista de las más importantes:

Variable Descripción
$@ Se sustituye por el nombre del objetivo de la presente regla.
$* Se sustituye por la raíz de un nombre de fichero.
$< Se sustituye por la primera dependencia de la presente regla.
$^ Se sustituye por una lista separada por espacios de cada una de las dependencias de la presente regla.
$? Se sustituye por una lista separada por espacios de cada una de las dependencias de la presente regla que sean más nuevas que el objetivo de la regla.
$(@D) Se sustituye por la parte correspondiente al subdirectorio de la ruta del fichero correspondiente a un objetivo que se encuentre en un subdirectorio.
$(@F) Se sustituye por la parte correspondiente al nombre del fichero de la ruta del fichero correspondiente a un objetivo que se encuentre en un subdirectorio.

Tabla: Lista de las variables automáticas más comunes en Makefiles.

Reglas virtuales

Es relativamente habitual que además de las reglas normales, los ficheros Makefile pueden contener reglas virtuales, que no generen un fichero en concreto, sino que sirvan para realizar una determinada acción dentro de nuestro proyecto software. Normalmente estas reglas suelen tener un objetivo, pero ninguna dependencia.

El ejemplo más típico de este tipo de reglas es la regla “clean” que incluyen casi la totalidad de Makefiles, utilizada para “limpiar” de ficheros ejecutables y ficheros objeto los directorios que haga falta, con el propósito de rehacer todo la próxima vez que se llame a “make”:

clean :
	rm –f juego *.o

Esto provocaría que cuando alguien ejecutase “make clean”, el comando asociado se ejecutase y borrase el fichero “juego” y todos los ficheros objeto. Sin embargo, como ya hemos dicho, este tipo de reglas no suelen tener dependencias, por lo que si existiese un fichero que se llamase “clean” dentro del directorio del Makefile, make consideraría que ese objetivo ya está realizado, y no ejecutaría los comandos asociados:

txipi@neon:~$ touch clean
txipi@neon:~$ make clean
make: `clean' está actualizado.

Para evitar este extraño efecto, podemos hacer uso de un objetivo especial de make, .PHONY. Todas las dependencias que incluyamos en este objetivo obviarán la presencia de un fichero que coincida con su nombre, y se ejecutarán los comandos correspondientes. Así, si nuestro anterior Makefile hubiese añadido la siguiente línea:

.PHONY : clean

habría evitado el anterior problema de manera limpia y sencilla.

Reglas implícitas

No todos los objetivos de un Makefile tienen por qué tener una lista de comandos asociados para poder realizarse. En ocasiones se definen reglas que sólo indican las dependencias necesarias, y es el propio make quien decide cómo se lograrán cada uno de los objetivos. Veámoslo con un ejemplo:

juego : juego.o
juego.o : juego.c

Con un Makefile como este, make verá que para generar “juego” es preciso generar previamente “juego.o” y para generar “juego.o” no existen comandos que lo puedan realizar, por lo tanto, make presupone que para generar un fichero objeto basta con compilar su fuente, y para generar el ejecutable final, basta con enlazar el fichero objeto. Así pues, implícitamente ejecuta las siguientes reglas:

cc –c juego.c –o juego.o
cc juego.o –o juego

Generando el ejecutable, mediante llamadas al compilador estándar.

Reglas patrón

Las reglas implícitas que acabamos de ver, tienen su razón de ser debido a una serie de “reglas patrón” que implícitamente se especifican en los Makefiles. Nosotros podemos redefinir esas reglas, e incluso inventar reglas patrón nuevas. He aquí un ejemplo de cómo redefinir la regla implícita anteriormente comentada:

%.o : %.c
$(CC) $(CFLAGS) $< -o $@

Es decir, para todo objetivo que sea un “.o” y que tenga como dependencia un “.c”, ejecutaremos una llamada al compilador de C ($(CC)) con los modificadores que estén definidos en ese momento ($(CFLAGS)), compilando la primera dependencia de la regla ($<, el fichero “.c”) para generar el propio objetivo ($@, el fichero “.o”).

Invocando al comando make

Cuando nosotros invocamos al comando make desde la línea de comandos, lo primero que se busca es un fichero que se llama “GNUmakefile”, si no se encuentra se busca un fichero llamado “makefile” y si por último no se encontrase, se buscaría el fichero “Makefile”. Si no se encuentra en el directorio actual ninguno de esos tres ficheros, se producirá un error y make no continuará:

txipi@neon:~$ make
make: *** No se especificó ningún objetivo y no se encontró ningún makefile.  Alto.

Existen además varias maneras de llamar al comando make con el objeto de hacer una traza o debug del Makefile. Las opciones “-d”, “-n”, y “-W” están expresamente indicadas para ello. Otra opción importante es “-jN”, donde indicaremos a make que puede ejecutar hasta “N” procesos en paralelo, muy útil para máquinas potentes.

Ejemplo de Makefile

La manera más sencilla de entender cómo funciona make es con un Makefile de ejemplo:

# Makefile de ejemplo
#
# version 0.1
#

CC := gcc
CFLAGS := -O2

MODULOS = ventana.o gestion.o bd.o juego

.PHONY : clean install

all : $(MODULOS)

%.o : %.c
	$(CC) $(CFLAGS) –c $<.c –o $@

ventana.o : ventana.c

bd.o : bd.c

gestion.o : gestion.c ventana.o bd.o
	$(CC) $(CFLAGS) –c $<.c –o $@
	$(CC) $* -o $@

juego: juego.c ventana.o bd.o gestion.o
	$(CC) $(CFLAGS) –c $<.c –o $@
	$(CC) $* -o $@

clean:
	rm –f $(MODULOS)

install:
	cp juego /usr/games/juego

Programando en C para GNU/Linux

Llevamos varios apartados hablando de todo lo que rodea a la programación en GNU/Linux, pero no terminamos de entrar en materia. En lo sucesivo comenzaremos desde lo más básico, para ir posteriormente viendo las llamadas al sistema más comunes y terminar con Intercomunicación Entre Procesos (IPC) y sockets en redes TCP/IP.

Hola, mundo!

Si hay un programa obligatorio a la hora de empezar a programar en un lenguaje de programación, ese es el mítico “Hola, mundo!”. La manía de utilizar un programa que saque por pantalla “Hola, mundo!” para mostrar un programa de ejemplo en un determinado lenguaje se remonta –una vez más- a los orígenes de C y UNIX, con Kerningan, Ritchie, Thompson y compañía haciendo de las suyas.

Para programar un “Hola, mundo!” en C para GNU/Linux simplemente tendremos que editar un fichero, “hola.c”, que contenga algo similar a esto:

#include <stdio.h>

int main( int argc, char *argv[] )
{
	printf( “Hola, mundo!\n” );

	return 0;
}

Queda fuera del ámbito de este curso explicar de forma detallada la sintaxis de C, por lo que pasaremos a analizar el proceso de compilación desde nuestro fichero fuente (“hola.c”) al fichero ejecutable (“hola”):

txipi@neon:~$ gcc hola.c –o hola
txipi@neon:~$ ./hola
Hola, mundo!
txipi@neon:~$

Como podemos observar, el proceso es muy sencillo. Hay que tener especial cuidado en añadir el directorio a la hora de llamar al ejecutable (“./hola”) porque en GNU/Linux la variable PATH no contiene al directorio actual. Así, por mucho que hagamos “cd” para cambiar a un determinado directorio, siempre tendremos que incluir el directorio en la llamada al ejecutable, en este caso incluimos el directorio actual, es decir, “.”.

10 pensamientos en “Curso de programación en C para GNU/Linux (II)

  1. JoNaN88

    El curso esta muy bien, pero los que vienen de Windows (que supongo que son a los que va dirigido), tienen un poco de consol-fobia, así que no se si no saldrán asustados 😉

    Yo por mi parte he aprendido alguna cosilla sobre los makefile que no sabia, buen trabajo txipi, estamos esperando la siguiente entrega xD

    Responder
  2. Luis Manuel

    Me parecio muy interesante lo que hay en esta page.. sigan adelante con este mundo de la programacion.. saludos.. muy bien hay que aprender algo nuevo hoy…

    Responder
  3. Luis Zambrano

    Excelente entrega, asombra la sencillez y la sequencia con la que abordas el tema. Digno de emular tu forma de escribir en pocos párrafos y subtitulos información que en ocasiones, quienes redactamos artículos, nos cuesta descifrar por donde comenzar.

    Responder
  4. benya

    wenas
    tengo un makefile (se supone listo para instalar) y al ejecutar el make install me manda el siguente error

    echo Moving autodock3 to
    Moving autodock3 to
    mv autodock3
    mv: falta el operando archivo de destino después de `autodock3′
    Pruebe `mv –help’ para más información.
    make: *** [install] Error 1

    alguna idea de que puede ser???
    gracias

    Responder
  5. Pingback: Curso de programación en C para GNU/Linux (III) « txipi:blog

Deja un comentario

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