Ipol:Modulo3

From Wiki
Jump to: navigation, search

Entornos de desarrollo

Un entorno de desarrollo integrado (IDE - Integrated Development Environment) es un programa informático compuesto por un conjunto de herramientas de programación. Hasta ahora, no hemos mencionado ningún entorno de desarrollo particular, simplemente hemos editado los archivos con simples editores de texto, y hemos compilado directamente desde una terminal de UNIX utilizando gcc.

Un entorno de desarrollo, es más que un editor de texto. Contiene:

  • Un editor de texto.
  • Un compilador.
  • Un intérprete.
  • Un depurador.
  • Un cliente.
  • Posibilidad de ofrecer un sistema de control de versiones (e.g. svn, git).
  • Factibilidad para ayuda en la construcción de interfaces gráficas de usuario (GUI).

Nota: se recomienda el uso del entorno de desarrollo NetBeans.

NetBeans

netbeans-logo-with-org.gif

NetBeans (http://www.netbeans.org/) es un entorno de desarrollo integrado libre, hecho principalmente para el lenguaje de programación Java. Existe además un número importante de módulos para extenderlo. NetBeans IDE es un producto libre y gratuito, sin restricciones de uso. Si bien este entorno se usa principalmente para Java, existe una versión del IDE para C/C++. Más información: http://netbeans.org/features/cpp/index.html.

Makefile

En el Módulo 1 se realiza una primera aproximación a makefile. Reutilizaremos ese ejemplo para ver más detalles de esta herramienta. Dado makefile utilizado:

quinto : quinto.o auxiliares.o
gcc -c auxiliares.c
gcc -c quinto.c
gcc auxiliares.o quinto.o -o quinto
  1. Escribir nuevamente las dependencias para que solamente se compile lo modificado desde la última compilación. (Solución en makefile2).
  2. Utilizar los comandos $@ y $^ para modificar el makefile anterior, de modo de evitar repetir el nombre de los archivos fuente. (Solución en makefile3).
  3. Utilizar la declaración de una variable que incluya todos los objetivos de las compilaciones sin enlazar (archivos .o), y carácteres especiales como $ y % para escribir de manera reducida el makefile. (Solución en makefile4).
Nota: para compilar utilizando un makefile con otro nombre, debe utilizarse la opción -f de make, por ejemplo:
> make -f makefile2
Archivos: makefile2 makefile3 makefile4.

Tutorial de makefile: http://www.gnu.org/software/make/manual/html_node/index.html.

Otro tutorial de makefile: http://iie.fing.edu.uy/~vagonbar/gcc-make/make.htm#ArchivosPrograma.

Adicional, un resumen de algunas herrmamientas de UNIX y C: http://iie.fing.edu.uy/ense/asign/str/curso-c/indice.htm.

Debugger

Ejemplo 1

Utilizaremos la herramienta ddd presentada en el Módulo 2. Se aplicará a tres ejemplos de código existentes.

  1. Para el primer ejemplo (primero.c), utilizar el ddd para ver como queda guardado en memoria una variable de tipo struct. Analizar también el puntero a un struct declarado en el programa.
    Código:
    #include <stdio.h>
    
    int main(){
    
        typedef struct {
            char *name;
            char sexo;
            int edad;
        } persona;
    
        persona p1;
        p1.name = "Juan";
        p1.sexo = 'M';
        p1.edad = 30;
        persona *pp1;
        pp1 = &p1;
    
        return 0;
    }
    • Usando ddd se pueden ver las variables asociadas al programa de la siguiente manera:
    dddgraph_primero.png

Ejemplo 2

  1. Para el segundo ejemplo (segundo.c), analizar como se guarda en memoria una lista encadenada. Ver la asignación de punteros en el ciclo for que imprime el valor de los elementos de la lista.
    Código:
    #include <stdio.h>
    
    struct elemento{
        int valor;
        struct elemento * next;
    };
    
    typedef struct elemento item;
    
    int main(){
        
        item item1, item2, item3;
        item1.next = &item2; item1.valor = 10;
        item2.next = &item3; item2.valor = 20;
        item3.next = &item1; item3.valor = 25;
        
        int i;
        item * actual = &item1;
        for ( i=0 ; i<10 ; i++){
            printf("Valor del elemento: %d.\n",actual->valor);
            actual = actual->next;
        }
    
        return 0;
    }
    • Usando ddd se pueden ver las variables asociadas al programa de la siguiente manera:
    dddgraph_segundo.png
    • También pueden ponerse breakpoints dentro del ciclo for e inspeccionar el valor de las variables en cada paso de dicho ciclo (se muestran las imágenes desde i=0 hasta i=5):
i=0
dddgraph_segundo-00.png
i=1
dddgraph_segundo-01.png
i=2
dddgraph_segundo-02.png
i=3
dddgraph_segundo-03.png
i=4
dddgraph_segundo-04.png
i=5
dddgraph_segundo-05.png

Nota: Recordar compilar los archivos utilizando la opción -g y luego ejecutar el ddd pasando como argumento el ejectuable generado. Por ejemplo:

> gcc -g primero.c -o primero
> ddd primero

Ejemplo 3

Considere el programa presentado en el ejemplo tercero.c que se muestra a continuación

persona* crear_persona(char* name, char sexo, int edad);

int main()
{
    /// Puntero a un struct
    persona *p1=crear_persona("Juan",'M',300);

	 //uso mediante puntero
	 int edad=p1->edad;
	 //printf("edad: %d\n",edad);

    return 0;
}

El programa crea un struct persona igual que en el ejemplo 2, pero con una función que devuelve un puntero al struct creado. Esto es un procedimiento estándar en C.

  1. Compilar el programa sin información de debug, y ejecutar. Se recomienda hacerlo desde el archivo tercero.c y no copiado desde el wiki.
  2. Descomentar la línea del printf. Deducir en qué línea ocurre el error y determinar la causa del mismo. Debería ser posible hacerse sin mirar la implementación de la función crear_persona()
  3. Utilizar el debugger para deducir la causa del error, en caso de no poder hacerlo de otra forma.

Referencias

Archivos: primero.c segundo.c.

Profiler de uso de memoria: Valgrind

Valgrind

Valgrind es un conjunto de herramientas libres que ayuda en la depuración de problemas de memoria y rendimiento de programas. La herramienta más usada es Memcheck. Memcheck introduce código de instrumentación en el programa a depurar, lo que le permite realizar un seguimiento del uso de la memoria y detectar los siguientes problemas:

  • Uso de memoria no inicializada.
  • Lectura/escritura de memoria que ha sido previamente liberada.
  • Lectura/escritura fuera de los límites de bloques de memoria dinámica.
  • Fugas de memoria.
  • Otros.

Valgrind incluye además otras herramientas como Cachegrind, que mide el rendimiento de la caché durante la ejecución, de acuerdo a sus características (capacidad, tamaño del bloque de datos, grado de asociatividad, etc.).

Ejemplo 1

  1. Un error común es intentar acceder a elementos de un array fuera de sus límites (ejemplo en primero.c).
    • Al ejecutar el programa se obtiene la siguiente salida:
    > ./primero 
    *** glibc detected *** ./primero: free(): invalid next size (fast): 0x0000000001edc010 ***
    primero: malloc.c:2451: sYSMALLOc: Assertion `(old_top == (((mbinptr) (((char *) &((av)->bins[((1) - 1) * 2])) - __builtin_offsetof (struct malloc_chunk, fd)))) && old_size == 0) || ((unsigned long) (old_size) >= (unsigned long)((((__builtin_offsetof (struct malloc_chunk, fd_nextsize))+((2 * (sizeof(size_t))) - 1)) & ~((2 * (sizeof(size_t))) - 1))) && ((old_top)->size & 0x1) && ((unsigned long)old_end & pagemask) == 0)' failed.
    Aborted (core dumped)
    • Normalmente uno esperaría alguna especie de queja del sistema operativo (si lo hay) por acceder a memoria no reservada. Esto ocurre siempre y cuando la posición accedida no este dentro de la memoria asignada a este programa. Sin embargo en este caso, el programa da un error relacionado con el free.
    • la solución intuititiva es eliminar el free(), sin embargo, estaríamos dejando de liberar la memoria que pedimos y eso es un problema. Esto es un típico caso de errores oscuros de tiempo de ejecución, donde el problema aparece en un lugar poco relacionado con la causa real del problema. Cambiar el programa y verificarlo.
    • Para comprender lo que ocurre hay que ver bien en detalle como funcionan el malloc y el free. Si observan, cuando uno hace malloc, solicita una cierta cantidad de bytes. Sin embargo cuando se hace free() no se especifica. La pregunta que surge entonces es: como sabé free cuantos bytes liberar?
    • Cuando se hace malloc de n bytes, se reservan esos bytes más una cierta cantidad de bytes que contienen información alternativa. Esta información entre otros contiene el tamaño almacenado. Normalmente esta información se almacena de forma contigua, antes o después de los datos, dependiendo del compilador y del sistema operativo.
    • lo que ocurre en este caso, es que la escritura fuera de lugar esta corrompiendo la información administrativa y por lo tanto el free no puede leerla correctamente.
    • Veamos como se ve este error en Valgrind:
    > gcc -g primero.c -o primero
    > valgrind ./primero
    ==5598== Memcheck, a memory error detector
    ==5598== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.
    ==5598== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
    ==5598== Command: ./primero
    ==5598== Invalid write of size 4 
    ==5598== at 0x8048434: main (primero.c:12) 
    ==5598== Address 0x41a5050 is 0 bytes after a block of size 40 alloc'd 
    ==5598== at 0x4025BD3: malloc (vg_replace_malloc.c:236) 
    ==5598== by 0x8048408: main (primero.c:7) 
    ==5598==
    ==5598==
    ==5598== HEAP SUMMARY:
    ==5598==     in use at exit: 0 bytes in 0 blocks
    ==5598==   total heap usage: 1 allocs, 1 frees, 40 bytes allocated
    ==5598== 
    ==5598== All heap blocks were freed -- no leaks are possible
    ==5598== 
    ==5598== For counts of detected and suppressed errors, rerun with: -v
    ==5598== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 11 from 6)
    • Invalid write of size 4 marca el error (se intentó escribir un int fuera del lugar que asignó malloc).
    • at 0x8048434: main (primero.c:12) marca que el error se cometió en la línea 12 de código.
    • También puede verse que se hizo un malloc y un free, con lo que no hay leaks.

Ejemplo 2

  1. Otro error común es olvidarse de inicializar una variable o un array antes de usarlo. Investigar el código implementado en segundo.c y utilizar Valgrind para encontrar los errores de no inicialización de variables.
    Nota: probar el funcionamiento de la opción --track-origins=yes de Valgrind. En ese caso se ejecutaría:
    > valgrind --track-origins=yes ./segundo

Ejemplo 3

  1. Valgrind también sirve para buscar memory leaks, o huecos de memoria que dejan los programas (memoria que se pide pero no se libera). Analizar entonces el código implementado en tercero.c, compilarlo, y utilizar Valgrind con la opción --leak-check=full para ver un informe del uso de memoria del programa. Encontrar los memory leaks.
    > valgrind --leak-check=full ./tercero
    ==3714== Memcheck, a memory error detector
    ==3714== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.
    ==3714== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
    ==3714== Command: ./tercero
    ==3714== 
    ==3714== 
    ==3714== HEAP SUMMARY:
    ==3714==     in use at exit: 400 bytes in 1 blocks
    ==3714==   total heap usage: 2 allocs, 1 frees, 800 bytes allocated
    ==3714== 
    ==3714== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
    ==3714==    at 0x4025BD3: malloc (vg_replace_malloc.c:236)
    ==3714==    by 0x8048418: main (tercero.c:7)
    ==3714== 
    ==3714== LEAK SUMMARY:
    ==3714==    definitely lost: 400 bytes in 1 blocks
    ==3714==    indirectly lost: 0 bytes in 0 blocks
    ==3714==      possibly lost: 0 bytes in 0 blocks
    ==3714==    still reachable: 0 bytes in 0 blocks
    ==3714==         suppressed: 0 bytes in 0 blocks
    ==3714== 
    ==3714== For counts of detected and suppressed errors, rerun with: -v
    ==3714== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 13 from 8)
    Nota: ver el HEAP SUMMARY.

Ejemplo 4

  1. Otro ejemplo un poco más complejo de memory leaks puede encontrarse en el ejemplo cuarto.c. Al igual que en el caso anterior, compilar este programa y utilizar Valgrind con la opción --leak-check=full para ver un informe del uso de memoria del programa. Encontrar los memory leaks.

Ejemplo 5

  1. Valgrind no sirve en casos de memoria estática, por ejemplo cuando se define un array de tamaño fijo: int a[10];. Para ver esto, compilar el archivo quinto.c, ejecutarlo, y encontrar el error que se produce. Luego usar Valgrind para analizar el funcionamiento del programa y ver que la herramienta no encuentra errores.

Referencias

Profiler de uso de CPU: Callgrind+KCachegrind

KCachegrind es una herramienta de visualización de los datos de profiling generados por Cachegrind y Calltree . Callgrind es parte de Valgrind (observe que antes se denominaba Calltree, pero el nombre podía inducir a error). Vamos a utilizar esta herramienta para estudiar el funcionamiento del FIR implementado en el Módulo 3.

  1. Para utilizar KCachegrind, primero se debe generar el archivo callgrind.out.pid utilizando la opción --tool=callgrind del comando valgrind:
    > valgrind --tool=callgrind ./test_fir
    ==7546== Callgrind, a call-graph generating cache profiler
    ==7546== Copyright (C) 2002-2010, and GNU GPL'd, by Josef Weidendorfer et al.
    ==7546== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
    ==7546== Command: ./test_fir
    ==7546== 
    ==7546== For interactive control, run 'callgrind_control -h'.
    Entrada: 
    0.000 0.000 0.000 0.000 0.000 1.000 1.000 1.000 1.000 1.000 
    Salida: 
    0.000 0.000 0.000 0.000 0.000 0.250 0.500 1.000 1.500 1.500 
    ==7546== 
    ==7546== Events    : Ir
    ==7546== Collected : 161006
    ==7546== 
    ==7546== I   refs:      161,006
  2. Esto genera el archivo callgrind.out.pid que se debe cargar en KCachegrind.
  3. Abrir el KCachegrind y cargar el archivo mencionado. Va a aparecer una ventana similar a la siguiente:
    kcachegrind1.png
  4. Estudiar las diferentes vistas del programa, como por ejemplo, la lista de funciones a las que se invoca durante la ejecución del programa, y el tiempo que estas consumen. Tambiién se puede ver un gráfico de la llamada de funciones durante la ejecución del programa en la pestaña Call Graph.
    Nota: En la lista de funciones invocadas (Flat Profile), si se utiliza la opción ELF Object para agrupar los objetos de la lista, podemos separar lo que son funciones de nuestro programa, de lo que son funciones del sistema operativo, y así ver solamente las funciones que se llaman dentro de nuestro programa. Ejemplo:
    kcachegrind2.png
Archivos: fir.c fir.h genera_senales.c genera_senales.h test_fir.c makefile callgrind.out.7546.

Página principal de KCachegrind: http://kcachegrind.sourceforge.net/html/Home.html.

Uso de KCachegrind: http://docs.kde.org/stable/es/kdesdk/kcachegrind/using-kcachegrind.html.

The KCachegrind Handbook: http://docs.kde.org/stable/en/kdesdk/kcachegrind/index.html.

Documentación

Doxygen es un generador de documentación para C y C++, entre otros lenguajes de programación. Para aplicar Doxygen utilizaremos primero un çódigo sencillo y luego el código del FIR implementado en el Módulo 3.

  • Generar el archivo de configuración de Doxygen, llamado Doxyfile. Este archivo contiene todas las instrucciones para generar la documentación del proyecto. Se genera utilizando el comando:
    > doxygen -g
  • Editar las variables necesarias en el archivo generado en el paso anterior. Algunas pueden ser:
    PROJECT_NAME           = "Filtro FIR en C"
    PROJECT_NUMBER         = 1.0
    PROJECT_BRIEF          = "Diseño de un filtro FIR en C."
    OUTPUT_DIRECTORY       = doc/
    OUTPUT_LANGUAGE        = Spanish
    OPTIMIZE_OUTPUT_FOR_C  = YES
    GENERATE_HTML          = YES
    GENERATE_LATEX         = YES
    HAVE_DOT               = YES
    CALL_GRAPH             = YES
  • Comentar el código y utilizar el comando doxygen para generar automáticamente la documentación del proyecto. Inspeccionar dicha documentación generada.
Ejemplo de documentación generada: http://iie.fing.edu.uy/~haldos/ipol/doxygen_example/index.html.

Documentación de Doxygen: http://www.stack.nl/~dimitri/doxygen/manual.html.

Comandos útiles de Doxygen: http://www.stack.nl/~dimitri/doxygen/commands.html.

Makefiles multiplataforma

CMake es una herramienta multiplataforma de generación o automatización de código. El nombre es una abreviatura para cross platform make (make multiplataforma). El proceso de construcción se controla creando uno o más ficheros CMakeLists.txt en cada directorio (incluyendo subdirectorios). Cada CMakeLists.txt consiste en uno o más comandos.

  1. Para el ejemplo utilizado en el ejercicio de makefile, generar el correspondiente archivo CMakeLists.txt y probar compilar el proyecto.
    Una vez creado el archivo CMakeLists.txt se deben correr dos comandos; cmake <directorio> y make:
    > cmake .
    -- The C compiler identification is GNU
    -- The CXX compiler identification is GNU
    -- Check for working C compiler: /usr/bin/gcc
    -- Check for working C compiler: /usr/bin/gcc -- works
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working CXX compiler: /usr/bin/c++
    -- Check for working CXX compiler: /usr/bin/c++ -- works
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /home/haldo/Documentos/Investigación/IPOL/ipol/curso_c/parte1
    > make
    Scanning dependencies of target quinto
    [ 50%] Building C object CMakeFiles/quinto.dir/quinto.c.o
    [100%] Building C object CMakeFiles/quinto.dir/auxiliares.c.o
    Linking C executable quinto
    [100%] Built target quinto
  2. Analizar el código que se implementa en los archivos a.c, b.c, c.c, libabc.h y demo.c. Generar un archivo CMakeLists.txt para compilar este programa utilizando cmake.
    1. Compilar utilizando cmake.
      Salida:
      > cmake .
      -- The C compiler identification is GNU
      -- The CXX compiler identification is GNU
      -- Check for working C compiler: /usr/bin/gcc
      -- Check for working C compiler: /usr/bin/gcc -- works
      -- Detecting C compiler ABI info
      -- Detecting C compiler ABI info - done
      -- Check for working CXX compiler: /usr/bin/c++
      -- Check for working CXX compiler: /usr/bin/c++ -- works
      -- Detecting CXX compiler ABI info
      -- Detecting CXX compiler ABI info - done
      -- Configuring done
      -- Generating done
      -- Build files have been written to: /home/haldo/Documentos/IPOL/C/ipol/examples/cmake/primer
      > make
      Scanning dependencies of target demo
      [ 25%] Building C object CMakeFiles/demo.dir/demo.c.o                                                         
      [ 50%] Building C object CMakeFiles/demo.dir/a.c.o                                                            
      [ 75%] Building C object CMakeFiles/demo.dir/b.c.o                                                            
      [100%] Building C object CMakeFiles/demo.dir/c.c.o                                                            
      Linking C executable demo                                                                                     
      [100%] Built target demo
    2. Cambiar la fecha de modificación (utilizando el comando touch) de alguno de los archivos (por ejemplo b.c), y volver a compilar utilizando cmake.
      Salida:
      > touch b.c 
      > cmake .
      -- Configuring done
      -- Generating done
      -- Build files have been written to: /home/haldo/Documentos/IPOL/C/ipol/examples/cmake/primer
      > make
      Scanning dependencies of target demo
      [ 25%] Building C object CMakeFiles/demo.dir/b.c.o                                                            
      Linking C executable demo                                                                                     
      [100%] Built target demo
      Nota: Observar que solamente se realiza la compilación de b.c, y luego el enlazado para obtener el ejecutable.
    Archivos: a.c b.c c.c libabc.h demo.c CMakeLists.txt.
  3. Nuevamente se utilizarán los archivos a.c, b.c, c.c, libabc.h y demo.c. Ahora mover los archivos a.c, b.c, c.c y libabc.h a un subdirectorio library y el archivo demo.c a otro subdirectorio demo. Generar entonces los tres archivos CMakeLists.txt necesarios (uno en el directorio raíz, y uno en cada subdirectorio creado). Compilar utilizando cmake.
    Nota: Considerar los comandos ADD_SUBDIRECTORY y ADD_LIBRARY de cmake.
    Salida:
    > cmake .
    -- The C compiler identification is GNU
    -- The CXX compiler identification is GNU
    -- Check for working C compiler: /usr/bin/gcc
    -- Check for working C compiler: /usr/bin/gcc -- works
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working CXX compiler: /usr/bin/c++
    -- Check for working CXX compiler: /usr/bin/c++ -- works
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /home/haldo/Documentos/IPOL/C/ipol/examples/cmake/segundo
    > make
    Scanning dependencies of target abc
    [ 25%] Building C object library/CMakeFiles/abc.dir/a.c.o                                                     
    [ 50%] Building C object library/CMakeFiles/abc.dir/b.c.o                                                     
    [ 75%] Building C object library/CMakeFiles/abc.dir/c.c.o                                                     
    Linking C shared library libabc.so                                                                            
    [ 75%] Built target abc                                                                                       
    Scanning dependencies of target demo
    [100%] Building C object demo/CMakeFiles/demo.dir/demo.c.o                                                    
    Linking C executable demo                                                                                     
    [100%] Built target demo
    Archivos:

Tutoriales

Otro compilador: clang

Clang es un compilador libre para C y C++, que intenta ser una alternativa de gcc. Fue desarrollado y patrocinado por Apple, y funciona bajo la licencia free software licence. Para instalar clang (tanto en sistemas UNIX como en Windows) ver http://clang.llvm.org/get_started.html.

Por ejemplo, para compilar el FIR creado en el Módulo 3 utilizando clang:

> clang -S fir.c 
> clang -S genera_senales.c 
> clang -S test_fir.c 
> clang fir.s genera_senales.s test_fir.s -o test_fir

Un ejemplo de makefile utilizando clang para el caso anterior sería:

#!/bin/bash

OBJ = fir.s genera_senales.s test_fir.s

test_fir : $(OBJ)
	clang $(OBJ) -o $@

%.s: %.c
	clang -S $^

clean: 
	rm -f $(OBJ) test_fir

Página principal de clang: http://clang.llvm.org/.

Creación de una biblioteca

referencias:

  1. http://crasseux.com/books/ctutorial/Building-a-library.html