[LinuxFocus-icon]
Hogar  |  Mapa  |  Indice  |  Busqueda

Noticias | Arca | Enlaces | Sobre LF
Este artículo está disponible en los siguientes idiomas: English  Castellano  ChineseGB  Deutsch  Francais  Italiano  Portugues  Russian  Turkce  Arabic  

convert to palmConvert to GutenPalm
or to PalmDoc

[Lorne Bailey]
por Lorne Bailey
<sherm_pbody(at)yahoo.com>

Sobre el autor:

Lorne vive en Chicago y trabaja como consultor informático especializado en el intercambio de datos entre bases de datos Oracle. Como se decidió programar únicamente en ambientes *nix ha podido escapar del infierno de las DLL. Actualmente está realizando su master de grado en Ciencias Informáticas.



Taducido al español por:
Walter Echarri <wecharri(at)yahoo.com>

Contenidos:

 

GCC: el origen de todo

[Illustration]

Resumen:

El presente artículo muestra como usar el compilador gcc y presupone conocimientos básicos del lenguaje C. Les enseñaremoas a invocar el compilador desde la línea de comandos para códigos fuentes sencillos escritos en C. Seguiremos con un análisis de lo que realmente ocurre durante la compilación de programas y cómo controlar la situación. Por último, analizaremos brevemente el uso del depurador.



 

Las reglas del GCC

¿Se pueden imaginar compilar software libre con un compilador propietario de código cerrado? ¿Cómo sabríamos qué ocurre en nuestro ejecutable? Podría haber todo tipo de puertas traseras o troyanos. Ken Thompson, programó un compilador que dejaba una puerta trasera en el programa 'login' y que perpetuaba el troyano cuando el compilador se compilaba a sí mismo. Puede leerse los detalles de este artículo clásico aquí. Afortunadamente, tenemos al compilador gcc. Cada vez que hacemos configure; make; make install gcc realiza el trabajo pesado tras bambalinas. ¿Cómo hacemos para que gcc trabaje para nosotros? Comenzaremos escribiendo un juego de naipes, pero lo haremos con el objeto de demostrar la funcionalidad del compilador. Puesto que empezaremos desde cero, necesitaremos una comprensión del proceso de compilación para saber qué necesitamos hacer y en qué orden para crear el ejecutable. Haremos un repaso general de cómo se compila un programa C y las opciones para que gcc haga lo que queremos que haga. Las diferentes etapas (y las herramientas necesarias) son: Pre-compilación (gcc -E), Compilación (gcc), Ensamblado (as), y Enlazado (ld).

 

En los comienzos...

Ante todo, debemos saber cómo invocar al compilador. En realidad, es sencillo. Comenzaremos con el primer programa en C más clásico de todos los tiempos (los programadores experimentados sabrán perdonarme).

#include <stdio.h>

int main()
{ printf("¡Hola mundo!\n"); }

Guarde el archivo como juego.c. Se puede compilar desde la línea de comandos, ejecutando

gcc juego.c
Por defecto, el compilador C crea un ejecutable llamado a.out que se puede ejecutar haciendo
a.out
Hello World

Cada vez que se compila un programa, el nuevo ejecutable a.out sobreescribirá el programa anterior. De esta manera nos resultará difícil saber qué programa se corresponde con el archivo a.out. Podemos evitar el problema indicándole a gcc que deseamos dar al ejecutable un nombre determinado mediante la opción -o . Llamaremos a este programa juego, aunque podríamos haberlo llamado como quisiéramos puesto que C carece de las restricciones que posee Java en cuanto a los nombres de archivos.
gcc -o juego juego.c
juego
Hello World

A estas alturas, estamos bastante lejos de tener un programa útil. Si piensa que esto no sirve para nada debiera considerar que tenemos un programa que compila y funciona. Queremos asegurarnos que esto siga siendo así a medida que agreguemos poco a poco cierta funcionalidad al programa. Parece ser que todos los principiantes quieren escribir un programa con 1000 líneas y corregirlas todas simultáneamente. Nadie, absolutamente nadie, puede hacer esto. La idea es hacer un pequeño programa que funcione, hacer pequeños cambios y ejecutarlo nuevamente. Esto limita la cantidad de errores que se deben corregir a la vez. Por otra parte, de esta manera uno sabe exactamente lo que hizo mal y dónde centrar nuestra atención. Esto evita crear programas que uno piensa que deberían funcionar, y que incluso compilan, pero que nunca terminan siendo ejecutables. Recuerden que el sólo hecho que compile no significa que el código sea correcto.

Nuestro siguiente paso consiste en crear un archivo de cabecera para nuestro juego. Un archivo de cabecera agrupa distintos tipos de datos y declaraciones de funciones en un mismo lugar. Esto nos asegura que las estructuras de datos estén consistentemente definidas de modo que cada parte de nuestro programa 'vea' todo exactamente de la misma manera.

#ifndef BARAJA_H
#define BARAJA_H

#define TAMBARAJA 52

typedef struct BARAJA_t
{
  int naipe[TAMBARAJA];
  /* número de naipes empleados */
  int repartidos;
}baraja_t;

#endif /* BARAJA_H */

Guardamos este archivo como baraja.h. Sólo los archivos .c se compilan, por lo tanto debemos modificar el código de nuestro juego.c. En la línea 2, escribamos #include "baraja.h" y en la 5, baraja_t baraja. Para asegurarnos que no hemos cometido ningún error, lo compilamos nuevamente.

gcc -o juego juego.c

No hay errores, por lo tanto, ningún problema. Si no compila deberemos corregirlo hasta que lo haga.

 

Pre-compilación

¿Cómo sabe el compilador qué tipo de datos es baraja_t? Pues bien, durante la pre-compilación el compilador copia el archivo "baraja.h" en el archivo "juego.c". Las directivas del precompilador en el código fuente están precedidas por un "#". No obstante, puede invocarse al precompilador mediante la interfaz de gcc mediante la opción -E.

gcc -E -o juego_precompilado.txt juego.c
wc -l juego_precompilado.txt
  3199 juego_precompilado.txt
¡Se obtienen casi 3200 líneas como salida! La mayoría de ellas provienen del archivo include stdio.h, pero si se las analiza atentamente, también se encuentran nuestras declaraciones. Si no se asigna un nombre al archivo de salida mediante la opción -o la salida es por consola. El proceso de pre-compilación brinda una mayor flexibilidad al código alcanzando tres objetivos principales.
  1. Copia los archivos "#include" en el archivo fuente a compilar.
  2. Reemplaza los "#define" por su valor real.
  3. Reemplaza las macros cuando son llamadas.
Esto permite tener constantes válidas en todo el código, definidas en un sólo lugar y actualizadas automáticamente cada vez que se modifican sus valores (como ocurre con TAMBARAJA, que representa el número de naipes en la baraja) En la práctica, casi nunca se usa la opción -E por sí sola.

 

Compilación

Como paso intermedio, gcc traduce el código a lenguaje ensamblador. Al hacerlo debe intuir lo que se pretende hacer analizando el código fuente. Si se comete un error de sintaxis, dará un mensaje de error y la compilación se abortará. La gente a veces confunde este paso con el proceso entero. Sin embargo, a gcc le queda mucho trabajo por delante.

 

Ensamblado

As transforma el código ensamblador a código objeto. En realidad, el código objeto no puede ejecutarse en la CPU pero está muy cerca de poder hacerlo. La opción -c del compilador transforma un archivo .c en un archivo objeto con extensión .o. Si ejecutamos

gcc -c juego.c
creamos automáticamente un archivo denominado juego.o. Aquí hemos tropezado con un algo importante. Podemos tomar cualquier archivo .c y crear un archivo objeto con él. Como veremos a continuación, podemos combinar estos archivos objetos en un archivo ejecutable durante la etapa de enlazado. Vayamos a nuestro ejemplo. Puesto que estamos programando un juego de naipes y hemos definido una baraja como baraja_t, escribiremos una función para poder barajar los naipes. Esta función toma un puntero de tipo baraja y le pasa un conjunto de valores aleatorios para los diferentes naipes. El arreglo 'retirados' permite contabilizar los naipes utilizados. Este arreglo de TAMBARAJA miembros evita dar valores duplicados a un mismo naipe.

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "baraja.h"

static time_t semilla = 0;

void barajar(baraja_t *pbaraja)
{
  /* permite contabilizar los números utilizados */
  int retirados[TAMBARAJA] = {0};
  int i;

  /* Inicialización aleatoria */
  if(0 == semilla)
  {
    semilla = time(NULL);
    srand(semilla);
  }
  for(i = 0; i < TAMBARAJA; i++)
  {
    int valor = -1;
    do
    {
      value = rand() % TAMBARAJA;
    }
    while(retirados[valor] != 0);

    /* marcar valor como utilizado */
    retirados[valor] = 1;

    /* sentencia de depuración */
    printf("%i\n", valor);
    pbaraja->naipe[i] = valor;
  }
  pbaraja->repartidos = 0;
  return;
}

Guardemos este archivo como barajar.c. Hemos puesto una sentencia de depuración en el código de modo que al ejecutar el programa éste escriba el número de naipes que genera. Esto no agrega ninguna funcionalidad al programa pero resulta crucial para ver qué está ocurriendo. Puesto que recién estamos empezando a programar nuestro juego, no tenemos otra manera de asegurarnos que nuestra función está haciendo lo que queremos que realmente haga. Con la sentencia printf podemos saber exactamente lo que está pasando de modo que al pasar a la próxima etapa sabemos que los naipes están bien barajados. Una vez que sepamos que todo funciona correctamente podemos eliminar la línea de nuestro código fuente. Esta técnica para depurar programas parece rudimentaria pero logra su cometido con una mínima cantidad de esfuerzo. Posteriormente, discutiremos técnicas de depuración más sofisticadas.

Obsérvense dos cosas.
  1. Pasamos un parámetro por referencia (su dirección se obtiene mediante el operador '&' dirección de). De esta forma, se pasa la dirección de la variable a la función de manera que ésta pueda cambiar el valor de la propia variable. Es posible hacer lo mismo con variables globales aunque su uso no es aconsejable. Los punteros constituyen una parte importante del lenguaje C y es fundamental entenderlos correctamente.
  2. Usamos una llamada a una función desde un archivo .c nuevo. El sistema operativo siempre busca una función 'main' y comienza la ejecución a partir de ella. Puesto que barajar.c carece de una función 'main', no puede dar lugar a un ejecutable autónomo. Es necesario, combinarla con otro programa que contenga una función 'main' y que la llame desde allí.

Hagamos

gcc -c barajar.c
y asegúrese que aparezca un nuevo archivo denominado barajar.o. Edite el archivo juego.c y en la línea 7 (a continuación de la declaración de la variable baraja_t baraja) agregue la línea
barajar(&baraja);
Ahora, si intentamos crear el ejecutable como hicimos anteriormente, obtendremos un mensaje de error
gcc -o juego juego.c

/tmp/ccmiHnJX.o: In function `main':
/tmp/ccmiHnJX.o(.text+0xf): undefined reference to `barajar'
collect2: ld returned 1 exit status

La compilación funcionó porque la sintaxis de nuestro código es correcta. No obstante, el enlazado falló porque no le dijimos al compilador dónde se encuentra la función 'barajar'. ¿En qué consiste el enlazado y cómo le decimos al compilador dónde encontrar a la función?

 

Enlazado

El enlazador, ld, toma el código objeto creado previamente por as y lo transforma en ejecutable mediante el comando

gcc -o juego juego.o barajar.o
Esto combinará los dos códigos objeto y creará el ejecutable juego.

El enlazador encuentra a la función barajar en el objeto barajar.o y la incluye en el ejecutable. La verdadera belleza de los archivos objeto consiste en que si deseamos utilizar nuevamente la función, todo lo que tenemos que hacer es incluir el archivo "baraja.h" y enlazar el archivo objeto barajar.o en el nuevo ejecutable.

La reutilización de código es sistemática. Así, por ejemplo, anteriormente no tuvimos que escribir la función printf que utilizamos como sentencia de depuración ya que el enlazador encontró su definición en el archivo incluido mediante #include <stdlib.h> y la enlazó con el código objeto almacenado en la biblioteca C (/lib/libc.so.6). De esta manera podemos usar la función de algún programador que sabemos que funciona correctamente y concentrarnos en la resolución de nuestros propios problemas. Es por eso que los archivos de cabecera normalmente contienen únicamente los datos y las definiciones de las funciones pero no los cuerpos de las funciones. Normalmente, uno crea archivos objetos o bibliotecas para que el enlazador los integre en el ejecutable. Si no se incluyen las definiciones de las funciones en las cabeceras pueden ocurrir errores. ¿Qué podemos hacer para asegurarnos que todo funcione correctamente?

 

Las dos opciones más importantes

La opción -Wall activa todos las advertencias disponibles sobre la sintaxis del lenguaje para ayudarnos a verificar que nuestro código es correcto y portable tanto como sea posible. Cuando se usa esta opción, al compilar nuestro código se obtienen mensajes del tipo :

juego.c:9: warning: implicit declaration of function `barajar'
Esto significa que nos resta algo por hacer. Necesitamos añadir una línea en el archivo de cabecera para para informarle al compilador sobre nuestra función barajar de modo que pueda saber lo que necesita hacer. Parece complicado, pero separa la definición de la implementación y nos permite usar nuestra función en cualquier lugar con tan sólo incluir nuestra nueva cabecera y enlazarla con nuestro código objeto. Incluyamos esta línea en el archivo barraja.h
void barajar(baraja_t *pbaraja);
Con lo cual desaparecerán todos los mensajes de advertencia.

Otra opción del compilador es la de optimización -O# (es decir, -O2). que permite elegir el nivel de optimización deseado. El compilador tiene toda una bolsa de trucos para hacer que el código se ejecute un poco más rápido. En el caso de pequeños programas como los nuestros no notaremos ninguna diferencia pero en el caso de programas más grandes puede incrementarse bastante la velocidad ejecución. La verán por todos lados, de modo que es necesario que conozcan qué significa.

 

Depurando

Como todo sabemos, que nuestro código compile no significa que vaya a funcionar de la manera que esperamos que lo haga. Se puede verificar que todos los números se usan una única vez haciendo

juego | sort - n | less
y verificando que no falta nada. ¿Qué hacemos si tenemos algún problema? ¿Cómo vemos debajo del capó y encontramos el error?

Puede analizarse el código con ayuda de un depurador. La mayoría de las distribuciones vienen acompañadas del clásico depurador gdb. Si las opciones de la línea de comandos les resultan intimidantes, KDE nos brinda una agradable interfaz gráfica con KDbg. Existen otras interfaces gráficas todas ellas muy similares entre sí. Para comenzar a depurar, hay que ir a Archivo->Ejecutable y buscar nuestro programa juego. Presionando F% o eligiendo del menú Ejecución->Ejecutar, se podrá ver la saida en otra ventana separada. ¿Qué sucede? En la ventana no se ve absolutamente nada. No se preocupen, KDbg no funciona mal. El problema es que no hemos puesto ninguna información de depuración en el ejecutable con lo cual KDbg no nos puede decir qué está ocurriendo internamente. La opción -g del compilador incluye la información necesario en los arhivos objeto. Se deben compilar los archivos objetos (con extensión .o) con esta opción, con lo cual el comando resulta ser
gcc -g -c barajar.c juego.c
gcc -g -o juego juego.o barajar.o
Esto inserta marcas en el ejecutable que permiten a gdb y KDbg saber qué está ocurriendo. El saber depurar correctamente es una habilidad muy importante y vale la pena aprender a hacerlo. La manera en que los depuradores ayudan a los programadores es colocando 'puntos de ruptura' en el código fuente. Intentemos colocar uno haciendo click derecho sobre la línea que contiene a la función barajar. Aparecerá un pequeño circulo rojo a continuación de dicha línea. Si se presiona F5 la ejecución del programa se detiene en esta línea. Presionando F8 entramos a la función barajar. ¡Estamos mirando el código de barajar.c! Podemos controlar la ejecución paso a paso y ver qué es lo que realmente está ocurriendo. Si posicionamos el puntero sobre una variable local veremos el valor almacenado en ella. Excelente. Es mucho mejor que esos printf ¿no es cierto?

 

Resumen

El presente artículo representa un rápido recorrido al proceso de compilación y depuración de programas escritos en C. Hemos analizado los pasos que sigue el compilador y las opciones que requiere gcc para llevarlos a cabo. Tratamos brevemente el enlazado de bibliotecas compartidas y finalizamos con una introducción a los depuradores. En realidad, lleva bastante tiempo saber lo que uno está haciendo, pero espero que este artículo haya servido para empezar con el pie derecho. Puede encontrarse más información en las páginas del manual man y de info de gcc, as y ld.

Escribir código por cuenta propia es una excelente forma de aprender. Para practicar, se puede considerar la estructura básica del programa del juego de cartas analizado y escribir un juego de blackjack. Tomense el tiempo para aprender a usar un depurador. Resulta mucho más fácil comenzar con una interfaz gráfica como KDbg Añadiendo poco a poco cierta funcionalidad al programa lo terminarán sin siquiera darse cuenta. Por lo tanto, ¡sigan practicando!

A continuación se detalla lo necesario para lograr un juego completo.

 

Enlaces

 

Formulario de "talkback" para este artículo

Cada artículo tiene su propia página de "talkback". A través de esa página puedes enviar un comentario o consultar los comentarios de otros lectores
 Ir a la página de "talkback" 

Contactar con el equipo de LinuFocus
© Lorne Bailey, FDL
LinuxFocus.org

Pinchar aquí para informar de algún problema o enviar comentarios a LinuxFocus
Información sobre la traducción:
en --> -- : Lorne Bailey <sherm_pbody(at)yahoo.com>
en --> es: Walter Echarri <wecharri(at)yahoo.com>

2002-03-15, generated by lfparser version 2.21