gentoo linux, java, software libre y otras hierbas
mar, 02 2007 - 7:30 pm

Bibliotecas compartidas de C++ en Linux

A veces, varios programas necesitan hacer las mismas cosas, como E/S por ejemplo. Hace ya bastante tiempo se desarrolló el concepto de biblioteca (o librería) para adaptar esto. Las funciones se colocan en un archivo, y luego, cada vez que se crea un programa, este extrae de la biblioteca las funciones que necesita.

En su momento, esto fue un gran avance, pero tenía varias desventajas. Los ejecutables se hacen más grandes ya que cada uno de ellos incrusta código copiado de las bibliotecas. Si se encuentra un error en la biblioteca o se agrega una característica, el ejecutable no hace uso de ella a menos que se vuelva a crear.

La solución a esto son las bibliotecas compartidas (o librerías dinámicas). El mecanismo de las bibliotecas compartidas esta fuera del alcance de este artículo. Solo veremos como crearlas y utilizarlas.

Regresemos al programa del dado que hicimos en C. Este programa tiene dos archivos fuente. Supongamos que nuestra función tirarDado (del archivo tirador.c) nos es muy útil, no solo en nuestro programa, sino en muchos otros programas que utilicen dados. Podemos pensar entonces en colocar dicha función en una biblioteca para que otros programas puedan usarla.

Primero necesitamos crear la biblioteca compartida. Para ello compilamos el archivo que contiene la función así:

gcc -fPIC -c tirador.c

Ahora lo convertimos en una biblioteca compartida llamada libtirar.so.1.0:

gcc -shared -Wl,-soname,libtirar.so.1 -o libtirar.so.1.0 tirador.o

Por último crearemos un enlace para libtirar.so, para que el programa en ejecución no necesite mantener un registro de la información de versión en el nombre de la biblioteca compartida:

ln -s libtirar.so.1.0 libtirar.so.1
ln -s libtirar.so.1 libtirar.so

Ahora que tenemos la biblioteca, debemos crear el programa principal para que enlace con esa biblioteca en tiempo de ejecución, en lugar de incluir el código dentro del ejecutable:

gcc -o juego juego.c -L. -ltirar

La opción -L. le indica al compilador que busque bibliotecas en el directorio actual, y la opción -ltirar le indica que busque una biblioteca llamada libtirar.so.

Al ejecutar el programa, el sistema operativo cargará dinámicamente la biblioteca correcta, pero tiene que saber donde buscarla. Si la biblioteca no se encuentra en un lugar estándar (/usr/lib por ejemplo), puede asignar una variable de entorno para que le indique en dónde localizar bibliotecas adicionales:

export LD_LIBRARY_PATH=/home/usuario/mislibrerias

Por último, para ver qué librerías usa un programa, utilice el comando ldd:

ldd juego
libtirar.so.1 =>/mnt/hda1/home/casidiablo/juegolibrerias/libtirar.so.1 (0×40018000)
libc.so.6 => /lib/libc.so.6 (0×40029000)
/lib/ld-linux.so.2 =>/lib/ld-linux.so.2 (0×40000000)

Otro ejemplo de bibliotecas… crear una biblioteca estática

A diferencia de las bibliotecas compartidas, al momento de compilar bibliotecas estáticas, se incluyen en el programa las rutinas que utiliza de la biblioteca; en otras palabras las rutinas están insertas en el programa sin la posibilidad de ser utilizados por otro programa. Nuestra demostración se basará en la construcción de una biblioteca estática; dentro de nuestra biblioteca incluiremos un procedimiento que imprimirá un mensaje en pantalla el que será pasado por parámetro y una función que nos entrega en cálculo del factorial de un numero dado (pasado por parámetro); para que luego estas rutinas sean llamadas desde un programa de ejemplo.

El proceso para crear bibliotecas estáticas es escribir el código fuente, compilar a código objeto el fuente y crear la biblioteca con los archivos objetos. A continuación procederemos a escribir nuestras rutinas que serán incluidas en la biblioteca, los archivo de las rutinas serán factorial.c e imprimir.c

//imprimir.c
#include<stdio.h>
void imprimir_en_pantalla(const char* mensaje)
{
printf("%s",mensaje);
}
//factorial.c
#include<stdio.h>
long factorial(long numero)
{
if(numero>0)
return numero * factorial(numero-1);
else return 1;
}

Ahora compilaremos estos archivos fuente y los convertiremos en código objeto utilizando gcc:

gcc -c imprimir.c factorial.c

Esto creará los archivos objeto (imprimir.o y factorial.o). El siguiente paso será crear nuestra biblioteca estática con estos archivos de código objeto; a la biblioteca la llamaremos libejemploestatica.a:

ar rsc libejemploestatica.a imprimir.o factorial.o

El comando ar creará nuestra biblioteca (opcion c), introducirá los archivos objeto al archivo creado (opción r) y por último creará un índice de los módulos (opción s), esto último permite que el linker (ld) al momento de compilar (ld es llamado por gcc) no tenga que leerse toda la biblioteca completa ya que se agregan los índices y descripción de los módulos. El proceso de crear el índice de los módulos también se pude realizar con ranlib.

//miprograma.c
#include<stdio.h>
#include "libejemploestatica.h"
int main()
{
printf("Programa de ejemplo de utilizacion de biblioteca\n");
imprimir_en_pantalla("Vamos a calcular un factorial\n");
printf("El valor del factorial de 4 es: %i\n",factorial(4));
}

Ahora nos falta crear nuestro archivo de cabecera (header), que son los que tienen terminación .h, en este archivo se incluirán las llamadas a las rutinas de la biblioteca y es una manera elegante porque también puedes incluir estas llamadas en el programa que las vas utilizar. El archivo de cabecera libejemploestatica.h tendrá el siguiente contenido:

extern void imprimir_en_pantalla(const char*);
extern long fatorial(long);

No es necesario que el archivo de cabecera y biblioteca sean iguales antes de la extensión. Ahora crearemos el archivo ejecutable en base a nuestro programa principal (miprograma.c), el cual utilizará la biblioteca que hemos creado. Para ello debemos compilarlo utilizando la biblioteca:

gcc -o programa_ejecutable miprograma.c -L. -lejemploestatica

Con la opción -L indicamos donde se encuentra nuestro archivo de cabecera, como en este se encuentra en el mismo directorio utilizamos el punto (-L.). Con la opción -l indicamos el nombre de la librería. Fíjate que no es necesario colocar el nombre completo (libejemploestatica) ya que gcc asume que los nombre de bibliotecas empiezan con el prefijo “lib”. Ahora ya tendremos el archivo ejecutable listo para correr nuestro programa:

$ ./programa_ejecutable
Programa de ejemplo de utilizacion de biblioteca
Vamos a calcular un factorial
El valor del factorial de 4 es: 24

14 Comentarios | deja el tuyo

mar, 02 2007 - 7:01 pm

Construcción o creación (uso del comando make)

Cuando creamos pequeños programas como los que hemos hecho hasta ahora, no resulta muy difícil el proceso de compilación y enlace. Pero cuando se crean proyectos grandes, con varios archivos fuente (15, 30 o más) y librerías resultaría muy difícil o hasta imposible.

Linux viene con la utilería make de GNU. make lee de un archivo conocido como make toda la información que necesita para crear su programa. Ésta utilería es tan importante y popular que se ha especificado como estándar de POSIX.

make

make de GNU busca automáticamente un archivo make llamado GNUmakefile. Sino lo encuentra, busca makefile, y si tampoco lo encuentra busca Makefile, y si tampoco lo encuentra dice “que mierdas, no juegues conmigo que no me gusta perder tiempo” XD. Estos son los nombres predeterminados, pero puedes crear un archivo con el nombre que quieras y especificar al make que lo utilice. Los archivos make contienen información acerca de la compilación y enlace de su programa, con una sintaxis muy específica que make puede entender.

make tiene una gran variedad de reglas integradas. Por ejemplo, sabe que los archivos que terminan con .c son archivos fuente de C, y sabe cómo compilarlos para convertirlos en archivos objeto (.o). Puedes redefinir cualquiera de estas reglas si gustas. En el caso más simple, todo lo que necesitas especificar en tu archivo make es el nombre que va a tener su archivo ejecutable, así como los archivos .o que se necesitan para crearlo.

He aquí un archivo sencillo para make, que creará el programa de los dados:

juego:juego.o tirador.o
$(CC) -o $@ juego.o tirador.o

Ahora podrás crear el programa con tan solo un comando: make.

Hay ocasiones en las que hay que indicar como compilar cierto tipo de archivo, por ejemplo cuando se utilizan otro tipo de extensiones (.cpp, .cxx, .c++). El error que se generaría sería similar a este:

make: *** No hay ninguna regla para construir el objetivo ‘juego.o’, necesario para ‘juego’. Alto.

Vamos a ver un ejemplo de archivo makefile en donde se especifica explícitamente como construir dichos archivos objeto:

juegodados: juego.o tirador.o
$(CC) -o $@ juego.o tirador.o
juego.o: juego.c
$(CC) -c juego.c
tirador.o: tirador.c
$(CC) -c tirador.c

Ahora veamos un archivo makefile aún más completo:

#Makefile para crear programa para tirar dados
CFLAGS = -O
OBJS = juego.o tirador.o
all=dado
juego: $(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS)
juego.o: juego.c
$(CC) -c juego.c
tirador.o: tirador.c
$(CC) -c tirador.c
clean:
-$(RM) dado *.o

Este archivo make define una regla llamada clean, que se utiliza para eliminar todos los archivos previamente compilados (incluso el ejecutable) y empezar con un directorio limpio. Fíjate también que se pueden declarar una especie de variables (como CFLAGS, OBJS, etc.), que se pueden utilizar al momento de indicar algún comando. Además fíjate que se pueden utilizar comentarios con el símbolo #.

Y un último ejemplo más complejo no nos vendría mal. Vamos a ver como sería nuestro archivo makefile, si deseáramos utilizar bibliotecas compartidas (librerías dinámicas):

#Makefile para crear programa para tirar dados
#usando bibliotecas compartidas
CFLAGS = -O
OBJS = juego.o
LIBS=libtirar.so
all=dado
juego: $(OBJS) $(LIBS)
$(CC) $(CFLAGS) -o $@ $(OBJS) -L. -ltirar
juego.o: juego.c
$(CC) -c juego.c
tirador.o: tirador.c
$(CC) -fPIC -c $<
libtirar.so: tirador.o
-$(RM) libtirar*
$(CC) -shared -Wl,-soname,libtirar.so.1 \
-o libtirar.so.1.0 $<
ln -slibtirar.so.1.0 libtirar.so.1
ln -slibtirar.so.1 libtirar.so
clean:
-$(RM) dado *.o libtirar*

Importante!!!
Sino quieres tener problemas en el futuro lee esto: la sintaxis de make nos obliga a respetar los separadores. ¿qué son separadores? talvez te hayas fijado en la forma en que están tabulados los archivos. Esto no es maña mía ni mucho menos, esto es una regla para los archivos make; sino se tabularan lo más seguro es que en la ejecución del make apareciera este error:

makefile:2: *** falta un separador. Alto.

Así que por lo que más quieras: utiliza los separadores si quieres que funcione.

Opciones de línea de comandos de make

make tiene varias opciones útiles de línea de comandos. Por ej., si quieres especificar un archivo make alternativo, en lugar de los predeterminados, invoca a make de la siguiente forma:

make -f nombrearchivo

make es un programa muy sofisticado. Una de las cosas que hace es comprender las dependencias. Por ejemplo, sabe que los archivos .o se crean a partir de archivos .c. Tu programa puede consistir en varios archivos fuente .c. Si cambias uno, no es necesario volver a compilarlos todos cada vez que vayas a crearlo. Sólo necesitas volver a compilar el archivo fuente que haya cambiado. make comprende esto y compila solo aquellos archivos que no estén actualizados. Algunas veces será necesario que veas primero qué es lo que make necesita para crear el programa. Puedes hacer esto con el siguiente comando:
make -n
Esto le indica a make que analice el makefile y que reporte qué comandos emitirá para crear el programa. make no ejecutará ningún comando.

5 Comentarios | deja el tuyo

mar, 02 2007 - 5:51 pm

Depuración de programas en Linux

Todo buen entorno de desarrollo debe proporcionar la capacidad de depurar nuestros programas, por ello Linux posee una herramienta llamada gdb. gdb es una excelente herramienta de depuración con interfaz de línea de comandos (modo texto). Aunque existen herramientas como gdbtui que es una versión del mismo depurador con una interfaz más amigable (pero en modo texto también). Y si sos de los que les da pereza la consola, tranquilo, también existe una herramienta llamada xxgdb que es una versión del gdb con interfaz gráfica que se ejecuta en X Windows.

gdb

gdb le permite analizar el funcionamiento de un programa paso a paso, establecer puntos de interrupción (breakpoints), examinar y modificar variables por su nombre. Se puede utilizar tanto en programas de C como de C++. Si has manejado alguna vez depuradores y debuggers como el OllyDbg, el manejo de este se te hará realmente fácil.

Para preparar un programa para su depuración, es necesario compilarlo con la opción -g. Esto hace que en alguna parte del ejecutable, se guarde información del código fuente tal como es. De otra forma no prodrás usar el depurador de forma óptima. Si estás utilizando el make para crear los programas, puedes indicar esta opción en la variable CFLAGS (¿la recuerdas?), desde la línea de comandos así:

make CFLAGS=-g

Cuando hayas creado el programa, puedes comenzar la sesión de depuración con el comando: gdb dado. (Suponiendo que el ejecutable se llama dado):

GNU gdb 6.1-debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” for details.
This GDB was configured as “i386-linux”…
Using host libthread_db library “/lib/libthread_db.so.1″.
(gdb)

Estamos frente al prompt del gdb, que se representa con los caracteres (gdb), indicándonos que podemos escribir los comandos a ejecutar. gdb tiene muchos comando disponibles, para verlos digita el siguiente comando:

(gdb) help

Como puedes observar, el comando help despliega la lista de clases de comandos existentes en el gdb, para ver las instrucciones de determinada clase de comandos, digita help seguido del nombre del comando, por ejemplo así:

(gdb) help breakpoints

Por último para salir del gdb presiona la tecla q. A continuación veremos una tabla con comandos más útiles para el gdb:

Comando Función
break [nombrearhivo:]función Establecer un punto de interrupción en la entrada a la función del archivo llamado nombrearchivo.
run [listaargumentos] Iniciar el programa, y pasarle argumentos si es necesario.
bt Desplegar la pila del programa.
print expre Evaluar la expresión e imprimir el resultado. La expresión puede ser un nombre de variable o una función que retorne un valor.
c
cont
Cualquiera de estos dos comandos, continúan con la ejecución del programa desde le punto actual.
next Ejecutar la siguiente línea de l programa. Si la siguiente línea es una función, dicha función se ejecutará y el programa se detendrá en la siguiente línea después de la llamada a la función.
step Ejecutar la siguiente línea del programa, y entrar a la función si esa es una línea de función.
help [nombre] Mostrar la ayuda general, o mostrar la ayuda específica a [nombre] si se indico uno.
q salir del gdb.

Sesión de ejemplo de depuración con gdb

Ahora vamos a ver un ejemplo de como trabaja esta herramienta. Habiendo creado ya el programa (el de los dados) con la opción -g así:

g++ -g -o dados *.c

Pasamos a correr el depurador:

gdb dados

Con lo que aparece lo siguiente:

GNU gdb 6.1-debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” for details.
This GDB was configured as “i386-linux”…Using host libthread_db library “/lib/libthread_db.so.1″.

(gdb)

Vamos entonces a ver el código fuente de nuestro programa principal (el que contiene la función main), esto lo hacemos con el comando list y le indicamos de que línea a que línea queremos ver, en este caso de la 1 a la 25:

(gdb) list 1,25
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4
5 int tirarDado(void);
6
7 int main(int argc, char * argv[])
8 {
9 int i, iIter, dado[6];
10
11 if(argc<2)
12 {
13 printf(“Uso: %s n\n”,argv[0]);
14 return 1;
15 }
16 iIter = atoi(argv[1]);
17 memset(dado, 0, sizeof(dado));
18 for(i=0; i<iIter; i++)
19 {
20 dado[tirarDado() - 1]++;
21 }
22 printf(“%d tiradas\n”,iIter);
23 printf(“\tCara\tTiradas\n”);
24 for(i=0; i<6; i++)
25 {

Ahora establecemos un punto de interrupción (breakpoint), en la línea 16 (iIter = atoi(argv[1]);), que hará que le programa se detenga en este punto al correrlo:

(gdb) break 16
Breakpoint 1 at 0x804850b: file juego.c, line 16.

Corremos el programa con el comando run seguido de los argumentos:

(gdb) run 5
Starting program: /mnt/hda2/archivos/programas C/juego/dados 5
Breakpoint 1, main (argc=2, argv=0xbffffd24) at juego.c:16
16 iIter = atoi(argv[1]);
Current language: auto; currently c++

Y vemos como se ejecuta, y posteriormente para en la línea 16. Nos muestra el nombre de la función en donde nos encontramos (en este caso main), y nos muestra el trozo de código fuente. Ahora veamos el valor de la variable iIter, utilizando el comando print iIter. Es de resaltar que utilizamos el nombre real de la variable para referirnos a esta, y que al estar sobre la línea en el cual se asigna el valor a la variable, el valor que nos imprime es el de la variable sin inicializar:

(gdb) print iIter
$1 = -1073742556

Veamos entonces que pasa si saltamos a la siguiente instrucción, y volvemos a imprimir el valor de iIter:

(gdb) next
17 memset(dado, 0, sizeof(dado));
(gdb) print iIter
$2 = 5

En este caso si se imprime el valor actual de la variable iIter (5). Avancemos hasta la línea 20, y observemos que podemos utilizar print para imprimir el valor de retorno de una función:

(gdb) next
18 for(i=0; i<iIter; i++)
(gdb) next
20 dado[tirarDado() - 1]++;
(gdb) print tirarDado()
$3 = 2

Ahora observemos que al estar aún dentro del for, al terminar la línea 20 el gdb nos devuelve automáticamente hasta la línea 18 (que es donde se evalua la condición del for):

(gdb) next
18 for(i=0; i<iIter; i++)
(gdb) next
20 dado[tirarDado() - 1]++;

Ahora continuemos con la ejecución normal del programa y salgamos del gdb, para ello digitamos el comando cont y después q:

(gdb) cont
Continuing.
5 tiradas
Cara Tiradas
1 : 0
2 : 2
3 : 0
4 : 1
5 : 1
6 : 1
Program exited normally.
(gdb) q

Eso es todo!!! Fácil ¿no? para un manejo avanzado del gdb leer la documentación del programa localmente (comando man) o en Internet.

Sin comentarios | deja el tuyo

« Entradas anteriores
Entradas recientes »