Oct 16

TOOLing

Tag: admin @ 5:44 pm

 

 

Esta seccion esta dedicada a cada una de las herramientas que nos facilitan las tareas a los desarrolladores (especificamente en UNIX y derivados). Es decir, esta seccion NO estara destinada a hablar de ningun lenguaje, ni de GNU/Emacs, ni nada parecido. Pero esta seccion cubrira herramientas fundamentales como Sistemas de Control de Versiones, en concreto GIT. Tambien cubrira todo lo referente a herramientas de construccion de proyectos como GNU/Make, Automake, Autoconf y en general Autotools. Por ultimo cubrira secciones de profilers como GNU/Prof, Gconv y de debugging como GDB. Asi pues, y si tengo tiempo, intentare explicar conceptos fundamentales del shell adentrandome en sh y BASH, sed y awk, pero todo esto solo ira orientado como apoyo logistico para la creacion de scripts para proyectos. Es posible tambien que si tengo tiempo hable de las cabeceras ELF de UNIX y quiza las syscalls de este mismo sistema. Por ultimo, no podemos olvidar de la importancia de herramientas en coreutils como od, md5sum, sha1sum, sha2, dd o herramientas como cpio y ar. A fin de cuentas, todos estos conceptos son utiles si somos o queremos ser buenos desarrolladores en sistemas UNIX. En esta guia, por supuesto no podia faltar el uso fundamental de GCC.

0. GCC

GCC, el mejor compilador bajo mi punto de vista y lo que es mas importante, de GNU.

Como funciona el compilador

La secuencia es la siguiente:

  • Preprocesador: para expandir macros: $ cpp file.c >> file.i. El fichero resultante sera el codigo con las macros expandidas. La extension .i se empleara para ficheros C, mientras que la extension .ii se empleara para ficheros C++.
  • Compilacion: conversion de codigo fuente a lenguaje ensamblador: $ gcc -Wall -S file.i. Convertira el fichero preprocesado de codigo fuente en lenguaje ensamblador para dicho procesador especificamente. Generara el fichero file.s. Que puede tener llamadas (calls) a funciones externas.
  • Ensamblaje: conversion de lenguaje ensamblador a codigo maquina: $ as file.s -o file.o. Cuando hay llamadas a funciones externas, el ensamblador dejara dichas funciones como referencias indefinidas pero generara el resto como codigo objeto.
  • Linkaje: creacion del ejecutable final mediante el linkaje. Este proceso es realmente complejo en cuanto a sintaxis se refiere y seria tal que asi para un simple programa de hello world: $ ld -dynamic-linker  /lib/ld-linux.so.2 /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/lib/i686/3.3.1/crtbegin.o -L/usr/lib/gcc-lib/i686/3.3.1 hello.o -lgcc -lgcc_eh -lc -lgcc -lgcc_eh /usr/lib/gcc-lib/i686/3.3.1/crtend.o /usr/lib/crtn.o o de forma mucho mas abreviada y transparente mediante gcc hello.o. (crt significa “compiler run-time).

Aunque todos los pasos anteriores se pueden realizar en uno solo mediante el compilador GCC y asi ser transparente de cara al usuario.

GCC Basico

  • Compilando y linkando con un solo comando

Pero vayamos al grano, si tenemos un fichero simple podemos compilar mediante:

$ gcc main.c -o main

  • Compilando multiples ficheros con nuestras cabeceras propias (headers)

Si en cambio tenemos el proyecto separado en ficheros de la forma mas simple posible:

  • main.c donde se incluya un func.h y algunos otros .h.
  • func.c donde estara la implementacion de funciones y donde incluira func.h.
  • func.h donde estaran los prototipos y macros

$ gcc main.c func.c -o binario

  • Activando warnings

Para activar los warnings podemos indicar la opcion -Wall siempre antes de indicar los ficheros .c, ya que sino el compilador intentaria interpretarlo ya bien como ficheros fuente o bien el binario resultante:

$ gcc -Wall main.c func.c -o binario

  • Compilando sin enlazar (sin linkar – por defecto y aunque esto sea transparente al usuario, se linka con la libc)

Tambien podemos compilar los ficheros de forma independiente y sin linkarlos mediante la opcion -c, de esta forma podemos ganar cierto tiempo de compilacion en caso de que tengamos estas necesidades. Esto dara un output con formato .o:

$ gcc  -c main.c

  • Solo enlazando (linkando – por defecto y aunque esto sea transparente al usuario, se linka con la libc)

Podemos a partir de aqui crear de nuevo los ejecutables mediante la opcion -o sobre estos ficheros .o creados justo en el comando anterior (aqui introducimos un nuevo codigo objeto ya compilado previamente):

$ gcc main.o func.o -o hello

Cuando recompilemos varios ficheros mediante la opcion -c, se mirara el timestamp, en este caso solo se recompilaran los ficheros en el que su fichero .c sea posterior a su .o, ganando asi tiempo. Si creamos un Makefile (ver la sigiuente seccion) para ello, podremos ver como en distintos casos se reompilara algunas veces todos los ficheros .c y en otros casos no, dependiendo de si han sido modificados y compilados anteriormente.

  • Libraries/Bibliotecas (enlazando estaticamente)

Las bibliotecas externas (libraries – cuidado con el false friend, debemos hacer una analogia de una biblioteca (library o lib), que contiene muchos libros (libreria) y que cada libro en sus hojas tendria sus funciones e implemenaciones que serian las cabeceras) son guardadas en un fichero especial con extension .a, referiendose a librerias estaticas. Estas librerias estaticas son creadas mediante la herramienta GNU archiver ( ar ) y es usada por el enlazador para resolver las referencias a funciones en tiempo real.

Las bibliotecas del sistema estandar se encuentran generalmente en los directorios /usr/lib y /lib, aunque para sistemas que soportan 32 y 64 bits indistintamente se suele crear  /usr/lib64 y /lib64 para 64 bits y /usr/lib y /lib para 32 bits.

Por ejemplo la biblioteca matematica suele estar en /usr/lib/libm.a, sin embargo los prototipos se encuentran en /usr/include/math.h. La biblioteca estandar C se encuentra en /usr/lib/libc.a y contiene las funciones del estandar ANSI/ISO C (recordemos que por defecto, esta libreria siempre es enlazada o linkada). Si empleasemos la funcion sqrt() debemos no solo incluir el header math.h sino tambien linkar libmath, mediante el siguiente comando:

$ gcc -Wall calc.c /usr/lib/libm.a -o calc

o bien gcc permite la opcion -l para linkar con librerias como shortcut (esta opcion tiene el formato -lnombre y buscara en las rutas mencionadas anteriormente con la extension .a):

$ gcc -Wall calc.c -lm -o calc

Recordemos que el orden debe ser ese, ya que este codigo y tambien la biblioteca matematica son dependencias de calc (el orden debe ser fuente.c dependencia_para_fuente.c y no al reves), y en ningun caso podriamos poner -lm antes de calc.c. En casos como la biblioteca para Programacion Linear de GNU llamada libglpk.a la cual emplea la biblioteca matematica el orden seria algo asi:

$ gcc -Wall data.c -lglpk -lm

La importancia de usar la opcion -Wall reside en que, si por alguna de aquellas se nos olvidase incluir math.h empleando la funcion pow() por ejemplo, lo que hariamos seria estar llamando a esta misma ya que las funciones serian encontradas (ya que si las habriamos linkado) pero sin tener en cuentra los prototipos de la cabecera, por ello el resultado de esta operacion seria totalmente incorrecto tomando los argumentos de un tipo invalido dependiendo de como se promocionasen en dicha arquitectura (de double a float, etc).

Opciones de compilacion

  • PATHs

GCC buscara por defecto para ficheros de cabecera (headers) en:

    • /usr/local/include/
    • /usr/include/
      • (cuando falte uno de estos ficheros o no se encuentre en este PATH el error sera del tipo fichero.h: No such file or directory )

Y para bibliotecas (libs) en:

    • /usr/local/lib/
    • /usr/lib/
      • (cuando falte uno de estos ficheros o no se encuentre en este PATH el error sera del tipo /usr/bin/ld: cannot find library)

Es posible que instalemos bibliotecas externas al sistema y que debamos extender los directorios donde estas se puedan encontrar para que posteriormente GCC pueda encontrarlas:

    • -I para anadir un PATH mas de los ficheros de cabeceras (includes o headers)
    • -L para anadir un PATH mas de las bibliotecas (lib o lib*.a)

Generalmente, las variables de entorno deben ser asignadas (llamado en jerga angloespanola como setteadas) en un lugar apropiado como el .bash_profile, en c de la siguiente forma:

C_INCLUDE_PATH=/opt/gdbm-1.8.3/include
export C_INCLUDE_PATH

Y en c++ de la siguiente forma:

CPLUS_INCLUDE_PATH=/opt/gdbm-1.8.3/include
export CPLUS_INCLUDE_PATH

El comando export como bien sabeis es necesario para que la variable este disponible fuera de esta propia shell. De la misma forma, podemos anadir el PATH para las respectivas librerias:

LIBRARY_PATH=/opt/gdbm-1.8.3/lib
export LIBRARY_PATH

Las busquedas se realizaran en el siguiente orden:

  • Primero se buscaran las bibliotecas y headers indicadas mediante linea de comando con las opciones -L y -I de izquierda a derecha y demas.
  • Despues se buscaran las bibliotecas y headers indicadas en las variables de entorno C_INCLUDE_PATH  (para c), CPLUS_INCLUDE_PATH (para c++) y LIBRARY_PATH.
  • Finlamente se buscaran en la lista de PATHs por defecto que mencionabamos antes.

Es posible anadir multiples directorios separandolo por los dos puntos : (notese que tambien se puede especificar el directorio actual senalandolo como un punto):

C_INCLUDE_PATH=.:/opt/gdbm-1.8.3/include:/net/include
LIBRARY_PATH=.:/opt/gdbm-1.8.3/lib:/net/lib

O si lo quisieramos para c++:

CPLUS_INCLUDE_PATH=.:/opt/gdbm-1.8.3/include:/net/include
LIBRARY_PATH=.:/opt/gdbm-1.8.3/lib:/net/lib

Desde linea de comandos tambien podriamos indicarlo varias veces con el parametro -I por ejemplo:

$ gcc -I. -I/opt/gdbm-1.8.3/include -I/net/include -L. -L/opt/gdbm-1.8.3/lib -L/net/lib …

  • Dynamic linking

Aunque los programas se hayan compilado y linkado correctamente, no significa que la ejecucion de estos puedan ser siempre exitosas, mas en los casos donde paquetes como GDBM requieren un tratamiento especial debido a que seran cargadas justo antes de que el ejecutable sea arrancado. A diferencia de las bibliotecas estaticas (.a) donde se copiaba el codigo maquina de los codigo objetos de la libreria al ejecutable final. En cambio, en las dinamicas (.so) donde se consigue que el ejecutable final sea mas pequeno, se emplea un tipo de enlace mas avanzado.

Cuando se enlazan bibliotecas dinamicas realmente en algun lugar de la memoria existe una tabla de las funciones requeridas, en lugar del codigo maquina completo. Antes de que el binario comience a ejecutarse, el codigo maquina de dichas funciones es copiado en memoria desde el fichero de la biblioteca dinamica. A este proceso se le llama dynamic linking. De esta forma se conseguiran ejecutables mas pequenos y librerias compartidas por distintos ejecutables, alguno OS ademas tienen un mecanismo virtual de memoria para permitir una copiia de la biblioteca compartida en memoria fisica para reducir no solo el espacio del binario, sino tambien el uso de memoria en el disco.
Para este tipo de bibliotecas tenemos tambien una variable para indicarle el PATH, de la siguiente manera:

LD_LIBRARY_PATH=/opt/gdbm-1.8.3/lib
export LD_LIBRARY_PATH

Si el PATH anterior pudiese tener otras entradas podriamos crear algo asi:

LD_LIBRARY_PATH=/opt/gdbm-1.8.3/lib:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH

Si queremos que esto sea global a todos los usuarios tambien podemos modificar el fichero /etc/profile (que sera ejecutado para cualquier usuario al iniciar la sesion) o especificamente para este caso de bibliotecas dinamicas en /etc/ld.so.conf.

En muchos casos nos encontraremos con que algunos de estos directorios contienen tanto ficheros .a como .so, es decir, que podremos linkar estatica o dinamicamente estas librerias. Podemos forzar para que se enlace de forma estatica mediante la opcion -static de GCC:

$ gcc -Wall -static  -I/opt/gdbm-1.8.3/include/ -L/opt/gdbm-1.8.3/lib/ dbmain.c -lgdbm

Esto creara un ejecutable enlazado con la biblioteca estatic libgdbm.a y ya podremos ejecutar nuestro programa con ./a.out.

Como podemos indicar directamente la dependencia podriamos indicar el fichero .a o .so directamente dependiendo de si queremos linkarlo de forma estatica o dinamica y cualquier biblioteca en particular.

$ gcc -Wall -I/opt/gdbm-1.8.3/include dbmain.c /opt/gdbm-1.8.3/lib/libgdbm.a (linkariamos de forma estatica)
$ gcc -Wall -I/opt/gdbm-1.8.3/include dbmain.c /opt/gdbm-1.8.3/lib/libgdbm.so (linkariamos de forma dinamica)
  • ANSI/ISO C

GCC es un compilador tan potente que tiene sus propias extensiones. Sin embargo es posible querer realizar un codigo que cumpla el estandar C (esto no siempre es posible debido a que hay implementaciones dependientes del OS – como el manejo de consola entre otras). Se pueden usar las siguientes opciones para obligar a GCC a que muestre errores de compilacion o muestre warnings en caso de no cumplir con el estandar, ya sea por el mal uso que se le ha dado al lenguaje o por el uso de extensiones:

    • -ansi : deshabilitara las extensiones GCC a la hora de compilar. Por ejemplo el uso de una variable llamada ‘asm’ no compilara por defecto ya que forma parte de un keyword en GNU/GCC extensions, sin embargo, con la opcion -ansi si compilara. (Esto mismo es aplicable para las keywords asm, inline, typeof, unix y vax. Se obtiene el efecto contrario con keywords como M_PI,  ya que no son parte del ANSI y si de GNU/GCC extensions).
    • -D : De esta forma podemos habilitar y deshabilitar extensiones GCC por separado, con la opcion -D.
      • -D_GNU_SOURCE :  habilita solo las extensiones propias de la biblioteca GNU C.
      • -D_POSIX_C_SOURCE : habilita extensiones POSIX.
      • -D_BSD_SOURCE : habilita extensiones BSD.
      • -D_SVID_SOURCE : habilita extensiones SVID (System V Interface Definition)
      • -D_OPEN_SOURCE :  habilita extensiones XOPEN.
    • -pedantic : en combinacion con -ansi, rechazara cualquier tipo de extension de GCC, no solo las incompatibles con el standard C. Esto hara que el programa sea portable. Por ejemplo un programa declarando int n = argc; double x[n]; compilaria con la opcion -ansi, pero no anadiendo la opcion -pedantic.
    • -std=c89 o -std=iso9899:1990 : Se restringe la compilacion al estandar ANSI/ISO C (ANSI X3.159-1989 ISO/IEC 9899:1990 con un par de correciones sobre el estandar original).
    • -std=iso9899:199409 : Enmienda ISO 1 publicada en 1994, tomada para internacionalizacion como el soporte multibyte para caracteres en la biblioteca C.
    • -std=c99 o -std=iso9899:1999 : Revision ISO/C publicada en 1999 (ISO/IEC 9899:1999).
    • -std=gnu89 : Se seleccionan las extensiones GNU/GCC89.
    • -std=gnu99 : Se seleccionan las extensiones GNU/GCC99.
  • Warnings

Es recomendable usar siempre la opcion -Wall ya que en muchos casos, nos permitira encontrar posibles fallos a nivel de programacion en nuestro codigo, que aunque puedan cumplir el estandar, en ciertos casos se pueden obtener errores en tiempo de ejecucion (estos son mucho mas complejos de detectar, y este es el primer y gran filtro para hacerlo):

    • -Wcomment : Warnings para comentarios anidados. La solucion elegante a los comentarios anidados se emplear directivas del preprocesador #if 0 … #endif
    • -Wformat : Warnings en caso de que se empleen funciones con cadena de formato y se empleen de forma incorrecta segun sus argumentos, tipico con funciones como printf() o scanf()
    • -Wunused : Warnings para variables no usadas.
    • -Wimplicit : Warnings para funciones usadas sin previamente haber sido declaradas, esto ocurre tipicamente cuando nos olvidamos de algun #include
    • -Wreturn-type : Warnings para funciones que no devuelven nada y no han sido declaradas como void o funciones con return prematuros o sin ningun valor.
    • -Wall :incluye -Wcomment, -Wformat, -Wunused, -Wimplicit y -Wreturn-type (las anteriormente comentadas).
    • W : Similar a -Wall pero que se centra en las posibles malas costumbres de programacion como -Wreturn-type o comparacion entre valores signed y unsigned. E.g.: if( x < 0) return 0; else return 1; Si x es unsigned, nunca ocurrira.
    • -Wconversion : Warnings para conversiones de tipo implicitas que puedan causar resultados inesperados. Por ejemplo conversiones entre float e int, entre signed y unsigned, entre distintos tamanos como long y short en enteros. Estas conversiones pueden ocurrir en expresiones, asignaciones o llamadas a funciones si no coinciden con el prototipo.
    • -Wshadow : Warnings si una variable ha sido redeclarada, lo que causa confusion entre el valor de la variable en ese preciso momento y el valor esperado.
    • -Wcast-qual : Warnings si algun puntero hace cast y elimina el cualificador de tipo como por ejemplo const.
    • -Wwrite-strings : Warning si alguna constante de cadena se intenta reescribir (el estandar ANSI/ISO C no define nada respecto a esto).
    • -Wtraditional : Warning si algunas partes del programa pueden ser interpretadas de manera distinta segun el estandar ANSI/ISO C y algun compilador pre-ANSI.
    • -Werror: Convierte los warnings en errores de compilacion.
    • -Wuninitialized : Warning si alguna variable no ha sido inicializada a lo largo de una funcion.

El preprocesador

cpp es el preprocesador  de GNU C y que forma parte del paquete GCC. Fundamentalmente su trabajo es el de expandir macros antes de que el codigo sea compilado.

    • -DTEST : define una macro llamada TEST desde la linea de comandos, es el equivalente en codigo a #define TEST. Por defecto tomara el valor de 1.
    • -DTEST=”” : define una macro que no tomara ningun valor por defecto.
    • -DNUM=100 : define una macro con un valor.
    • -DNUM=”2+2″ : define una macro con un valor. Las operaciones de cualquier tipo jamas seran realizadas por el preprocesador (ya que este solo realiza sustituciones) y se insertara como tal. Notese que en el codigo fuente es ideal poner las macros entre parentesis en caso de que puedan obtener valores asi, de esta forma siempre podremos respetar la prioridad de operadores para dentro de la macro cuando esta sea expandida y que no interfiera en la parte de la expresion fuera de la macro.
    • -DMESSAGE='”Hello world!”‘ : define una macro con comillas incluidas, para ello deberemos incrustarlas dentro de comillas simples.

Algunas de estas macros son definidas por el compilador, generalmente estas comienzan por doble guion abajo _ _. Para mostrar todas las macros definidas por el compilador podemos ejecutar el siguiente comando (para ello nos sirve un fichero vacio como /dev/null):

$ cpp -dM /dev/null

Obviamente estas macros seran deshabilitadas con la opcion -ansi.

Es posible llamar directamente al preprocesador y ver la expansion de la siguiente forma, de la siguiente forma:

$ gcc -E test.c 

El formato de la salida sera indicando algo asi:

# numero_de_linea "fichero-fuente"

De esta forma sera mucho mas facil poder entender la expansion que se ha producido y en que partes ha afectado. Como el output suele ser enorme, es posible usar la opcion -save-temps para redirigir la salida de manera mas apropiada que con >:

$ gcc -c -save-temps fichero.c

Se guardara la siguiente informacion:

    • fichero.i : la salida del preprocesado.
    • fichero.s : los ficheros ensambladores.
    • fichero.o : ficheros objeto.

Compilacion para debugging

Como los ejecutables no contienen ni el numero de linea, ni referencias al codigo original, o nombres de variables o funciones, sino que mas bien es una secuencia de codigo maquina producidas por el compilador, es realmente dificil depurar programas. Para ello tenemos la opcion -g de gdb, toda esta informacion es guardada en una tabla de simbolos dentro de los ficheros objetos y ejecutables.

Cuando un programa crashee, esto generara un fichero core dump. Este fichero contendra el estado de la memoria del programa cuando el crash ocurrio. Aqui tenemos un codigo que en la mayoria de sistemas crasheara:

int foo (int *p);
int
main (void)
{
int *p = 0; /* NULL pointer */
return foo(p);
}
int
foo (int *p)
{
int y = *p;
return y;
}

La ejecucion de este programa, causara un fichero llamado core en el directorio actual. El error que habra dado sera un segmentation fault, el cual se refiere a que se ha intentado acceder a un segmento de memoria fuera de la reservada para ello. Si el fichero core no fuese creado.

    • ulimit -c : muestra el tamano maximo del fichero core en caso de crearse. Si el limite esta establecido en 0, significa que no se crearan por  defecto estos ficheros.
    • ulimit -c unlimited : establece que el fichero core no tendra limite de tamano (en kilobytes). Esto sera setteado para la shell actual, recordemos la importancia de usar el .bash_profile.
    • ulimit -c numerodekbytes : establece el tamano del core a numerodekbytes.
    • gdb a.out : GDB cargara el ejecutable y la tabla de simbolos para depurar donde sea deseado.
    • gdb a.out core : GDB cargara el ejecutable junto con el core cargando la tabla de simbolos y mostrando cierta informacion lista para ser debuggeada.

El uso de GDB sera explicado en secciones posteriores pero de momento es interesante saber las funciones basicas:

    • print variable : imprime el valor de una variable.
    • backtrace : muestra el stack trace. Pudiendose mover entre llamadas con up y down y viendo los valores en cada uno de estos lugares.
    • break main : anade un breakpoint. (Esto generalmente es cuando cargamos el binario sin el coredump).
    • run : arranca el programa desde GDB. (Esto generalmente es cuando cargamos el binario sin el coredump).
    • step : ejecuta la siguiente instruccion del programa (entrando en las funciones).
    • next : ejecuta la siguiente instruccion del programa (sin entrar en las funciones).
    • set variable p = malloc(sizeof(int)) : asigna un valor distinto en tiempo de ejecucion a una variable.
    • finnish : continua la ejecucion del programa hasta el final de la funcion actual.
    • continue : continua la ejecucion del programa hasta el final del programa.

Compilando con optimizaciones

GCC permite optimizacion para incrementar los tiempos de ejecucion y reducir el tamano de los ficheros. Este proceso es realmente complejo dependiendo del set de instrucciones de cada procesador y de la forma en la que un compilador procese y convierta el codigo fuente en instrucciones de codigo maquina, los registros que se usen con resultados intermedios de calculos o guardando y extrayendo resultados de memoria cada vez. El orden de las expresiones cuando se escribe codigo, tambien influye para aplicar las tecnicas de optimizacion a nivel de codigo (ya que existen multiples niveles de optimizacion).

  • Optimizacion del usuario a nivel de codigo
    • Eliminacion de subexpresiones comunes

Para ello debemos entender un minimo las expresiones y evitar que estas sean reevaluadas. Si tenemos:

x = cos(v)*(1+sin(u/2)) + sin(w)*(1-sin(u/2))

Puede ser reescrito empleando la variable temporal t y eliminar una evaluacion extra innecesaria:

t = si(u/2)
x = cos(v)*(1+t) + sin(w)*(1-t)

A esta tecnica se le llama Common Subexpression Elimination (CSE) o simplemente Eliminacion de Subexpresiones Comunes. El compilador las realizara automaticamente si las optimizaciones son activadas. Son realmente importantes, porque permiten reducir el tamano del codigo al mismo tiempo que es mas rapido el tiempo de ejecucion.

    • Alineamiento de funciones (function inlining)

Esta tecnica incrementa la eficiencia de aquellas funciones llamadas de manera frecuente. Esto es debido ya que para cada funcion la CPU debe guardar valores (apilar) como los argumentos a la nueva llamada y valor de retorno, todo esto en zonas de memoria y registros,  comenzar a ejecutar el codigo en esta nueva funcion proveyendo de las apropiadas paginas de memoria virtual en la memoria fisica o la cache de la CPU si fuese necesario, y por supuesto, teniendo que volver al punto original de la llamada para continuar con la ejecucion despues de esta llamada. A este problema se le conoce como function-call overhead.

La optimizacion mediante function inlining evita toda esta sobrecarga, reemplazando la llamada por el codigo de la funcion en si mismo. Suele ser util cuando las funciones tienen pocas lineas y son llamadas multiples veces. Sin embargo, generalmente es posible reemplazar las funciones largas en funciones mas pequenas en muchos casos, siendo mas optimo. Un claro ejemplo seria el siguiente:

double
sq (double x)
{
return x * x;
}

Como la funcion es tan pequena, carece de sentido hacer una llamada a ella pudiendo hacerla inline. Cuando la funcion tenga una sola linea, siempre sera optimo ponerla como inline. Si llamasemos a esta funcion en un for, podriamos ver la diferencia claramente.

  • Optimizacion del compilador a nivel de codigo: Ventajas e inconvenientes de la velocidad en el espacio (speed-space tradeoffs)
    • Bucle desenrollado (Loop unrolling)

Incrementa la velocidad de los bucles mediante la eliminacion de “end of loop” en cada interaccion. Por ejemplo si un for va a ser ejecutado 8 veces, es posible ignorar la condicion del for de la siguiente forma:

for (i = 0; i < 8; i++)
{
y[i] = i;
}

Sera mucho mas rapido esto:

y[0] = 0;
y[1] = 1;
y[2] = 2;
y[3] = 3;
y[4] = 4;
y[5] = 5;
y[6] = 6;
y[7] = 7;

Por supuesto, el tamano del ejecutable aumentara, sin embargo la ejecucion sera mucho mas rapida. Esto tambien es posible y aplicable cuando la condicion del for no sea conocida aunque se emplean tecnicas mas complejas empleando el uso de modulos y con multiples loops.

  • Optimizacion del compilador a nivel de instrucciones: Planificacion (Scheduling)

Este nivel de optimizacion es el mas bajo, y es donde el compilador decide que instrucciones son mas apropiadas para la CPU y en que orden. Esto suele requerir de mas memoria cuando se realiza este proceso debido a su complejidad.

    • Niveles de optimizacion

Dependiendo del nivel de optimizacion se empleara mas o menos memoria..

      • -O0 (opcion por defecto): No se realiza ninguna optimizacion mas que convertir cada linea de codigo en sus correspondientes instrucciones, ideal cuando se quiere depurar un programa.
      • -O1 o -O : Activa las optimizaciones sin requerir de optimizaciones de speed-space tradeoffs ni scheduling. Los ejecutables seran mas pequenos y rapidos.
      • -O2 : Activa las optimizaciones rapidas, esto incluye las mismas que con -O1 y ademas de scheduling, pero no de speed space trade-offs. Por lo que el ejecutable no deberia aumentar de tamano.
      • -O3 : Activa las optimizaciones de -O1 y -O2, ademas de las inline y speed space trade-offs. Incrementara la velocidad del ejecutable, pero tambien su tamano.
      • -funroll-loops : Activa las optimizaciones para loop-unrolling. Incrementara el tamano del ejecutable final.
      • -Os :  Reducira el tamano del ejecutable final. En algunos casos puede ejecutarse mas rapido debido al mejor uso de la cache, pero basicamente prioriza el tamano reducido del fichero.

Con el comando time, podemos ver el tiempo que tarda en ejecutarse cada aplicacion:

$ time ./a.out

El valor de las columnas seran los siguientes:

    • real : total tiempo real necesitado para ejecutar el proceso.
    • user : tiempo que la CPU empleo para ejecutar el proceso.
    • sys : tiempo que sistema empleo teinendo cuenta otros procesos del sistema, tiempos de espera entre procesos, etc.

Existen casos donde compilar con optimizaciones (mas especificamente -O2) o con la opcion -Wuninitialized :

int
sign (int x)
{
int s;
if (x > 0)
s = 1;
else if (x < 0)
s = -1;
return s;
}

Este programa tiene un error y con las optimizaciones mencionadas anteriormente, podremos detectar cuando s no es inicializado, eso ocurrira cuando x valga 0. Gracias a una de estas 2 optimizaciones, podremos ver este bug en nuestro programa.

Compilando para c++

Las opciones descritas anteriormente son compatibles con g++ (el compilador de c++). Debemos siempre usar g++ para compilar programas c++ (que llevaran extension .C, .cc, .cpp, .cxx) ya que en caso contrario es posible que podamos compilar pero que al intentar linkar bibliotecas es muy probable que obtengamos undefined reference debido a que no encuentre la biblioteca correcta.

Ademas g++ no traduce a c los programas c++ y luego los compila como hacen otros compiladores.

Por supuesto existen opciones especificas en g++ que no se encuentran en gcc, ya que algunas son dependientes del lenguaje. Por ejemplo la opcion -ansi tendra en cuenta seguir el estandar c++  en lugar del estandar c.

    • -Wall o -W: incluye extra warnings especificos para c++ (para funciones miembro y clases virtuales).
    • -fno-default-inline : deshabilita por defecto el inline para las funciones miembro. Por defecto g++ intenta realizar dicha optimizacion incluso cuando no se ha puesto dicha palabra reservada. Esto es importante debido a que no es posible poner breakpoints en funciones inline.
    • -Weffc++ : informa sobre guidelines rotas en c++ siguiendo “Effective c++” y “More effective c++” de Scott Meyers. La libreria estandar de c++ no cumple estas guidelines, por lo que se suele usar en momentos de testing del propio codigo.
    • -Wold-style-cast :  resalta cualquier uso de castings en c++ como static_cast, dynamic_cast, reinterpreted_cast o const_cast.
  • Templates

Mediante templates o plantillas se provee la habilidad de definir clase en c++ para soporte generico en tecnicas de programacion. libstdc++ provee de un amplio contenedor de estas clases que anteriormente se encontraban en la STL pero que ahora forma parte la biblioteca estandar de c++.

Se pueden crear templates propios y lo ideal para ello es seguir el modelo de compilacion de inclusion; donde la definicion de los templates estaran en los ficheros de cabeceras. Los ficheros de cabeceras pueden ser incluidos usando la directiva #include.

#ifndef BUFFER_H
#define BUFFER_H
template <class T>
class buffer
{
public:
Buffer (unsigned int n);
void insert (const T & x);
T get (unsigned int k) const;
private:
unsigned int i;
unsigned int size;
T *pT;
};
template <class T>
Buffer<T>::Buffer (unsigned int n)
{
i = 0;
size = n;
pT = new T[n];
};
template <class T>
void
Buffer<T>::insert (const T & x)
{
i = (i + 1) % size;
pT[i] = x;
};
template <class T>
T
Buffer<T>::get (unsigned int k) const
{
return pT[(i + (size -k)) % size];
};
#endif /* BUFFER_H */

GNU Linker tendra en cuenta si existen simbolos duplicados, en lugar de como hacen otros linkers que devuelven “multiply defined symbol”.  Es posible forzar la compilacion usando la opcion -fno-implicit-templates cuando se empleen linkers distintos a GNU Linker, ya que en muchos casos puede interesarnos compilar los templates por un lado pero no linkarlos hasta tiempo mas tarde. Con esta opcion nos aseguramos que para cada template aparecera solo un fichero objeto y sera compatible con linkers que no puedan eliminar definiciones duplicadas en ficheros objeto. Un ejemplo de este uso seria:

$ g++ -Wall -fno-implicit-templates -c tprog.cc
$ g++ -Wall -fno-implicit-templates -c templates.cc
$ g++ tprog.o templates.o
$ ./a.out

De esta forma no habra codigo objeto para las funciones de template en tprog.o, pero si en templates.o. Pudiendo modificar asi o anadiendo nuevos templates en templates.cc. Sin embargo esto es una desventaja en proyectos grandes, ya que puede ser dificil saber que templates se deben usar o no en cada source.

Otras opciones del compilador

  • Opciones especificas para plataformas

Con la opcion -march=CPU podemos especificar el tipo de CPU que usamos, aunque de esta forma la compilacion no sera compatible con ninguna otra familia de procesadores, de esta forma el codigo se traducira a instrucciones optimizadas para el propio micro. Una alternativa es usar la opcion -mcpu=CPU o -mtune=CPU, que provee un compromiso entre velocidad y portabilidad; generando codigo para un micro especifico en terminos de scheduling, pero no usara instrucciones no disponibles en otras CPU de la familia x86.

Existen distintas extensiones para x86:

    • -mmmx : activa extensiones MMX
    • -msse : activa extensiones SSE (solo permite operaciones de precision simple).
    • -msse2 : activa extensiones SSE2 (permite operaciones de precision doble).
    • -msse3 : activa extensiones SSE3
    • -m3dnow : activa extensiones 3DNOW
    • -mfpmath=sse : activa extensiones SSE para operaciones de punto flotante donde sea posible. (se requiere que se active tambien -msse y -msse2)

Modelo de memoria (para micros de 64 bits):

    • -mcmodel=small : permite hasta 2 Gb para codigo y data.
    • -mcmodel=medium : permite ilimitada memoria para data.
    • -mcmodel=large : permite ilimitada memoria para data y codigo.
    • -mcmodel=kernel : se provee para codigo a nivel de sistema, como el kernel Linux.
    • -mno-red-zone : Para micros AMD64 (que se debe combinar con -mcmodel=kernel) ya que poseen a 128 bytes para el area de memoria reservados por debajo del puntero de pila para data temporal llamada “red-zone”.

Algunas plataformas pueden ejecutar codigo para una o mas arquitecturas por ejemplo, plataformas de 64 bits como AMD64, MIPS64, Sparc64 o PowerPC64 soportan codigo de 32 y 64 bits. Por defecto en estas plataformas se generara codigo de 64 bits, pero es posible forzarlo a 32 bits con la opcion -m32.

Ademas de esto debemos tener en cuenta que las bibliotecas esten disponibles. Las bibliotecas de 64 bits suelen estar en /usr/lib64 y /lib64. Las de 32 bits suelen estarlo en /usr/lib o /lib.

  • Problemas en operaciones de punto flotante

El estandar IEEE754 define a nivel de bit el comportamiento en operaciones aritmenticas de punto flotante para todos los procesadores modernos. Sin embargo, existen ciertos problemas. Por ejemplo la unidad de punto flotante x87 (FPU) en procesadores x86 computa los resultados empleando de forma interna precision extendida (los valores son convertidos a precision doble solo cuando estos son almacenados en memoria). La mayoria del resto de procesadores importantes como SPARC, PA-RISC, Alpha, MIPS y PPC trabajan de forma nativa con valores de doble precision. Por lo que las comparaciones que envuelvan valores de precision extendida fallaran cuando se comparen con los de precision doble. Internamente x87 FPU ofrece precision doble a nivel de hardware para sus registros. Para activar esta doble precision a nivel de redondeo debemos enviar la directiva al micro fldcw (floating-point load control word) con el valor 0x27F. Podemos hacerlo de la siguiente forma:

Y podemos realizar este codigo de prueba:

#include <stdio.h>
void
set_fpu (unsigned int mode)
{
asm (“fldcw %o” : : “m” (*&mode));
}
int
main(void)
{
double a = 3.0, b = 7.0, c;
#ifdef DOUBLE
set_fpu(0x27F); /* use double precision rounding */
#endif
c = a / b;
if ( c == a / b ){
printf(“comparison succeeds\n”);
} else {
printf(“unexpected result\n”);
}
return 0;
}

Compilando el codigo con: gcc -Wall -DDOUBLE file.c conseguimos el redondeo a nivel de hardware con doble precision y por lo tanto un resultado esperado.

El fldcw afecta a todo el entorno en general del proceso, incluyendo funciones de lib C, etc, tomando operaciones en doble precision en lugar de precision extendida. Afectando asi al comportamiento general de la FPU x87. Teniendo esto en cuenta, las instrucciones SSE y SSE2  seran siempre convertidas a precision doble de forma nativa. De esta forma empleando la forma gcc -Wall -msse2 -mfpmath=sse sera suficiente para eliminar los efectos de la precision extendida.

  • Portabilidad en tipos signed y unsigned

En C y C++ es posible tener chars de tipo signed o unsigned dependiendo de la plataforma y del compilador ,algunos de ellos por defecto usan signed y otros unsigned. Esto puede ser un problema a la hora de hacer codigo portable. Si por ejemplo hacemos un codigo tal que asi:

#include <stdio.h>
int
main (void)
{
char c = 255;
if (c > 128) {
printf(“char is unsigned (c = %d)\n”, c);
} else{
printf(“char is signed (c = %d)\n”, c);
}
return 0
}

Debemos tener especial cuidado con esto debido a que muchas veces, podemos tener el error comun de realizar un codigo tal que asi:

#include <stdio.h>
int
main (void)
{
char c;
while (( c = getchar() ) != EOF) /* not portable */
{
printf(“read c = ‘%c’\n”, c);
}
return 0;
}

Pudiendo leer el caracter con valor 255 o ÿ, que en caso de ser signed char tomaria el valor -1 (1 con el bit de signo cambiado). Este valor es especial y sirve para indicar EOF (EOF esta definido como -1). Por eso, debemos en lugar de emplear un char, un int o bien pasar como argumento a gcc algo asi: gcc -Wall -funsigned-char file.c. Otras opciones importantes a tener en cuenta referente a esto y mas especificamente a bitfields son: -fsigned-bitfields y -funsigned-bitfields.

  • Troubleshooting

GCC permite la opcion -v para que compilemos de forma detallada (verbose). Esto puede darnos cierta informacion sobre algunos errores de compilacion y ver como se sigue todo el proceso, mostrando ademas todos los PATH de bibliotecas o librerias.

Con la opcion -g, GCC permitira compilar guardando informacion para el coredump en caso de producirse. O por ejemplo si entramos en un loop infinito, podemos sacar el PID del proceso y llamarlo mediante: gdb a.out y una vez dentro de gdb, hacer un attach 891 (siendo 891 el PID por ejemplo). Que ademas nos mostrara la linea actual en la que el proceso ha sido justo “adjuntado”, pudiendo encontrar asi facilmente el loop infinito. Haciendo por ejemplo un print de la variable que nos interese o bien un kill para terminar el proceso desde el propio gdb. Con kill -3 PID crearemos un SIGQUIT en lugar de SIGKILL, de esta forma crearemos un coredump en caso de necesitarlo para investigaciones futuras.

Por ultimo y para evitar excesivo uso de memoria de un proceso podemos usar el comando “ulimit -v 4096″ dando asi 4 Mb de memoria virtual para un proceso. Con la opcion -p que limitaria el numero de procesos hijo. O la opcion -t que limitaria el numero de segundos de CPU de uso para un mismo proceso.

1. ar

La herramienta GNU ar (archiver) esta intimamente relacionada con la seccion anterior de compiladores. Fundamentalmente se centra en combinar una coleccion de objetos en un solo fichero, tambien conocido como biblioteca o library.

Creamos el fichero hello.c:

#include <stdio.h>
#include “hello.h”
void
hello (const char * name)
{
printf(“hello %s\n”, name);
}

Creamos el fichero bye.c:

#include <stdio.h>
#include “hello.h”
void
bye (void)
{
printf(“Goodbye\n”);
}

Creamos el fichero hello.h

void hello (const char *);
void bye (void);

Compilamos usando la herramienta GCC:

$ gcc -Wall -c hello.c
$ gcc -Wall -c bye.c

Para insertar todo ello en un solo fichero a modo biblioteca debemos entonces usar ar con opcion cr (create and replace):

$ ar cr libhello.a hello.o bye.o

Para ver la lista de objetos de una lib usariamos el comando ar con opcion t (table of contents):

$ ar t libhello.a

Es importante distribuir los ficheros de cabecera o headers (.h) cuando vayamos a distribuir publicamente nuestras libs.

Si quisieramos hacer uso de esta libreria en un programa fundamental podriamos hacerlo de la siguiente forma:

#include “hello.h”
int
main (void)
{
hello(“everyone”);
bye();
return 0;
}

Y lo compilariamos de la siguiente forma:

$ gcc -Wall main.c libhello.a -o hello

o bien sin necesidad de especificar la lib:

$ gcc -Wall -L. main.c -lhello.o -o hello

2. Gprof

Con un profiler como Gprof, podemos medir el rendimiento de un programa viendo el numero de llamadas para cada funcion asi como el tiempo consumido en ellas. Para hacer uso del profiler, el programa debe estar compilado con la opcion -pg de esta forma se creara un ejecutable instrumental el cual contiene instrucciones adicionales para el analisis del tiempo empleado en cada funcion.

$ gcc -Wall -c -pg file.c
$ gcc -Wall -pg file.o

La opcion -pg se debe usar no solo en cada fichero en concreto a ser compilado, sino tambien en el paso del linker.

Una vez ejecutemos el fichero mediante ./a.out obtendremos de forma silenciosa el fichero gmon.out en el directorio actual. El cual, puede ser analizado mediante la herramienta gprof de la siguiente manera:

$ gprof a.out

El cual mostrara informacion sobre el tiempo empleado en cada funcion, numero de llamadas, etc.

3. Gcov

La herramienta Gcov sirve para dar cobertura (coverage) a nivel de testing, indicando el numero de veces que una linea de codigo es ejecutada, de esa forma podriamos encontrar codigo que jamas sea ejecutado.  Para dar soporte a esta opcion debemos compilar de la siguiente forma con gcc:

$ gcc -Wall -fprofile-arcs -ftest-coverage file.c 

Esto creara otro ejecutable instrumentado. La opcion -ftest-coverage anadira instrucciones para contar el numero de veces que cada linea es ejecutada. La opcion -fprofile-arcs anadira instrucciones referente a las ramificaciones de codigo y los distintos caminos que sigue.

Una vez se haya ejecutado el codigo, este creara 3 ficheros en el directorio actual: .bb.bbg.da. Estos datos pueden ser analizados mediante el comando gcov:

$ gcov file.c  

Esto generara un fichero llamado file.c.cov, paralelo al fichero fuente pero con anotaciones del numero de veces que son ejecutadas cada una de las lineas. Las lineas que en lugar de un numero contengan ###### implicara que son lineas de codigo jamas ejecutadas.

 

4. GNU/Make

Generalmente crearemos un fichero llamado makefile (tambien puede ser Makefile o GNUMakefile) donde pondremos los targets a construir. De esta manera cuando lancemos el comando make, se compilara el codigo de esos targets. En caso de no tener ningun target desde el shell, se construira el target de default. Generalmente el objetivo sera compilar el codigo de algun programa, pero en algunos casos el codigo estara incompleto y se requeriran de herramientas como flex o bison. El siguiente paso ya sera compilar el codigo a binarios (.o en el caso de C/C++) y mas tarde enlazarlos con un linker (esto suele venir incluido ya de forma transparente en gcc).

4.1 Formato basico de make

En el fichero makefile tambien se determina la relacion que existe entre distintos ficheros fuente, ficheros intermedios y el ejecutable final. Todo esto quedara en este fichero, simplificando y optimizando el tiempo entre modificar codigo-compilar-debuggear.

El comando make ejecutara la regla por defecto, que tiene las siguientes partes:

    • target : es el fichero resultante que se debe crear.
    • sus prerequisitos : son las dependencias o aquellos ficheros que existen antes del objetivo.
    • el comando : es el comando que generara ese objetivo con sus prerrequisitos.

target: prereq1, prereq2

comandos

El ejemplo mas simple seria:

foo.o: foo.c

gcc -c foo.c

Y otro ejemplo sencillo seria:

hello: hello.c

gcc -o hello hello.c

Y un ejemplo teniendo nuestro fichero de cabecera seria:

hello: hello.c hello.h

gcc -o hello hello.c

4.2 Como trabaja make

La forma en la que make trabaja es la siguiente:

  • make evalua la regla.
  • comenzando por encontrar los ficheros de prerrequisitos.
  • si algunos de estos prerrequisitos estan asociados a alguna regla, make intentara actualizar esto primero.
  • si algunos de estos prerrequisitos estan asociados a una biblioteca de la forma -l, entonces make buscara un fichero con la forma libNAME.so y en caso de no encontrarlo lo buscara de la forma libNAME.a, para proceder al linkado.
  • Despues, si cualquiera de los prerrequisitos es mas nuevo que el target, entonces se volvera a reconstruir este target ejecutando los comandos enviados a un subshell.

El orden de ejecucion de los comandos ejecutados en make, son inversos a como estan escritos en el fichero (estilo top-down). Siendo los comandos generales los primeros y los mas especificos a medida que se baja en el fichero.

Aqui un ejemplo de makefile mas complejo:

Si modificasemos el fichero lexer.l (por ejemplo para crear nuevas reglas con palabras desconocidas) y lanzasemos de nuevo make, gnu/make minimizara las reconstrucciones y se cercionara que count_words.o es mas nuevo que count_words.c, por lo que no necesitara recompilarlo, por lo que al lanzar make de nuevo, veremos como obvia esta linea.

Si quisieramos actualizar solo un target especifico, podriamos indicando el fuente al que nos referimos por ejemplo para lexer seria:

$ make lexer.c

Si lexer.c tuviese algun prerrequisito o algunos ficheros fuera de fecha, esto se volveria a compilar, construir y actualizar en cuanto a fecha.

4.3 Sintaxis basica en makefiles

Otro tipo basico de sintaxis Makefile seria:

target1, target2, target3 : prerrequisito1, prerrequisito2

comando1

comando2

comando3

Debemos tener muy en cuenta que los comandos deben tener como primer caracter un TAB, de esta forma make conoce si deben ser pasados como comando a un subshell o no.

Por ultimo, las lineas que sean demasiado largas, las podremos partir mediante el uso del caracter ‘\‘.

Los comentarios vendran precedidos por # y seran ignorados.

4.4 Reglas

Si en nuestro makefile escribimos reglas tal que asi indicaremos que los 2 targets dependen del mismo set de prerrequisitos:

vpath.o variable.o: make.h config.h getopt.h gettext.h dep.h

Si tenemos muchos prerrequisitos para un target, podremos distribuirlo en dos lineas para mejorar su legibilidad:

vpath.o: vpath.c make.h config.h getopt.h gettext.h dep.h

vpath.o: filedef.h hash.h job.h commands.h variable.h vapth.h

4.5 Willcards

 

4.6 Variables y Macros

4.7 Funciones

4.8 Comandos

4.9 Manejando proyectos grandes

4.10 Makefiles portables

4.11 Makefiles: C y C++

4.12 Mejorando el rendimiento de make

4.13 Ejemplos de Makefiles

4.14 Debuggeando Makefiles

4.15 Parametros de make

4.16 Estructuras de datos

5. Automake

<en desarrollo>

6. Autoconf

<en desarrollo>

7. Autotools

<en desarrollo>

8. GIT

Git es un sistema de control de versiones moderno y distribuido. Si queremos hacer de nuestro proyecto algo serio y ganar tiempo y seguridad, es casi obligatorio usar hoy en dia un sistema de control de versiones como este. Pasamos a comentar desde los comandos mas habituales hasta algunos mas avanzados.

8.1 Primeros pasos para iniciar un repositorio git:

  1. git init -> inicializa el repositorio git
  2. touch file.c -> creamos el primer fichero
  3. git add . -> indicamos que el directorio actual y ficheros seran controlados
  4. git commit -m “Proejct initialized” -> hacemos el primer commit
  5. emacs file.c -> modificamos los fuentes a nuestro antojo
  6. git diff -> mostramos los cambios entre el commit anterior (release) y el fuente actual
  7. git commit -a -m “un commit” -> hacemos un commit global (-a de all) con un mensaje especifico (-m)

8.2 Hacer cambios locales sobre un fichero:

  1. git add file2.c -> anadimos un nuevo fichero a controlar
  2. git commit -m “otro commit” -> y hacemos commit de ese fichero especifico

8.3 Hacer cambios sobre un directorio

  1. git add DIR -> controlamos un directorio entero
  2. git commit -m “otro commit mas”

8.4 Eliminar ficheros

  1. git rm fichero.c

8.5 Renombrar ficheros

  1. git mv old.c new.c

8.6 Configuracion de git

  1. git config user.name “Johan Strongen”
  2. git config user.email “foo@bar.org”

8.7 Otros comandos informacionales

  • git log -> sumatorio de commits hechos
  • git diff -> ver los diff o cambios hechos actualmente
  • git status -> estado de la rama y en que rama nos encontrabamos
  • git commit -v -> muestra informacion detallada de los commit
  • git ls-files -> ?????
  • git branch -> muestra las ramas
  • git branch -a -> muestra todas las ramas, tambien las remotas

8.8 Trabajando con ramas

  1. git branch nueva_rama master -> crea una nueva rama replica de master
  2. git checkout nueva_rama -> switch a la nueva rama
  3. git checkout master -> switch a la rama principal

8.9 Ejemplo completo de manejo en Git en remoto (por ejemplo github.com) sobre un proyecto:

  1. Crearse un usuario en la pagina, actualizar el perfil y crear un repositorio (newrepo)
  2. Configuracion
    1. git config –global user.name “Foo Bar”
    2. git config –global user.email “foo.bar@fred.org”
    3. git config –global core.editor “emacs”
    4. git config –global color.branch auto
    5. git config –global color.diff auto
    6. git config –global color.interactive auto
    7. git config –global color.status auto
    8. git config –global http.proxy host_proxy:port_proxy
    9. anadir la clave publica generada por ssh-keygen al servidor (emplear la interfaz web).
  3. Inicializar y preparar el proyecto
    1. mkdir lab
    2. cd lab
    3. git init
    4. touch README
    5. git add README
    6. git commit -m “First commit”
    7. git remote add origin git@github.com:username/lab.git
    8. git push origin master
  4. Comenzar con el desarrollo con nuevos ficheros o modificar los ya existentes
    1. git add nuevofichero.c
    2. git diff
    3. Aqui podriamos usar un comando informacional como: “git ls-files” / “git commit -v” / “git diff” / “git log”
    4. git commit -m “Mensaje del commit”
    5. git push
  5. Trabajando con tags
    1. git tag “v0.1”
    2. git push –tags
  6. Trabajando con nuevas ramas
    • git branch nombre_rama -> crea una nueva rama
    • git push origin nueva_rama_remota -> crea una nueva rama en remoto (push)
    • git fetch origin rama_remota:rama_local -> hace pull de una nueva rama desde una remota
    • git checkout nombre_rama -> cambia a otra rama (hace checkout) local HD sera modificado
    • git rebase master -> asegura que los cambios en master aparecen en tu rama
    • git fetch origin && git fetch origin master -> asegura que los cambios de master estan en la actual
  7. Merging de ramas
    1. git checkout master / git co master -> switch a master
    2. git diff master otherbranch -> vemos los cambios a fusionar entre dos ramas
    3. git merge otherbranch -> hacemos el merge
    4. Eliminar las marcas, anadir el fichero y commitear en caso de conflictos
    5. git reset –hard ORIG_HEAD -> revertimos el cambio en caso de necesitarlo
  8. Stashes
    1. git checkout -b nombre_rama -> creamos una rama
    2. git stash save “Mensaje para recordar que estabas haciendo” -> crea un stash, que es como un clipboard, permite cambiar de ramas sin hacer commit en los cambios.
    3. git checkout rama_a_cambiar
    4. realizariamos los cambios necesarios en nuestro codigo
    5. git checkout a_la_rama_stashed -> ponemos los cambios del stash
    6. git stash list -> ver los cambios del stash
    7. git stash apply -> volvemos atras cuando estabamos con el stash, ahora podemos continuar nuestro trabajo que teniamos a mitad.
    • git stash clear -> eliminaria un stash
  9. Eliminando ramas
    • git branch -d nombre_rama
    • git branch -D nombre_rama_unmerged
  10. Configurando repositorio para uso de servidores remotos
    1. scp -r my_prj user@yourbox.com:my_prj (mover los ficheros en un servidor remoto a /var/git/my_prj)
    2. sudo chown -R git:git my_prj -> restringir git-shell en /etc/passwd por seguridad
    3. git clone git@yourbox.com:/var/git/my_prj -> clonar el repo remoto en tu maquina
    4. cat .git/config -> ver entra info sobre el repo remoto
    5. git pull -> actualizar el server local del remoto
    6. git fetch laptop -> bajar una copia entera del repo remoto sin hacer mergin con el local
    7. git merge laptop/xyz -> hacemos fusion de 2 ramas locales
    8. git remote show laptop -> ver el metadata del repositorio remoto
    9. git push laptop xyz -> push de un cambio de una rama local en otra rama remota
    10. git branch –track local_branch remote_branch -> crear un tracking branch (no hay que especificar el local si estas ‘sentada’ en ella)
    11. git pull -> ahora seria posible hacer track de diferentes ramas locales en diferentes ramas remotas
    12. git remote show origin -> ver en que ramas se hace tracking (comando informacional)
  11. Git atacando a SVN
    1. git-svn clone http_o_svn_repo -> clonar en forma de repo SVN.
    2. git-svn dcommit -> pushing/commiting de un repositorio remoto SVN.
    3. git-svn rebase -> actualizar un git local usando un SVN remoto.


9. GDB

Para comenzar a poder usar GDB con nuestros programas, debemos guardar la tabla de simbolos una vez compilemos con GCC. La tabla de simbolos es la lista de direcciones de memoria correspondientes a las variables y las lineas de codigo.

$ gcc -g -o ficherobinario ficherofuente.c

Para iniciar GDB sobre un binario debemos lanzar:

$ gdb ficherobinario

O bien si queremos usar el frontend DDD:

$ ddd ficherobinario

Para poner un breakpoint con gdb:

gdb> break numerodelinea o b numerodelinea

Para poner un breakpoint con ddd:

  1. Hacemos doble click en la linea deseada
  2. Hacemos click en la linea deseada y pulsamos el boton de “Break”
  3. Hacemos click derecho en la linea deseada y pulsamos “Set breakpoint”.

Si usamos Eclipse debemos instalar CDT (nos centraremos en C/C++ aunque puede ser aplicable a otros lenguajes de forma parecida).

CDT lanza internamente GDB y podemos apretar en el margen izquierdo del codigo donde con un punto azul se simbolizara que existe un breakpoint.

Como vemos cada operacion es posible usando distintos caminos, con gdb en modo texto; ya sea usando un keyword o su abreviatura. Usando una interfaz grafica; empleando distintas formas con boton izquierdo del raton, boton derecho o doble click en muchos casos. O bien usando un IDE (o entorno de desarrollo); de una forma parecida al anterior pero conectado directamente a nuestro desarrollo. Nos centraremos solo en los caminos mas importantes y generalmente solo en gdb en modo texto debido a las ventajas que este tiene:

  1. Es mas rapido si lo lanzamos para un debugging rapido.
  2. Permite hacer uso de debugging remoto via ssh sin necesidad de servidor de X11 ni similar.
  3. Para realizar multiples debuggings de programas que interactuan entre ellos, no es necesario ocupar la pantalla completa con cada GUI.
  4. Los programas a debuggear con interfaz grafica pueden interferir en cuestion de eventos y keystrokes con el propio debugger grafico.
  5. El uso medio y avanzado de GDB es mucho mas rapido teniendo un minimo de conocimientos que de forma grafica, tambien es mas potente.
  • TUI (Terminal User Interface)

Tambien es posible lanzar GDB en TUI mediante el comando gdb -tui. De esta forma podemos seguir el progreso del programa aun usando un terminal sin necesidad de emplear un entorno de desarrollo. Con control+X+A permitira hacer toggle de este modo.

  • Historial GDB

Control + p : muestra el comando previo.

Control + n : muestra el siguiente comando.

  • Ejecutando codigo

run : inicia la ejecucion del programa.

<Enter> : ejecuta de nuevo el ultimo comando lanzado.

  • Breakpoints, watchpoints y catchpoints

La diferencia entre breakpoints, watchpoints y catchpoints reside en que los breakpoints detienen la ejecucion en un particular punto (codigo) del programa. Los watchpoints detienen la ejecucion en una zona particular de memoria. Y por ultimo, los catchpoints detienen la ejecucion en un evento particular. Podemos poner breakpoints de la siguiente forma:

break numerodelinea

break nombredefuncion

break fichero:numerodelinea

break fichero:nombredefuncion

break +offset o break -offset : pone un breakpoint en un offset dado segun la linea actual, donde se detuvo la ejecuccion.

break *address : pone un breakpoint en una direccion de memoria virtual (generalmente usado donde no se tiene informacion de debugging, por ejemplo en librerias compartidas y demas).

break nombrefuncionolinea if condicion : breakpoint condicional en una linea especifica o funcion de codigo solo cuando cambia un valor de una variable a un valor determinado. Las condiciones que se pueden emplear son similares a las de C, incluso funciones si dichas han sido linkadas a nuestro codigo, pero los valores de retorno siempre seran enteros, siendo mal interpretados por GDB en caso contrario (por ej. tened cuidado con funciones como cos()).

cond numerobreakpoint condicion : anade una condicion a un breakpoint ya asignado.

cond numerobreakpoint : elimina una condicion existente de un breakpoint.

step : ejecuta la siguiente instruccion incluyendo las subfunciones. No parara si no se tiene la tabla de simbolos, por lo que generalmente en funciones de muchas libs tiene el mismo efecto que next.

next : ejecuta la siguiente instruccion sin incluir las subfunciones.

previous : vuelve a la anterior instruccion ejecutada.

continue : continua la ejecucion hasta el siguiente breakpoint (para continuar despues de un step o next).

Otros comandos sobre breakpoints:

info breakpoints : muestra la lista de breakpoints.

delete numerobreakpoint : elimina un breakpoint (se puede usar la misma sintaxis que para crearlos, si no se le pasa ningun argumento, eliminara todos los breakpoints)

clear : elimina el siguiente breakpoint.

clear nombredefuncion : elimina un breakpoint dado un nombre de funcion.

disable numerobreakpoint : deshabilita un breakpoint.

enable numerobreakpoint : habilita un breakpoint.

enable once numerobreakpoint : habilita y cuando se alcanza el breakpoint lo vuelve a deshabilitar.

tbreak numerodelinea : inserta un break temporal, el cual solo sera valido la primera vez que se llegue a ese punto, no las consecutivas en caso de haber.

until fichero:numerodelinea : breakpoint especial. Continuara el programa hasta cierta linea. El uso mas tipico es para poner breakpoints al salir de funciones recursivas, una vez se vaya a salir de estas o en iteraciones largas.

until fichero:nombredefuncion : breakpoint especial. Continuara el programa hasta cierta funcion.

finnish : breakpoint especial. Continuara el programa hasta que finalice el frame actual.

Otros tipos de breakpoints:

Si en lugar de usar break usamos hbreak, pondremos hardware breakpoints en lugar de software breakpoints. Esto se emplean generalmente para debuggear dispositivos EEPROM o ROM. Tambien se pueden emplear breakpoints hardware de manera temporal como thbreak.

Tambien es posible usar rbreak, que se refeiren a breakpoints mediante el uso de expresiones regulares al mas puro estilo grep. Por ejemplo podriamos hacer uso de * y otros caracteres especiales para poner breakpoints en todas las funciones que comiencen por la palabra func*.

Lista de comandos:

Es posible realizar listas de comandos para breakpoints, para una tarea repetida cada vez que se alcance un breakpoint, de la siguiente manera:

commands numerobreakpoint

silent

… comandosgdb …

continue

end

El comando silent es opcional, sin embargo es util para evitar una salida demasiado detallada de forma repetitiva.

El comando continue tambien es opcional, pero evita que tengamos que estar continuando de forma manual de forma repetitiva cada vez que pare en el breakpoint.

Estos dos comandos suelen ser muy utiles cuando queremos ver de manera rapida los valores que se le envian a una funcion de forma repetida o los que devuelve sin necesidad de parar la ejecucion realmente.

Catchpoints:

Los catchpoints pueden capturar distintos tipos de eventos ya sean excepciones, senales, llamadas a fork, carga y descarga de librerias, etc.

Disposicion de breakpoints:

Cuando se realiza un info breakpoints, hay un campo que indicara la disposicion, que puede tomar los siguientes valores:

keep : cuando se alcance el breakpoint, se seguira manteniendo este.

del : cuando se alcance el breakpoint, se eliminara este.

dis : cuando se alcance el breakpoint, se deshabilitara.

  • Inspeccionando variables

El uso de watchpoints tiene sentido cuando con un breakpoint no podemos saber a ciencia cierta en donde cambiara el valor de una variable, ya que en ciertos lenguajes como C esto puede ser realmente oculto y no tan trivial, como por ejemplo mediante el uso de punteros y punteros a punteros o punteros dentro de estructuras y demas.

print nombredevariable : muestra el valor de una variable en un momento determinado.

watch nombredevariable : parara la ejecucion en el momento del programa en el que una variable inspeccionada cambie su valor.

watch (nombredevariable > 28) : parara la ejecucion en el momento del programa en el que una variable inspeccionada cumpla una condicion, por ej. ser mayor que 28. Para booleanos se puede emplear true o false. Se pueden emplear macros del preprocesador en la condicion si se compila mediante la opcion -g3.

  • Imprimiendo variables avanzado

print nombrepunteroeastructura->miembro : nos permite imprimir el valor de una variable en una estructura.

print *nombrepunteroaestructura : imprime la estructura al completo.

display *nombrepunteroaestructura : imprime la estructura al completo cada vez que se detenga la ejecucion por un breakpoint, next, step, etc.

disable display numerodisplay : desactiva un display.

enable display numerodisplay : activa un display.

undisp numerodisplay : elimina un display.

call funcionqueimprime(raizarbol) : podemos emplear la funcion call para llamar a una funcion e imprimir un arbol al completo por ejemplo o realizar cualquier otra tarea. De esta forma no necesitaremos realizar esta llamada en el propio codigo ni modificarlo, pudiendo usar gdb para ello.

jump lineadecodigo : salta a una linea de codigo especifica (debemos tener cuidado con esto, porque algunas instrucciones maquina pueden depender de otras).

ddd –separate bintree : permite imprimir estructuras enlazadas por punteros de forma grafica y visual para DDD.

print nombredearray : imprime un array completo.

print punteroaarray : imprime la direccion del array.

print *punteroaarray : imprime el array completo dada una direccion.

print *punteroaarray@numeroelementos : imprime una lista de elementos dado una direccion de memoria asociada a un array.

print (int [25]) *x : imprime 25 elementos de un array casteado a entero.

info locals : imprime el valor de todas las locales variables a esa funcion.

  • Examinando memoria

Si en lugar de imprimir una zona especifica de memoria mediante el uso de un puntero, deseamos imprimir la zona contigua ya que nos puede ser util en el debuggeo. Podemos usar x de eXamine.

x direcciondememoria

x /NFUdirecciondememoria

Los parametros NFU son opcionales:

Donde N es el numero de veces que imprimiremos.

Donde F es el formato en el que imprimiremos (c – chars, s – strings, f – floats, x – hexadecimal, d – decimal).

Donde U es el tipo de Unidad (b – bytes, h – half words, w – words, g – giants)

  • Frames

Cuando se realiza un call a funcion, la informacion de runtime asociada a esa llamada es guardada en una region de memoria llamada stack frame. Dicho frame contiene los valores de las variables locales, los parametros y la direccion desde donde fue llamada la funcion. Cada vez que una funcion nueva es llamada, un nuevo frame es creado e insertado en un stack mantenido por el sistema. El frame que se encuentra en el top del stack representa la funcion ejecutada actualmente, sera extraido y desasignado (deallocated) una vez se haya salido de la funcion.

Cuando estamos ejecutando codigo, es posible que nos interese saber valores de variables o inspeccionar el frame anterior. El frame actual se reconoce como 0, por lo tanto el anterior sera el 1. De esta forma estando en una funcion podemos ver valores de la anterior (la que hizo el call a la actual) de la siguiente forma:

frame 1

Tambien podemos movernos a frames padres o hijos mediante:

up

down

Si queremos ver el stack completo podemos hacer:

backtrack

  • Otros comandos interesantes:

list nombredefuncion : imprime el codigo de una funcion.

list *direcciondememoria : imprime el codigo que se encuentra en una direccion de memoria.

  • Variables

Es posible modificar el valor de variables en gdb para alterar el flujo de ejecucion del programa:

set variable=valor

Para modificar los argumentos que se le pasan a una funcion como main:

set args valor1 valor2 valor3 …

Es posible almacenar valores de variables en gdb para uso futuro su sintaxis es:

set $q = p

p $q

Y este podria ser un uso tipico:

set $i=0

p w[i++]

y pulsar repetidas veces intro, de esta forma podriamos imprimir facilmente los valores de un array.

  • Crash

Cuando un programa crashea generalmente es porque el programa esta intentando acceder a una zona de memoria sin permisos. El hardware notara esto y hace que se realice un jump al OS. El sistema operativo anunciara generalmente que ha causado un segmentation fault y descontinuara la ejecucion del programa. El hardware debe soportar memoria virtual y el OS debe debe hacer uso de estos errores cuando ocurran.

Las distintas secciones son:

.text : las instrucciones de codigo maquina generadas por el compilador a partir del codigo fuente. Cada linea en C se traduce generalmente como 2 o 3 instrucciones. Ademas aqui tambien se incluye el codigo enlazado estaticamente /usr/lib/crt0.o que realiza algunas inicializaciones y llama a main().

.data : contiene todas las variables que fueron reservadas en tiempo de compilacion (es decir, variables globales).

.bss : contiene aquellas variables que contengan datos no inicializados (como int y;) pero si declarados.

heap : esta zona de memoria se empleara cada vez que nuestro programa reserve memoria dinamica.

stack : esta zona de memoria se empleara para datos reservados de forma dinamica para funciones; incluyendo argumentos, variables locales y la direccion de retorno.

el codigo linkado de forma dinamica como librerias externas se encuentra entre el heap y el stack (aunque esto es dependiente de la plataforma y sistema).

env : variables de entorno.

[Diagrama de las distintas secciones]

Para ver el layout de memoria de un proceso especifico en GNU/Linux tan solo debemos visitar el fichero /proc/PID/maps.

Las direcciones de memoria virtual van de 2^numerobitsprocesador – 1. La memoria virtual y la memoria fisica (RAM y ROM) es organizada en paginas que en procesadores pentiums tienen un tamano de 4096 kbytes. Cuando un programa es cargado en memoria para su ejecucion, el OS reserva algunas de estas paginas de la memoria fisica, a estas paginas se les llamaran residentes, el resto estaran en disco. Es posible que durante la ejecucion de un programa no sea necesario que este todo el tiempo residente, asi que puede pasar a disco y luego volver a estar residente; es el hw quien se cerciona de esto y da el control al OS para que realice las tareas oportunas, existen muchas situaciones. En cualquier caso para todas estas tareas, el OS mantiene una tabla de paginas por cada proceso (en intel tienen una estructura hierarchical). Cada pagina virtual para un proceso tiene la siguiente estructura:

– La direccion actual fisica ya sea en memoria o en disco.

– Permisos ya sea para lectura, escritura o ejecucion para esa pagina especifica.

[Diagrama de paginas y relacion entre memoria fisica y virtual]

Por lo tanto, podemos decir que la pagina virtual 0 comprende desde los bytes 0 hasta 4095, la pagina virtual 1 desde 4096 hasta 8191, etc. El registro de tabla de paginas del hardwre apuntara a esta tabla asociada a un proceso en ejecucion. Es decir, cada pagina del espacio de memoria virtual tiene una entrada a esta tabla de paginas, y los segmentation faults vienen cuando se intenta acceder a una de ellas por falta de permisos. Y estos son los accesos que ocurren y que da segfault cuando falten permisos:

  1. Cada vez que el programa usa variables globales: Requerido permiso lectura/escritura en la seccion de datos.
  2. Cada vez que el programa usa variables locales: Requerido permiso lectura/escritura en el stack.
  3. Cada vez que el programa entra o sale de una funcion: Requerido permiso lectura/escritura en el stack.
  4. Cada vez que el programa accede a almacenamiento por memoria dinamica: Requiere permiso lectura/escritura en el heap.
  5. Cada instruccion maquina ejecutada de un programa sera emplazada (o la parte de codigo dinamica): Requiere permiso lectura/ejecucion en el text.

Como las paginas de memoria tienen un tamano dado (el minimo usado por la VM), no significa que el exceder por ejemplo los limites de un array siempre obtengamos un segfault, ya que podemos estar usando lo que se llaman elementos fantasmas en un array. Pero ello no significara que estemos haciendo accesos dentro de los limites de este array y por lo tanto no seran legales.

  • Senales

Las senales indican condiciones excepcionales reportadas durante la ejecucion de un programa y que permiten al OS o el propio programa reaccionar ante una variedad de eventos. Las senales pueden ser lanzadas desde:

  1. Por el hardware: como SIGSEGV o SIGFPE.
  2. Por el OS: como SIGTERM o SIGABRT.
  3. Por otro proceso: como SIGUSR1 o SIGUSR2.
  4. Por el propio proceso: mediante raise().

La senal mas tipica es la de control+c en un proceso, que el OS reconocera y lanzara entonces un SIGINT hacia el proceso. Realmente lo que ocurre es que el OS graba la senal en la tabla de procesos, y la siguiente vez que el proceso reciba tiempo de CPU entonces la funcion del manejo de senales ejecutara realmente la senal. Cada senal tiene su propio manejador de senales, que realmente lo que hace es lanzar una funcion cuando una senal en particular se lanza hacia un proceso. Algunas funciones pueden ser reescritas, otras no.

  • Debugging threads

Existen distintas areas para trabajar con threads (o bien multiples vias de codigo). Analizamos cada una de ellas:

Networking

Se recomienda el uso de errno.h que crea una variable global llamada errno y que podemos imprimir via GDB para saber si tuvo exito las llamadas a algunas funciones como connect() o similar. Los distintos errores de errno se pueden consultar en /usr/include/linux/errrno.h.

Se recomienda el uso de strace, el cual traza llamadas al sistema, podemos ejecutar el programa tal que asi: strace binario argumentos.

Desde GDB tambien podemos llamar directamente a funciones como connect() con todos sus argumentos pero generalmente debemos eliminar los casts por tratarse de usos locales ajenos a GDB en muchos casos.

Para aplicaciones cliente/servidor mas complejas, podemos lanzar el uso de dos sesiones GDB paralelas, lanzando los comandos en tandem.

Threads

Cuando se lanza un programa con threads, asumiendo que solo tenemos 1 procesador (ya que en multiples esto se ejecutaria de forma paralela en distintos micros), para cada proceso se dedica un tiempo en milisegundos llamado timeslices. Una vez pasado uno de estos slices, el timer del HW emite una interrupcion, que hace que ejecutar al OS, en este momento decimos que el proceso prempted (adelanta). El OS guarda el estado actual del proceso para que pueda ser continuado mas tarde, selecciona el siguiente proceso y le da otro timeslice, esto es conocido como context switch (ya que el entorno de ejecucion de la CPU cambia de un proceso a otro). Muchas lineas dependen de la I/O u otros eventos dependientes de usuarios y que el proceso llama al OS para ello. Es por esta razon por la que el tiempo en el que el siguiente timeslice comenzara es impredecible. Tambien es la razon por la que si debuggeamos un programa con threads no sabremos el orden en que estos seran planificados, esto hace un debugging mucho mas complejo.

El OS mantiene la tabla de procesos que muestra informacion sobre el proceso actual, indicando si esta en ejecucion (run) o durmiendo (sleep). El OS generalmente marca procesos como sleep cuando se espera la accion del usuario de I/O para continuar. Esto significa que el proceso esta bloqueado esperando a que ocurra dicho evento, cuando eso ocurra, el OS cambiara el estado a run de nuevo. No todos los procesos deben esperar acciones de usuario, es posible que existan otros que esperen mediante la funcion wait() esperando un proceso padre a que el proceso hijo termine, de nuevo, esto es impredecible. Por lo tanto, podriamos pensar que a nivel de comportamiento en el procesador, un thread es parecido a un proceso, pero que generalmente consume menos memoria y tambien menos tiempo entre switching. Por supuesto la principal diferencia es que aunque el main de un proceso sea un thread propio y pueda tener varios threads hijos, cada uno con sus propias variables locales, las variables globales del programa padre son compartidas por todos los threads. Para ver todos los procesos con sus threads debemos lanzar el comando ps axH.

Para trabajar con Pthreads debemos linkar mediante el argumento -lpthread. Si por ejemplo trabajamos con un codigo con un monton de hilos que realizan una misma tarea en paralelo y por alguna razon olvidamos un pthread_mutex_unlock en el reparto para realizacion de estas tareas, tendremos un problema, ya que un hilo realizara su trabajo mientras que el resto se encontraran a la espera ya que siempre estaria dicha tarea bloqueada. Si lanzamos GDB con un programa asi de ejemplo, el debugger nos informara cada vez que se lance un thread y veremos que el programa no continua, teniendo que terminarlo con un Control+c.  (SIGINT). Con el comando de gdb info threads podemos ver la lista de threads. Dejamos a continuacion una lista de comandos aplicables a threads desde GDB:

thread numerodethread : cambia para debuggear un thread especifico.

break numerolinea thread numerodethread : pone un breakpoint en una linea dada de un thread especifico.

break numerolinea thread numerodethread if condicion : pone un breakpoint en una linea dada de un thread especifico si se cumple una condicion como por ejemplo x==y.

  • Debuggeando en paralelo

Existen dos tipos principales de programacion en arquitecturas paralelas.

Memoria compartida (shared memory)

Se basa en que multiples CPU tienen acceso a una memoria fisica comun, permitiendo a otras CPUs la lectura y escritura. Existen sistemas reales de memoria compartida asi como emuladas por software mediante memoria distribuida.

La memoria compartida real, generalmente se desarrolla usando threads, que puede ser casi transparente de cara al usuario mediante el uso del popular OpenMP.

La memoria compartida emulada por software, es la que podemos ver en los dual cores o similares, en las que se crea una libreria para aparentar el uso de memoria compartida que luego sera usada entre los distintos cores, SDSM es uno de los sistemas mas populares en este aspecto. En estos sistemas se suele realizar una especie de duplicado de paginas de memoria en una maquina virtual. Esto es importante a la hora de debugging ya que nos encontraremos en casos donde parece que misteriosamente el debugger se detiene en lugares donde no hay seg faults, ya que son generados por este SDSM de forma deliberada y no existentes en las zonas esperadas, para evitar esto podemos lanzar con cautela la orden: handle SIGSEGV nostop noprint.

El uso de OpenMP (OMP) se realiza de la siguiente manera, indicamos desde el shell el numero de threads que necesitamos:

$ setenv OMP_NUM_THREADS 4

Ademas de incluir omp.h requerido para el uso de OMP, debemos aplicar directivas al micro desde codigo C:

#pragma omp parallel Especifica que a partir de aqui la ejecucion sera paralela

#pragma omp barrier Especifica un punto de encuentro para todos los threads

#pragma omp single Especifica que a partir de aqui la ejecucion dejara de ser paralela.

#pragma omp critical Especifica que para solo un thread estara permitida esta seccion.

Tambien necesitaremos hacer uso de funciones tal que omp_get_thread_num(), omp_get_num_threads(), etc.

Lo mas importante que debemos tener presente, es que como OMP usa directivas al preprocesador, no podemos tener la certeza de mantener las mismas lineas cuando debuggeemos . Cuando comencemos a debuggear, si paramos en distintos breakpoints para cada thread, podemos reejecutar el programa y abrir otra consola con gdb y hacer un debugging paralelo, con sorprendentes resultados, como esta vez podamos tener las lineas donde hagamos el debugging alineadas correctamente. Ademas gdb hereda las variables de la primera instancia llamada. Pero si en muchos casos el programa persiste, lo ideal es usar barreras (barriers) adicionales y compilar mediante el flag –fopenmp .

Paso de mensajes (message passing)

Se basa en que para compartir informacion se debe enviar cadenas o strings llamadas mensajes entre las distintas CPUs las cuales tienen su propia memoria local.

Generalmente se usa la popular Interfaz de Paso de Mensajes o Message Passing Interface (MPI). Sin entrar mas en detalle, tan solo diremos que se suelen usar las funciones MPI_INIT() para iniciar el proceso del paso de mensajes, MPI_Comm_size() para indicar el numero de nodos, MPI_Comm_rank() para indicar el nodo actual, MPI_Send() para el envio de mensajes, MPI_Recv() para la recepcion de mensajes desde otros nodos, una struct de tipo  MPI_Status que indica el estado y por ultimo MPI_Finalize() que indica la terminacion.

Para debuggear este tipo de programacion en arquitecturas paralelas, debemos tener en cuenta que no nos podemos olvidar ningun MPI_Send por ejemplo, ya que sino el resto de nodos quedarian a la espera de manera indefinida. Para ello, podemos pausar el programa con control+c, y acto seguido lanzar gdb de la siguiente forma: gdb nombredelbinario processid, luego lanzar un bt (backtrace) e ir cambiando de frame e imprimiendo los valores del dato trabajado en paralelo en ese momento. Tambien podemos emplear el comando attach. Si jugamos con el comando next, podemos tambien descubrir en el punto donde nos podemos quedar en el resto de nodos, por ejemplo en el MPI_Recv() que indicaria que no hemos hecho posiblemente un MPI_Send().

  • Lineas fantasmas

En algunos casos el compilador puede dar error en una linea inexistente, por ejemplo una linea mas de las totales del fichero. Sin embargo, en este tipo de errores se suele nombrar alguna funcion, donde se suele encontrar el error, generalmente por la falta de cierre de alguna llave. Podemos jugar con comentar funciones enteras, de tal forma que el siguiente error a obtener sea un undefined reference a una funcion, pero al mismo tiempo sabremos de que funcion se trata.

  • Bibliotecas ausentes

Cuando empleamos el uso de algunas bibliotecas, es posible, por alguna razon, que estas no esten linkadas correctamente o simplemente no se encuentre ni resuelva su direccion, en muchos casos es porque nos ha faltado incluir el nuevo path mediante la opcion gcc de -Ldirectorio. Para bibliotecas dinamicas podemos hacer uso de ldd, de la siguiente forma: ldd a.out y ver si realmente ha resuelto todas las bibliotecas. La otra forma alternativa de resolverlo es anadiendo el directorio al path como variable de entorno:

LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/nuevodirectorio

export LD_LIBRARY_PATH

  • Debugging ncurses

Debugguear ncurses puede ser una tarea interesante, ya que nos obliga a no poder usar un debugging manual mediante printf() ni nada parecido, ya que este ocasionaria un comportamiento imprededible. Asi que nos obligamos a hacer uso de gdb desde otra consola o terminal mediante el comando:

gdb> tty /dev/pts/numeroterminal

Para el uso de ncurses por supuesto necesitaremos linkar con la biblioteca -lncurses, incluir ncurses.h, hacer uso de la estructura que mantendra el estado de la ventanda mediante un puntero WINDOW*, y hacer uso de funciones tal que attron(), mvaddstr(), attrof(), refresh(), initscr(), noecho(), cbreak() o endwin().

Si en algun momento pausamos la ejecucion del programa con un control+c y la terminal tiene un comportamiento incorrecto, es probable que sea por el uso de funciones como noecho() o cbreak(), que habran cambiado algunas propiedades del terminal. Para solucionar esto se recomienda hacer uso de Control+j lanzar reset y de nuevo volver a pulsar Control+j.

  • Strace, ltrace

El uso de strace y ltrace es practicamente el mismo. La diferencia principal estriba en que strace hara un traza de las llamadas al sistema realizadas por nuestro binario, mientras que ltrace hara una traza sobre las funciones a las librerias llamadas por nuestro binario.

Strace se ejecutara en espacio del kernel, mientras que ltrace se ejecuta en espacio de usuario. Es posible almacenar el log de la salida, ya que esta puede ser inmensa, mediante la opcion -o, aunque la salida es truncada a 32 caracteres por linea pudiendo perder mucha informacion valiosa para el debugging, con la opcion -s numerodecaracteres podemos darle un tamano mayor. Por ultimo con la opcion -ff switch podremos guardar en distintos logs (anadira al fichero de log un .xxx donde xxx es el PID de cada proceso) para programas con distintos forks y similares.

  • Lint, sclint y splint

El nombre de lint, es el original que se creo por primera vez, mientras que sclint fue una mejora de este primero. En las ultimas mejoras se decidio cambiar el nombre a splint por su mejor sonoridad, para poder referirse a este programa facilmente. Splint es un analizador estatico de codigo, su ejecucion es muy sencilla:

$ splint fuente.c

Existen distintos niveles para analizar con splint:

+weak : chequeo debil, para codigo c no anotado.

+standard : el modo por defecto.

+checks : chequeo estrictamente moderado.

+strict : chequeo estricto.

Splint puede detectar en nuestro codigo cosas como el no uso del valor de retorno de una funcion. A veces esto no es necesario, como en funciones como scanf(), pero splint se quejara de tal forma que nos sugerira que hagamos un cast a (void) por ejemplo.

  • Problemas con DAM

En muchos casos sera necesario analizar la memoria reservada de forma dinamica (DAM – Dynamic Allocated Memory). Esto se refiere generalmente a las funciones malloc(), calloc(), realloc(), y free() para liberarla. Generalmente existen distintos problemas:

Memory leak: La memoria dinamica reservada, no es liberada.

Sistema con memoria insuficiene: La llamada a malloc() falla.

Error de acceso: Se intenta una lectura/escritura fuera de la memoria dinamica reservada.

Violacion de acceso: Se intenta una lectura/escritura de la memoria dinamica reservada despues de haber sido liberada.

Doble free: free() es llamada dos veces en la misma zona de memoria dinamica reserva.

Para solventar estos problemas a veces realmente dificiles de detectar, podemos hacer uso de:

  • Valor de retorno de malloc()

– En los casos de reserva de memoria como malloc(), chequear siempre el valor de retorno y tomar algun accion al respecto.

  • Efence

– Uso de electric Fence (Efence). Que tendra en cuenta errores como: lectura/escritura fuera de la zona DAM, lectura/escritura en una zona DAM ya liberada, uso de free() en una direccion a la que se obtuvo al reservar DAM. Para hacer uso de Efence, podemos realizarlo de la siguiente forma: gcc -g3 -Wall -std=c99 fichero.c -o binario -lefence. Ademas de incluir un codigo similar al siguiente:

extern int EF_PROTECT_BELOW;
void function( void ){
EF_PROTECT_BELOW=1;
codigo…
}

El problema de Efence, es que por la forma de trabajar, tan solo es posible chequear cada vez un tipo de error, por ejemplo necesitaremos hacer una ejecucion para overflows y otra para underflows, etc. Podemos realizar chequeo de las siguientes partes:

EF_DISABLE_BANNER : Desactiva el banner que es mostrado cuando el programa es ejecutado mediante Efence.

EF_PROTECT_BELOW : Chequea underruns (por defecto chequea overruns).

EF_PROTECT_FREE : Chequea acceso a zonas DAM ya liberadas.

EF_FREE_WIPES : Escribe 0xbd a las zonas DAM liberadas por seguridad.

EF_ALLOW_MALLOC_0 : Permite malloc de 0 elementos, aunque a veces puede ser considerado un bug, pensando que no vamos a pasar un valor variable como argumento, en algunos casos puede ser necesario.

Desde el shell podemos activar estas variables mediante por ejemplo:

export EF_DISABLE_BANNER=1

  • MALLOC_CHECK_

– Podemos hacer uso de la variable de entorno MALLOC_CHECK_ que puede ser usada para encontrar violaciones de acceso DAM. Se usa de la siguiente forma y sin necesidad de recompilar el codigo:

$ export MALLOC_CHECK_=3

Los valores que puede tomar son los siguientes:

0 : Todo el checking DAM se desactiva.

1 : Un mensaje de diagnostico es impreso en stderr cuando se produce corrupcion en el heap.

2 : El programa aborta inmediatamente y realiza un coredump con el heap corrupto detectado.

3 : Combina los modos 1 y 2.

Por ultimo decir que tiene algunos inconvenientes, como que solo tendra en cuenta estos accesos para DAM, no indicando la linea donde esta el problema, ni la variable puntero que pueda ocasionarlo, ya que muchos de estos mensajes no son lo suficientemente exactos o explicitos, solo sabemos que existe esa violacion de acceso. Ademas los its de setuid y getuid son deshabilitados por defecto, para evitar posibles exploits, pero pueden ser habilitados creando el fichero /etc/suid-debug.

  • Mcheck

– Podemos usar tambien la facilidad mcheck(), para ello debemos incluir el header mcheck.h y llamar a mcheck() antes de llamar a funciones relacionadas con el heap. Como argumento se le pasara un puntero a ABORTHANDLER que sera un manejador de excepciones en caso de que ocurra esa violacion, o si se le pasa NULL se tomara el manejador por defecto, que simplemente mostrara un mensaje de error para finalizar con una llamada a abort().

int main(void){
mcheck(NULL);
… codigo con funciones como malloc() etc…
}

Debemos compilar con la opcion:

$ gcc -g3 -Wall -std=c99 fuente.c -o binario -lmcheck

  • Mtrace

– Se utiliza principalmente para encontrar memory leaks y doubles free. Para hacer uso de esta herramienta GNU debemos realizar los siguientes pasos:

1 – Setear la variable de entorno MALLOC_TRACE a un fichero valido, donde mtrace() escribira los mensajes.

2 – Incluir mcheck.h.

3 – Llamar a matrace al principio del programa, justo debajo de los headers. Tal que asi: mtrace().

4 – Ejecutar el programa. Si existen problemas detectados, se escribiran en dicho fichero. Pero mtrace() jamas actuara sobre ficheros con setuid/setgid() de root por seguridad.

5 – Lanzar el script llamado mtrace desde consola para poder parsear el fichero de salida en un formato legible para los humanos.

6 – Si el programa crashea, mtrace(), mcheck() y MALLOC_CHECK_ no evitaran esto, y de hecho puede ensuciar el fichero de salida, por lo que debemos intentar manejar la excepcion antes de un crash para poder acceder a dicho fichero de manera legible y que no este corrupto, incluyendo una senal para capturar este momento y un muntrace() para detener la memoria trazada hasta ese momento y evitar falsos positivos en el momento del crashing:

void sigsegv_handler(int signum);
int main(void){
signal(SIGSEGV, sigsegv_handler);
mtrace();
… llamada a funcion malloc()…
raise (SIGSEGV);
return 0;
}
void sigsegv_handler(int signum){
printf(“Capturada sigsegv: senal %d. Cerrando de forma correcta.\n”, signum);
muntrace();
abort();
}
  • Errno, perror, strerror

El uso de errno puede ser interesante a la hora de encontrar ciertos errores. errno es una variable global de tipo entero, que guardara el codigo de error para una funcion dada. Es necesario incluir errno.h antes. Generalmente esta cabecera se encuentra en /usr/include/errno.h donde podemos ver la lista de errores disponibles (que en muchos casos son dependientes del sistema). Este podria ser uno de sus usos:

#include <stdio.h>
#include <math.h>
#include <errno.h>

int main(void){
double foobar = exp(1000.0);
if (errno){
printf(“error encontrado en foobar=%f por error %d”, foobar, errno);
exit(-1)
}
return 0;
}

Tambien podemos hacer uso de perror(“error encontrado”) en el que imprimira un mensaje de error despues de nuestro mensaje (y no solo un valor numerico).

O tambien podemos hacer uso de strerror(“%s”, strerror(errno)) en el que mediante el numero de error, imprimira un mensaje de error.

  • Obteniendo ayuda

Podemos obtener ayuda mediante la palabra help palabraclave. Como por ejemplo help breakpoints y demas.

  • Debuggeando

Si un programa se queda en un bucle infinito:

  1. gdb nombredelprograma
  2. cuando se quede en el bucle infinito en GDB lanzamos Control+C.
  3. Nos mostrara la linea de codigo y la funcion donde se interrumpio la ejecucion del programa y a partir de aqui podemos tener indicios.

Es posible crear ficheros .gdbinit donde tener una configuracion ya preestablecida para proyectos en general y otro en el directorio de trabajo que sea especifico para ese proyecto.

Podemos lanzar comandos para ver el ensamblador de una funcion determinada:

disassemble nombredefuncion

p/x $pc : imprime en notacion hexadecimal la localizacion actual.

  • Define

El comando define nos permite definir macros de la siguiente forma:

define nombre_de_macro

printf $arg0, $arg1

continue

end

De esta forma podemos definir listas de comandos y haciendo uso de nuestra nueva macro en gdb de la siguiente forma (lo ideal es usarlo dentro de una lista de comandos):

nombre_de_macro “Llamando a funcion() y se le pasa el valor %d\n” n

Con el comando show user podemos listar todas las macros que hayamos creado.

  • Debuggeando ASM

Es posible debuggear asm mediante las siguientes instrucciones:

$ as -a –gstabs -o testff.o testff.s

Esto produce un fichero .o que establece una vinculacion entre el source .s y el codigo correspondiente al codigo maquina de tal forma que podemos ejecutar:

$ ld testff.o

Esto producira el ejecutable a.out, ahora podemos lanzar gdb con el binario final, pudiendo poner breakpoints en nombres de funciones (labels en asm), imprimiendo valores de registros al estilo p $eax, o bien examinando memoria del stack al estilo x/4w $esp.

10. ELF

<en desarrollo>

11. Syscalls

<en desarrollo>

12. Bash

<en desarrollo>

13. Sed

<en desarrollo>

14. AWK

<en desarrollo>

15. Coreutils

Las coreutils es un paquete fundamental en sistemas GNU con los comandos y herramientas mas fundamentales que nos permiten trabajar con el sistema. Realmente es la combinacion de 3 paquetes anteriores: fileutils, shellutils y textutils. Como tenemos demasiadas herramientas fundamentales, nos centraremos en las que mas nos interesen para el desarrollo y tareas relacionadas.

  • Comando file

Muestra informacion detallada sobre el fichero binario u objeto (asi como otros ficheros). Indica el tipo de cabecera, por ejemplo ELF para ejecutables en sistemas gnu. Si el binario fue compilado para 32 o 64 bits. Si se usa LSB (Less Significant Bit o Little-Endian) o MSB (Most Significant Bit o Big-Endian). Si el ejecutable fue compilador para sistemas x86 u otros. La version de dicho formato interno. Si fue linkado de manera estatica o dinamica. Y finalmente si es stripped o no; en caso de no ser stripped, indica que el binario contendra la tabla de simbolos, esta se puede eliminar mediante el comando strip.

  • Comando nm

Con el comando nm podemos analizar la tabla de simbolos que puede estar tanto en ficheros ejecutables como en ficheros de codigo objeto. Generalmente cuando lanzamos el comando nm a.out o similar, buscaremos las letras T o U. la T indica que la funcion es definida en el mismo fichero y la U indica undefined, y que la direccion debe ser resuelta mediante el linkade de una lib externa.

  • Comando ldd

Cuando un programa es ejecutado mediante el uso de bibliotecas compartidas, se requiere la carga de estas de forma dinamica en tiempo de ejecucion para asi poder realizar las llamadas a las funciones empleadas. Con el comando ldd podemos examinar un ejecutable y obtener un listado de libs requeridas. Por ejemplo si lanzamos ldd a.out obtendremos las dependencias de este binario: la lib, el lugar donde se encuentra y el offset en memoria.

<en desarrollo>

Akismet is almost ready. You must enter your Akismet API key for it to work.

Leave a Reply

You must be logged in to post a comment.