====== C++ en Linux ======
===== General =====
* Los directorios por defecto de los includes en linux son ''/usr/include'' y ''/usr/local/include''.
===== El entorno =====
==== Funciones generales ====
* **char* getenv (char* var)** consulta la variable de entorno indicada en //var//.
==== Directorios ====
=== Saber directorio actual ===
* Podemos consultar la variable de entorno **$PWD**.
* La función **char *getcwd(char *buf, size_t size)** coloca la ruta del directorio actual en la variable //buf//, //size// corresponde al tamaño de dicha variable.
* La función **char *get_current_dir_name(void)**
* La función **char *getwd(char *buf)**
char *directorioPtr, *pathPtr;
pathPtr = getenv( "PATH" );
printf("%s\n", pathPtr);
=== Cambiar directorio actual ===
* **int chdir(const char *path)**, recibe el directorio donde cambiar mediante la ruta de este.
* **int fchdir(int fd)**, recibe el directorio donde cambiar como descriptor de fichero.
Estas dos funciones devuelven 0 si el cambio ha sido correcto y -1 si no.
=== Creación y eliminación ===
* **int mkdir(const char *pathname, mode_t mode)** crea un directorio en la ruta //pathname// en un modo //mode//, este es el número que representa sus permisos (generalmente 755).
* **int rmdir(const char *pathname)** elimina el directorio indicado en //pathname//.
=== Listado ===
Las funciones que necesitamos llamar para listar el contenido de un directorio son:
* **DIR *opendir(const char *name)**
* **dirent *readdir(DIR *dir)**
* **int closedir(DIR *dir)**
Abriremos y cerraremos un directorio con las funciones **opendir** y **closedir** respectivamente, la primera devuelve una variable DIR que utilizaremos para llamar a **readdir** que a su vez de vuelve una variable de la estructura **dirent** que tiene el siguiente formato:
struct dirent {
ino_t d_ino; // numero de i-node de la entrada de directorio
off_t d_off; // offset
wchar_t d_reclen; // longitud de este registro
char d_name[MAX_LONG_NAME+1] // nombre de esta entrada
}
La función **readdir** cada vez que se la llame devolverá un nombre de archivo distinto, recorrerá así el directorio; habrá pasado por todos cuando devuelva NULL.
==== Descriptores ====
Debido a que en Unix\Linux todo tiene una representación de fichero las funciones a más bajo nivel tienen que ver con estos. Los descriptores son la dirección en memoria necesaria para acceder a los recursos del sistema (ficheros, pantalla, sockets...) que tienen forma de ficheros. \\
Las funciones básicas para la apertura, creación, cerrado, escritura y lectura de descriptores son: **open**, **creat**, **close**, **read** y **write**.
===== Procesos =====
Los procesos dentro del SO tienen un identificador denominado ''pid'', este puede ser cogido mediante la función ''getpid()''; también podremos coger el pid del padre mediante la función ''getppid()''. \\
Podremos crear procesos mediante las funciones ''system'', ''exec'' o ''fork''.
==== system ====
Utiliza el shell para lanzar un comando. El proceso donde es llamado quedará en espera del retorno de la función que será el integer correspondiente al retorno del proceso\comando lanzado.
int main(int argc, char** argv) {
system("ls -la");
return 0;
}
==== fork ====
Duplica el proceso padre, copiando sus variables con sus respectivos valores en memoria, el proceso creado a partir de este será llamado proceso hijo y este proceso padre. El proceso hijo se ejecutará a partir de donde ha sido creado y tendrá un pid distinto al del padre. Aún así, la forma para diferenciarlos será el retorno de la función ''fork'' que en el proceso padre retornará el pid del proceso hijo, en cambio en el proceso hijo retornará un 0.
int main(int argc, char** argv) {
if (fork() == 0)
printf("Este es el proceso hijo\n");
else
printf("Este es el proceso padre\n");
return 0;
}
==== exec ====
Reemplaza el proceso actual por otro proceso, es decir, detiene la ejecución del proceso donde es llamado y lanza el comando indicado, dando a la ejecución de este el identificador del proceso actual. \\
Las variantes del exec son:
* Variantes que contienen **p** (''execvp'' o ''execlp'') buscan en el path actual el comando pasado (si el nombre de función no llevase p habría que indicar el path completo).
* Variantes que contienen **v** (''execvp'', ''execv'' o ''execve'') aceptan la lista de parámetros del comando como un array de strings.
* Variantes que contienen **l** (''execlp'', ''execl'' o ''execle'') aceptan la lista de parámetros del comando en formato de argumentos de C.
Una forma de trabajar con exec es, primero hacer un fork del proceso y luego lanzar un comando mediante ''exec''
int child_pid;
char* arg_list[] = {
"ls", // El primer argumento ha de ser el nombre del programa
"-l",
"/",
NULL // El último elemento de la lista ha de ser NULL
};
child_pid = fork ();
if (child_pid != 0)
return child_pid;
else {
execvp ("ls", arg_list);
fprintf (stderr, "Error en llamada de execvp\n");
abort ();
}
printf ("Done!\n");
return 0;
==== Otras funciones ====
=== Terminar un proceso ===
Para usar la función **kill** deberemos incluir ''signal.h'' y ''sys/types.h'', dicha función acaba un proceso hijo indicado. Recibe por parámetro el pid del proceso y una señal de finalización, esta puede ser:
* ''SIGINT'', es como si el sistema enviase un ctrl+c.
* ''SIGTERM'', el enviado por el comando del sistema ''kill''.
* ''SIGKILL'', la más poderosa ya que acaba el proceso sin contemplaciones.
* ''SIGABRT'', ''SIGBUS'', ''SIGSEGV'', y ''SIGFPE''.
=== Esperar un proceso ===
* La función **wait** bloquea el proceso donde es llamada esperando a que un proceso hijo cualquiera acabe. Se le ha de pasar una referencia a un integer donde colocará el retorno del proceso hijo.
* La función **waitpid** funciona igual que **wait** sólo que a esta se le pasa el pid del proceso hijo que esperará.
===== Comunicación entre procesos =====
==== Pipes ====
Una pipe es una conexión de la salida de un proceso a la entrada de otro. Esto es lo que ocurre cuando en la terminal hacemos:
command1 | command2
Internamente, lo que está pasando es:
* La entrada standard de //command1// viene del teclado.
* La salida standard de //command1// está conectada a la entrada de //command2//.
* La salida standard de //command2// es la salida standard.
=== popen y pclose ===
FILE *popen(const char *command, const char *open_mode);
int pclose(FILE *stream_to_close);
La forma más rápida de recibir los datos de un comando es mediante una pipe, y la forma más rápida de crear una pipe es utilizando las funciones **popen** y **pclose**. La primera (popen) devuelve un puntero a una variable ''FILE'', recibe el comando que se lanzará y la forma en la que es la pipe (de lectura ''r'' o de escritura ''w''), por lo que se podrá leer la salida con ''fread'' o escribir con ''fwrite''. La función pclose cerrará una pipe abierta por popen.
FILE *read_fp;
char buffer[BUFSIZ + 1];
int chars_read;
memset(buffer, '\0', sizeof(buffer));
read_fp = popen("uname -a", "r");
if (read_fp != NULL) {
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
if (chars_read > 0)
printf("Output was:-\n%s\n", buffer);
pclose(read_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
=== Función pipe ===
La función **pipe** crea una pipe a partir de un array de integers de tamaño 2, en este guardará dos descriptores de ficheros, uno de entrada (el primer entero del array, que es abierto como solo lectura) y otro de salida (el segundo entero del array, que es abierto como solo escritura). Una vez creado el pipe se podrán hacer lecturas y escrituras como si de un fichero se tratase. \\
Las pipes son perfectas para la comunicación entre procesos padre-hijo, se crea antes de llamar a fork y, como estos descriptores se comparten entre padre e hijo, una vez sea llamado uno podrá utilizar un descriptor para enviar y el otro para recibir. Es aconsejable que si los dos quisieran enviar y recibir se creasen dos pipes, y que se cerrasen en los procesos adecuados los descriptores que no se utilizarán. \\
Se utilizarán las funciones **write** en la pipe[1] y **read** en la pipe[2].
int tuberia [2];
int idson;
char buffer[BSIZE];
int bytesReaded = 0;
pipe(tuberia);
idson = fork();
if (idson == 0) {
// El hijo lee de la tuberia
close (tuberia[1]);
while ((bytesReaded = read(tuberia[0], buffer, BSIZE)) > 0)
write(1, buffer, bytesReaded);
close(tuberia[0]);
} else {
// El padre escribe por la tuberia
close (tuberia[0]);
strcpy(buffer, "hola caracola\n");
write(tuberia[1], buffer, strlen(buffer));
close(tuberia[1]);
waitpid(idson, NULL, 0);
}
==== Señales ====
===== Threads =====
Los threads se ejecutan dentro de un proceso que, a diferencia de estos, al crearse un thread no se copia su espacio de memoria sino que este es compartido con los demás threads y con el proceso principal. \\
Es el SO el que decide cuando un thread se ejecutará, es por ello que para la programación de estos no es correcto hacerlo pensando en que seguirán un orden concreto. \\
El tipo identificador de thread es denominado ''pthread'' este nombre viene de //POSIX thread//. \\ \\
Para utilizar los pthreads debemos **linkar la librería pthread** (desde línea de comandos utilizando ''-lpthread'') e incluir ''pthread.h''.
==== Recopilación ====
=== Tipos de datos ===
* pthread_t: manejador de thread.
* pthread_attr_t: atributos de un thread.
=== Funciones para el manejo de threads ===
* pthread_create(): crea un thread
* pthread_exit(): acaba el thread actual
* pthread_cancel(): cancela la ejecución de un thread
* pthread_join(): bloquea el thread actual hasta que otro acabe
* pthread_attr_init(): inicializa los atributos de un thread
* pthread_attr_setdetachstate()
* pthread_attr_getdetachstate()
* pthread_attr_destroy(): destruye los atributos de un thread
* pthread_kill(): envia una señal de finalización a un thread
=== Funciones de sincronización ===
* pthread_mutex_init(): Inicializa un mutex
* pthread_mutex_destroy()
* pthread_mutex_lock(): bloquea el mutex
* pthread_mutex_trylock(): bloquea el mutex sin bloquear el proceso
* pthread_mutex_unlock(): desbloquea el mutex
* pthread_cond_init()
* pthread_cond_destroy()
* pthread_cond_signal()
* pthread_cond_wait()
==== Manejo de threads ====
Para crear un thread utilizamos la función ''pthread_create'' pasándole los siguientes argumentos:
* thread, una referencia al identificador del thread.
* attr, una referencia al objeto que contiene los atributos, si queremos los valores por defecto asignaremos NULL.
* start_routine, la función en la que se ejecutará el thread, del estilo ''void *nombre (void *param)''.
* arg, el argumento (en forma de puntero) que se le pasa a la rutina del thread.
Un thread puede finalizar de cualquiera de las siguientes formas:
* La función acaba haciendo un return.
* Se hace una llamada a ''pthread_exit'' dentro del thread.
* El thread es cancelado por otro thread mediante ''pthread_cancel''.
* El proceso principal acaba.
Si el proceso principal acaba con un ''pthread_exit'' los threads vinculados a este no finalizarán y el proceso acabará cuando todos los threads acaben.
#include
#include
#define NUM_THREADS 5
void *PrintHello(void *threadid) {
int tid;
tid = (int)threadid;
printf("Hello World! It's me, thread #%d!\n", tid);
pthread_exit(NULL);
}
int main (int argc, char *argv[]) {
pthread_t threads[NUM_THREADS];
int rc, t;
for(t=0; t
La función ''pthread_join'' bloquea el thread actual hasta que el thread indicado no acabe (se puede impedir que un thread sea //joinable//, es decir que no pueda esperarse, si se crea como //detached//, para ello hay que hacerlo con los atributos de este). \\
La función ''pthread_self'' devuelve el identificador del thread actual.
==== Sincronización ====
Consiste en la realización de operaciones atómicas, esto es que no pueden ser interrumpidas por el cambio de thread en ejecución realizado por el procesador, para que los threads no accedan a recursos compartidos entre ellos a la vez lo que provocaría acciones erróneas. \\
Entre otras formas de sincronización existen: los mutexes, los semáforos, las variables condicionales...
=== Mutexes ===
Un mutex es como una puerta a una porción de memoria compartida por los threads con la cual hacen operaciones. Cuando un thread pasa por dicha puerta la cierra (si estubiese cerrada no podría pasar), operación de mutex ''lock'', cuando acaba las operaciones abre la puerta, ''unlock''. La metodología para utilizar los mutex es la siguiente:
- Crear e inicializar una variable mutex.
- Varios threads intentarán entrar al mutex.
- Sólo uno conseguirá adueñarse de este.
- El propietario realiza las acciones que desee.
- El propietario desbloquea el mutex.
- Otro thread accede al mutex y repite el proceso.
- Al final el mutex se destruye.
Un bloqueo del mutex que no bloquea el thread es una llamada a la función ''trylock''. \\
Cuando se inicializa un mutex este lo hace desbloqueado. \\
Las funciones que se utilizan son:
* pthtread_mutex_lock que se usa para adquirir y bloquear un mutex, si el mutex ya está bloqueado la llamada a esta función bloqueará el thread hasta que el mutex sea desbloqueado.
* pthread_mutex_unlock que desbloqueará el mutex. Es una función necesaria de llamar después de haber realizado los cálculos, Retornará error si el mutex ya estaba desbloqueado o si pertenecía a otro thread.
Podemos inicializar un mutex con los atributos por defecto utilizando la macro ''PTHREAD_MUTEX_INITIALIZER'':
pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;
===== Sockets =====
* [[code:tools#sockets|Explicación de sockets]].
===== X Window =====
Las interficies gráficas (GUI) en Linux, en su nivel más bajo, son manejadas por las X Window (o X11 o símplemente las X). Consiste en una arquitectura cliente-servidor, en la que el servidor realiza el enlace con el hardware (ratón, pantalla, teclado...) y el cliente recibe datos de este y muestra las aplicaciones, por lo que podemos decir que los sistemas de escritorios son los clientes de las X. Una de las ventajas de este modo de comunicación es que el cliente no tiene por qué estar en la misma máquina que el servidor, ni tampoco tiene por qué ser una pantalla ya que puede ser cualquier dispositivo gráfico. \\
El X Server gestiona la pantalla, la entrada por teclado y por ratón. Cuando el usuario hace click sobre una posición concreta de la pantalla el X Server envia los datos a la aplicación adecuada. Los mensajes que recibirá el X Server serán de redibujar ventanas. \\
La comunicación entre cliente-servidor se realiza mediante el XProtocol. \\
Xlib es la librería que se ha de agregar para trabajar a bajo nivel con las X, es tan de bajo nivel que únicamente se utiliza para dibujar sobre la ventana (por ejemplo un menú requeriría de un gran número de líneas de código). Es por eso que para desarrollar una aplicación generalmente se utiliza un Toolkit (GTK, [[fw:qt|Qt]]...). \\ \\
Para desarrollar con XLib deberemos linkar la librería **X11** e incluir **X11/Xlib.h** y **X11/Intrinsic.h** (para poder disponer de este último necesitarás añadir el paquete ''libxt-dev'').
==== Manejo de ventanas ====
Las funciones de XLib necesitan de una variable Display, esta equivale al enlace entre el cliente y el servidor, para conseguir una debemos llamar a la funcion **XOpenDisplay** la cual recibe el nombre de la pantalla. El nombre de la pantalla por defecto podemos encontrarlo en la variable de entono ''DISPLAY'' (aunque también podríamos pasarle ''NULL''):
Display *dpy = XOpenDisplay(getenv("DISPLAY"));
Para la creación de ventanas utilizaremos la funciones **XCreateWindow** (función general para crear nuevas ventanas) y la **XCreateSimpleWindow** (función que crea una subventana utilizando los valores por defecto de la aplicación padre). Estas funciones requieren de una variable Window (la ventana padre) y una variable Visual (almacena la estructura de color de las X). Para conseguirlas podemos utilizar las siguientes funciones:
Visual *vis = DefaultVisual(dpy,0); // Devuelve el Visual por defecto de la Dislay
Window w = DefaultRootWindow(dpy); // Devuelve la ventana raíz de la Display
Las funciones XCreateWindow y XCreateSimpleWindow devuelven una variable Window, esta no es más que un integer identificador de la ventana. Sus definiciones son:
Window XCreateWindow(display, parent, x, y, width, height, border_width, depth, class, visual, valuemask, attributes)
Display *display; // Display utilizado
Window parent; // Ventana padre
int x, y; // Posición de la ventana
unsigned int width, height; // Tamaño
unsigned int border_width; // Tamaño del borde
int depth; // Profundidad de color
unsigned int class; // Clase Visual
Visual *visual // La variable Visual
unsigned long valuemask; //
XSetWindowAttributes *attributes; // Atributos de la ventana
Window XCreateSimpleWindow(display, parent, x, y, width, height, border_width, border, background)
Display *display;
Window parent;
int x, y;
unsigned int width, height;
unsigned int border_width;
unsigned long border;
unsigned long background;
Tanto la clase Visual como la profundidad de color podemos coger las de por defecto mediante el comando ''CopyFromParent'':
Window w;
vis = DefaultVisual(dpy,0);
w = XCreateWindow(dpy,
DefaultRootWindow(dpy),
100, 100,
320, 200,
0,
CopyFromParent,
CopyFromParent,
vis,
0, NULL);
Una vez creada una ventana necesitaremos hacerla visible, para ello debemos mapearla mediante la función **XMapWindow**. \\
Podremos asignar el nombre (título) de la ventana con **XStoreName**. \\
Mientras llamemos a la función **XCheckWindowEvent** la ventana se mostrará.
main() {
Display* dpy = XOpenDisplay(NULL);
Visual *vis;
Window w;
vis = DefaultVisual(dpy,0);
w = XCreateWindow(dpy,
DefaultRootWindow(dpy),
100, 100,
320, 200,
0,
CopyFromParent,
CopyFromParent,
vis,
0, NULL);
XStoreName(dpy, w, "hello");
XMapWindow(dpy, w);
XCheckWindowEvent(dpy,w,0,0);
while(1) {}
}
La función para desmapear una ventana es **XUnmapWindow** y para destruir la ventana **XDestroyWindow**. Si quisiesemos destruir todas las sub-ventanas de una aplicación utilizaríamos **XDestroySubWindows**.
==== Gestión de eventos ====
Para interceptar eventos de en las X debemos indicar de qué eventos queremos se nos avise, para ello **XSelectInput** que recibe la Display, la Window y los siguientes eventos de la lista:
0, KeyPressMask, KeyReleaseMask, ButtonPressMask, ButtonReleaseMask, EnterWindowMask,
LeaveWindowMask, PointerMotionMask, PointerMotionHintMask, Button1MotionMask, Button2MotionMask,
Button3MotionMask, ButtonMotionMask, ExposureMask, VisibilityChangeMask, ResizeRedirectMask, FocusChangeMask
Para tratar los eventos tenemos las siguientes funciones:
* **XCheckWindowEvent**
* **XNextEvent**
* **XPeekEvent**
* **XWindowEvent**
* **XCheckWindowEvent**
* **XMaskEvent**
* **XCheckMaskEvent**
* **XCheckTypedEvent**
* **XCheckTypedWindowEvent**
==== Dibujando en una ventana ====
===== Librerías =====
==== Estáticas ====
==== Dinámicas ====
===== Otros =====
==== Insertar código ensamblador ====
=== Ensamblador inline ===
gcc permite insertar código ensamblador en medio de código C, este código es denominado ''inline assembly'' y, al ser en Linux, sigue la sitnaxis AT&T. \\
Para la inserción de código inline utilizaremos la función especial es ''__asm__'', su sintaxis es:
__asm__ ("instrucciones" : lista_salida : lista_entrada : lista_destruida);
Las //instrucciones// es un string donde está el código ensamblador. La //lista de salida// son los registros donde se guardará algún dato. La //lista de entrada// son los registros y variables que se utilizaran dentro del código. La //lista destruida// son los registros que han sido modificados por las instrucciones (para que gcc sepa cuales ha de reservar en un futuro). A las variables y registros utilizados en el código se les hace referencia a partir de un índice (que empieza por 0 (indicado con ''%'') y cuenta a partir de la lista de salida y se les une las demás).
int i = 100;
__asm__ ("movl %0, %%eax" : : "g" (i));
En este caso ''%0'' corresponde al valor de entrada ''i''; el acceso al registro ''eax'' se hace mediante ''%%'' (esto se ha de hacer para cada registro); lo que el código hace es asignar el valor de ''i'' (100) a ''eax''. \\
La directiva ''"g"(i)'' indica al compilador donde almacenar el parámetro. "r" le permitiría guardar la variable en cualquier registro qeu no esté en uso. "a" en ax\eax, "b" en bx\ebx, "c" en cx\ecx, "d" en dx\edx, "D" en di\edi, "S" en si/esi, etc.
void *memcpy( void *dest, const void *src, unsigned int n)
{
__asm__("cld ; rep ; movsb": : "c" ((unsigned int) n), "S" (src), "D" (dest));
return dest;
}
También podemos utilizar la directiva ''volatile'' que optimiza el código.
int i=0, j=1;
__asm__ __volatile__(" \
pushl %%eax \n \
movl %0, %%eax \n \
addl %1, %%eax \n \
movl %%eax, %0 \n \
popl %%eax "
:
: "g" (i), "g" (j)
);
Para indicar valores constantes utilizaremos ''$'' delante del valor. \\
Las variables de salida han de ser precedidas por ''=''.
int i=0, j=1, k=0;
__asm__ __volatile__(" \
pushl %%eax \n \
movl %1, %%eax \n \
addl %2, %%eax \n \
movl %%eax, %0 \n \
popl %%eax"
: "=g" (k)
: "g" (i), "g" (j)
);
/* k = i + j; */
Si quisieramos decir que después de la llamada al código ensamblador no se toque el ''ecx'' haríamos:
__asm__ __volatile__ ("..." : : : "ecx");
Podemos insertar etiquetas locales dentro del ensamblador en línea, la llamada a estas debe terminar por una b o una f según si dicha etiqueta esta después o antes de la instrucción de salto:
__asm__ __volatile__("
0: \n \
...
jmp 0b \n \
...
jmp 1f \n \
...
1:\n \
..."
);
===== Herramientas =====
==== gcc y g++ ====
Son los compiladores de C y C++ (gcc y g++ respectivamente) en linux.
* Para compilar un archivo .c haríamos: ''gcc hola.c''.
* Para compilar un archivo .c e indicar el nombre del resultado: ''gcc -o hola hola.c''
* El parámetro ''-c'' hace que no salga un ejecutable sino uno con código objeto.
* Para realizar el linkage de librerías estáticas (ficheros .lib) en tu proyecto tendrás que utilizar el parámetro ''-l'' seguido del nombre de la librería, pero seguido sin espacios; por ejemplo, tenemos la librería ''ddraw.lib'' o ''gdi32.lib'' haremos ''-lddraw'' o ''-lgdi32''.
* El parámetro ''-I'' agrega un directorio de cabeceras (archivos .h): ''-I/usr/X11R6/include/''
* El parámetro ''-s'' saca el archivo en ensamblador del compilado.
* http://iie.fing.edu.uy/~vagonbar/gcc-make/gcc.htm
==== Debug ====
Si compilamos un código con el parámetro ''-g'' podremos ejecutar el comand ''gdb '' y nos aparecerá una consola de debugación, los comandos que pueden sernos útiles son:
* **run** para iniciar el programa.
* **b file:line**, o ''break file:line'', para poner un breakpoint en una línea del programa.
* **delete**, elimina todos los breakpoints.
* **kill** para detener la ejecución del programa.
* **c** o **continue** para continuar la ejecución de un programa parado.
* **quit** para salir de gdb.
* **n** ejecuta la siguiente instrucción.
* **s** ejecuta la siguiente instrucción entrando en funciones.
* **print variable** muestra el valor de la variable indicada.
* **list** muestra las líneas que rodean la actual.
* **info r** muestra los valores de los registros.
* **info stack** muestra la pila.
* **info line** muestra información sobre la línea actual.
* **info locals** muestra las variables locales.
* **source ** ejecuta un script de gdb.
* **add-symbol-file ** agrega otro fichero al debug.
===== Notas =====
==== Links ====
* [[https://computing.llnl.gov/tutorials/pthreads|Tutorial de POSIX threads]]
==== Documentos ====
* {{highlevel:c:programaciongnulinux.pdf|Programación en GNU Linux}}
* {{highlevel:c:gnu_c_library.pdf|Documentación de la librería GNU C}}
* {{highlevel:c:gdb-refcard-a4.pdf|Comandos del gdb}}