Depurar con GDB

Una de las tareas que cualquier progamador debe realizar a menudo es depurar el código. Vamos a aprender a depurar con GDB nuestros programas de C++, para ello utilizaremos un sencillo programa de Hola Mundo como ejemplo. Si no tienes uno preparado puedes utilizar el que se muestra en el tutorial de CMAKE.

Preparación

Edita o crea un archivo hello.cpp para que quede tal que así:

#include <iostream>

int main()
{
  int x = 10;
  x += 2;
  std::cout << "Hello word! x = " << x << std::endl;
  
  return 0;
}
  

Asegúrate de que funciona mediante make.

Ejecutando GDB

Una vez que estamos seguros de que el programa funciona perfectamente procedemos a depurarlo mediante gdb. Para ello escribe gdb hello en la consola, lo que produce la siguiente salida:

GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from hello...
(No debugging symbols found in hello)
(gdb)

Como puedes ver, la salida del programa nos indica que no hay símbolos de depuración para poder proceder, así que vamos a comunicarlos al compilador durante el propio paso de compilación. Antes de nada sal de la sesión de compilación simplemente escribiendo q y pulsando Enter. Edita el archivo makefile para añadir la opción -g en la sección del compilador (sección hello.o):

CC = g++
all: hello
hello: hello.o
	${CC} -o hello hello.o
hello.o: hello.cpp
	${CC} -c -g hello.cpp
clean:
	rm hello.o hello

Ahora tenemos que establecer un punto de ruptura para detener la ejecución en ese punto. Para ello entramos al modo depuración, como ya hicimos antes, mediante:

gdb hello

Comandos comunes

Una vez dentro de la sesión de depuración, teclea b 5 para establecer un punto de ruptura en la línea 5.

(gdb) b 5
Breakpoint 1 at 0x11d5: file hello.cpp, line 5.

Con esto ya estaríamos listos para ejecutar de nuevo el programa. Para ello introducimos el comando r y pulsamos Enter. En este momento el programa se ejecutará con nuestro punto de ruptura correctamente establecido.

Starting program: /home/jflores/CLionProjects/cplusplus/hello

Vemos como el proceso indica que se ha detenido en la línea 5:

Breakpoint 1, main () at hello.cpp:5
5               int x = 10;

Para proceder paso a paso a partir de aquí, introducimos el comando n, equivalente a next step. Otro comando útil es s, que significa step into, para continuar dentro del paso actual (útil si estamos detenidos sobre una función, por ejemplo).

6               x += 2;

Si tenemos que conocer el contenido de una variable podemos ejecutar el comando p, que significa imprimir:

$1 = 10

Se puede continuar la ejecución del programa hasta el final, o bien hasta el próximo punto de ruptura, mediante el comando c (significa continuar).

(gdb) c
Continuing.
Hello word! x = 12
[Inferior 1 (process 423538) exited normally]

Puedes encontrar más información sobre GDB en la página oficial del proyecto (en inglés).

Usar CMAKE para compilar proyectos en C++

En este breve artículo vamos a mostrar cómo usar CMAKE para compilar programas de cualquier complejidad.

Para ello vamos a utilizar la herramienta g++ como compilador (en el resto de ejemplos vamos a tratar de usar siempre g++).

Comencemos por generar un archivo de código fuente en C++ como el ejemplo que se presenta a continuación:

#include <iostream>

int main()
{
  std::cout << "Hello World!" << std::endl;
  
  return 0;
}

Si necesitas una breve introducción a C++ puedes empezar por aquí.

Llama a este archivo de código fuente hello.cpp.

Ahora necesitarás un archivo CMAKE por lo que puedes crear uno escribiendo lo siguiente desde la consola:

touch Makefile

Con el siguiente contenido:

CC = g++
all: hello
hello: hello.o
      ${CC} -o hello hello.o
hello.o: hello.cpp
      ${CC} -c hello.cpp
clean: 
      rm hello.o hello

Es importante señalar que se deben usar tabulaciones en vez de espacios para indentar los comandos.

A pesar de que se trata de un simple programa Hello World! el ejemplo resulta útil para ver como se estructura un archivo makefile.

De forma simplificada, se puede decir que un archivo makefile define una serie de reglas, que se componen de prerrequisitos y comandos.

La primera regla, all, tiene como prerrequisito a hello. Esta regla carece de comando.

La segunda regla, hello, tiene como prerrequisito a hello.o y como comando:

${CC} -o hello hello.o

La tercera regla, hello.o, tiene como prerrequisito al archivo hello.cpp y un comando para compilar:

${CC} -c hello.cpp

La última regla, clean, tiene como comando una instrucción para eliminar tanto hello como hello.o forzando así una nueva compilación en la próxima ejecución.

Ahora por lo tanto podríamos compilar el programa sin problemas mediante el archivo makefile que hemos creado introduciendo el comando:

make

Lo cuál nos dejaría el binario compilado y enlazado, solamente tendríamos que ejecutarlo con:

./hello

Este ejemplo únicamente nos ha mostrado unos conceptos muy básicos sobre los archivos makefile y la utilidad make. Otros casos de uso más complejos son:

  • Uso de macros: Un makefile permite el uso de macros, que aparecen como variables. Éstas a su vez pueden ser usadas para dar más modularidad al makefile, como por ejemplo:
    • Macros para las librerías dinámicas usadas en el programa: LIBS = -lasd -lfgh
    • Macro para el compilador: COMPILER = GCC
    • Puedes usar como referencia esas macros en cualquier parte del makefile: ${COMPILER} o ${LIBS}
  • Cuando tecleas make en la terminal, la primera regla definida el el makefile será ejecutada. En nuestro ejemplo era all. Si lo hubiéramos cambiado por clean por ejemplo se habría ejecutado esa regla. Como norma general siempre es deseable ejecutar make con algún parámetro a continuación, por ejemplo: make clean.

¡Si tienes alguna pregunta no dudes en dejar tus comentarios!

Estructura de un programa en C++

Estructura de un programa en C++

Los programas de C++ tienen una estructura básica muy definida que al novato le puede parecer demasiado complicada o incluso arcaica. Como ya vimos en la introducción, C++ es un lenguaje compilado. Vamos a ver que la estructura de un programa en C++, a nivel básico, consta de dos partes principales.

Estructura básica

Cada programa de C++ consta de dos partes básicas: las directivas de preprocesador y la función principal, o main(). Mira este programa de ejemplo:

#include <iostream>

int main() 
{
  std::cout << "Hola, mundo!";
  return 0;
}

Directivas de preprocesador

La primera línea contiene el símbolo #, o hash, como primer caracter. Esto significa que es una directiva de preprocesador.

Tras el símbolo del hash viene la palabra include. Hay varias directivas de preprocesador disponibles en C++, pero include es la que veremos más a menudo.

Lo que hace include es indicar al compilador que queremos incluir o añadir declaraciones de la librería referenciada. Aquí estamos añadiendo las declaraciones de la librería iostream.

Los símbolos de mayor y menor (de aquí en adelante brackets) indican de qué forma se va a localizar esta librería. En este caso al ir entre brackets se va a tratar de buscar en el directorio de la librería estándar, donde se encuentran el resto de librerías.

También podemos usar comillas:

#include "main.hpp"

En este caso el compilador busca esta librería en el directorio actual y si no la encuentra busca en el directorio en el que están las librerías estándar.

Más adelante veremos por qué es muy importante esta diferenciación.

Función principal

El siguiente bloque es la sección principal del programa.

int main() 
{
	std::cout << "Hola, mundo!";
    return 0;
}

La función principal de los programas de C++, o main(), acostumbra a devolver un int. Por eso justo antes de la declaración del nombre de la función añadimos el tipo que vamos a retornar, en este caso int. Retornamos 0 al finalizar la ejecución cuando queremos indicar que no hay errores.

El propósito del programa no es más que el de imprimir la línea «Hola, mundo!» a través de la consola, que se encuentra conectada al búfer de salida. No te preocupes si no entiendes muy bien qué es un búfer de salida, de momento basta con saber que es posible mandar datos a diferentes fuentes tales como la terminal o archivos de diversa índole.

En el ejemplo, tenemos la cadena «Hola, mundo!», sabemos que es una cadena por estar entre comillas dobles. Utilizamos el símbolo ‘<<‘ para indicar que queremos redirigir dicha cadena a la salida std::cout. Como ya dijimos, esta salida suele estar conectada a la consola, por lo que veremos el texto escrito en nuestra pantalla.

Referencias

Microsoft preprocessor directives