|
|
Este documento está disponible en los siguientes idiomas: English Castellano ChineseGB Deutsch Francais Italiano Portugues Russian Turkce Arabic |
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: |
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.
¿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).
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.cPor defecto, el compilador C crea un ejecutable llamado
a.out
que se puede ejecutar haciendo a.out Hello WorldCada 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.
¿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.
-E
por sí sola.
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.
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.ccreamos 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.
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.cy 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 statusLa 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?
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.oEsto 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?
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.
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 | lessy 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.oEsto 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?
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.
|
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:
|
2002-03-15, generated by lfparser version 2.21