Skip to Content

Depurar C++

Comprobar accesos fuera de rango

Escribir en un índice de arreglo fuera de rango se conoce como desbordamiento de buffer . C++ puede o no producir un error de ejecución ante un desbordamiento de buffer. Por ejemplo, el siguiente código da un error de ejecución en ide.usaco.guide , pero imprime 4 en mi computadora.

#include <iostream> #include <vector> using namespace std; int main() { vector<int> invalid_vec{1}; vector<int> valid_vec{1234}; cout << valid_vec[0] << "\n"; // outputs 1234 for (int i = 0; i < 10; i++) { invalid_vec[i] = i; // may or may not error } cout << valid_vec[0] << "\n"; // may output 4 }

Para asegurarse de que se lance un error al acceder a un índice fuera de rango, se puede usar vector::at  en lugar de vector::operator[]  así:

#include <iostream> #include <vector> using namespace std; int main() { vector<int> invalid_vec{1}; vector<int> valid_vec{1234}; cout << valid_vec.at(0) << "\n"; // outputs 1234 for (int i = 0; i < 10; i++) { invalid_vec.at(i) = i; // throws std::out_of_range } cout << valid_vec.at(0) << "\n"; }

C++ ahora va a comprobar los límites al acceder a los vectores y va a producir la siguiente salida:

1234 terminate called after throwing an instance of 'std::out_of_range' what(): vector::_M_range_check: __n (which is 1) >= this->size() (which is 1) 1 zsh: abort ./$1 $@[2,-1]

Orden de evaluación no especificado

Aquí hay un comportamiento inesperado que se puede encontrar al intentar crear un trie  o un árbol de segmentos persistente.

#include <bits/stdc++.h> using namespace std; vector<int> res{-1}; int add_element() { res.push_back(-1); return res.size() - 1; // index of added element } int main() { for (int i = 0; i < 5; ++i) { res[i] = add_element(); cout << i << " " << res[i] << "\n"; } }

Compilar y ejecutar el código de arriba con -std=c++17 da la salida esperada:

0 1 1 2 2 3 3 4 4 5

Pero compilar y ejecutar con -std=c++14 da algo inesperado:

0 -1 1 -1 2 3 3 -1 4 5

Tanto con -std=c++17 como con -std=c++14, se produce la salida esperada si el resultado de add_element() se guarda en una variable temporal.

#include <bits/stdc++.h> using namespace std; vector<int> res{-1}; int add_element() { res.push_back(-1); return res.size() - 1; // index of added element } int main() { for (int i = 0; i < 10; ++i) { int tmp = add_element(); res[i] = tmp; cout << i << " " << res[i] << "\n"; } }

El problema es que res[i] = add_element(); solo funciona si add_element() se evalúa antes que res[i]. Si res[i] se evalúa primero, y después add_element() hace que se reasigne la memoria de res, entonces res[i] queda invalidado. El orden en que se evalúan res[i] y add_element() no está especificado (al menos antes de C++17).

Véase este  post de StackOverflow para una discusión de por qué es así (aquí  hay un problema similar).

Opciones de warning de GCC

En esta sección y la siguiente veremos opciones que se pueden agregar al comando de compilación de g++ para ayudar a depurar.

Recursos
FuenteRecursoNotas
CFandreyv - Catching Silly Mistakes with GCC

Incluye todas las opciones de abajo.

GCCGCC Warning Options

Documentación oficial de las opciones de abajo.

Estas son las opciones de warning que usa Ben:

-Wall -Wextra -Wshadow -Wconversion -Wfloat-equal -Wduplicated-cond -Wlogical-op

Damos ejemplos de algunas de ellas abajo.

-Wall

Activa muchas (pero no todas) las opciones de warning, incluyendo -Wuninitialized y -Wunused-variable.

#include <bits/stdc++.h> using namespace std; int main() { int x; cout << x; }

Salida de la compilación:

main.cpp: In function ‘int main()’: main.cpp:6:10: warning: ‘x’ is used uninitialized in this function [-Wuninitialized] 6 | cout << x; | ^

-Wextra

Activa algunas opciones de warning que -Wall no activa, como -Wmissing-field-initializers.

#include <bits/stdc++.h> using namespace std; struct s { int f, g, h; }; int main() { s x = {3, 4}; }

Salida de la compilación:

main.cpp: In function ‘int main()’: main.cpp:7:18: warning: missing initializer for member ‘s::h’ [-Wmissing-field-initializers] 7 | s x = { 3, 4 }; | ^

-Wconversion

Advierte sobre conversiones implícitas que pueden alterar un valor.

#include <bits/stdc++.h> using namespace std; int main() { double x = 5.5; int y = x; cout << y; }

Salida de la compilación:

main.cpp: In function ‘int main()’: main.cpp:6:13: warning: conversion from ‘double’ to ‘int’ may change value [-Wfloat-conversion] 6 | int y = x; | ^

-Wshadow

Recursos
FuenteRecursoNotas
LCPP6.5 - Variable Shadowing (Name Hiding)
#include <bits/stdc++.h> using namespace std; int x; int main() { int x = 5; cout << x; }

Salida de la compilación:

main.cpp: In function ‘int main()’: main.cpp:7:6: warning: declaration of ‘x’ shadows a global declaration [-Wshadow] 7 | int x = 5; | ^ main.cpp:4:5: note: shadowed declaration is here 4 | int x; | ^

-Wfloat-equal

Advierte si se usan valores de punto flotante en comparaciones de igualdad.

#include <bits/stdc++.h> using namespace std; int main() { double x = 1.0 / 49 * 49; cout << (x == 1.0); // 0 }

Salida de la compilación:

main.cpp: In function ‘int main()’: main.cpp:6:16: warning: comparing floating-point with ‘==’ or ‘!=’ is unsafe [-Wfloat-equal] 6 | cout << (x == 1.0); | ~~^~~~~~

Opciones de depuración de GCC

-fsanitize=undefined

Ejemplo: arreglo fuera de rango

Sin -fsanitize=undefined, este programa se ejecuta con éxito e imprime basura:

#include <bits/stdc++.h> using namespace std; int main() { int v[5]; cout << v[5] << endl; // may output an arbitrary integer }

Con -fsanitize=undefined, este programa igual se ejecuta con éxito, pero se imprime el siguiente error de ejecución al error estándar:

main.cpp:6:13: runtime error: index 5 out of bounds for type 'int [5]' main.cpp:6:13: runtime error: load of address 0x7ffc4efaf2d4 with insufficient space for an object of type 'int' 0x7ffc4efaf2d4: note: pointer points here 11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f0 15 40 00 00 00 00 00 00 00 00 00 00 00 00 00 ^

Ejemplo: vector fuera de rango

El código de abajo produce un segmentation fault:

#include <bits/stdc++.h> using namespace std; int main() { vector<int> v; cout << v[-1] << endl; }

Salida:

/tmp/program/run.sh: line 1: 71 Segmentation fault ./prog Command exited with non-zero status 139

Con -fsanitize=undefined, se produce un mensaje de error un poco más informativo:

/opt/rh/devtoolset-10/root/usr/include/c++/10/bits/stl_vector.h:1046:34: runtime error: applying non-zero offset 18446744073709551612 to null pointer /tmp/program/run.sh: line 1: 1845 Segmentation fault ./prog Command exited with non-zero status 139

Ejemplo: desbordamiento de enteros

#include <bits/stdc++.h> using namespace std; int main() { int x = 1 << 30; cout << x + x << endl; }

Con -fsanitize=undefined, este programa igual se ejecuta con éxito, pero se imprime el siguiente error de ejecución al error estándar:

main.cpp:6:14: runtime error: signed integer overflow: 1073741824 * 2 cannot be represented in type 'int'

Ejemplo: detectar varios errores

Por defecto, el sanitizer de comportamiento indefinido intenta continuar después de detectar un error. Por ejemplo, el siguiente programa con -fsanitize=undefined produce varios errores de ejecución:

#include <bits/stdc++.h> using namespace std; int main() { cout << (1 << 32) << endl; cout << (1 << 32) << endl; cout << (1 << 32) << endl; }

Error estándar:

main.cpp:5:13: runtime error: shift exponent 32 is too large for 32-bit type 'int' main.cpp:6:13: runtime error: shift exponent 32 is too large for 32-bit type 'int' main.cpp:7:13: runtime error: shift exponent 32 is too large for 32-bit type 'int'

Para desactivar este comportamiento y salir después del primer error detectado, se puede usar -fsanitize=undefined con -fno-sanitize-recover.

Error estándar:

main.cpp:5:13: runtime error: shift exponent 32 is too large for 32-bit type 'int' Command exited with non-zero status 1

-fsanitize=address

Recursos
FuenteRecursoNotas
GCC3.10 - Options for Debugging Your Program

documentación de -g

Ejemplo: vector fuera de rango

Recordar que este ejemplo de la subsección anterior da un segmentation fault:

#include <bits/stdc++.h> using namespace std; int main() { vector<int> v; cout << v[-1] << endl; }

Compilar con -fsanitize=address da:

Error estándar
AddressSanitizer:DEADLYSIGNAL ================================================================= ==5669==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x000000401290 bp 0x7fffc91c63b0 sp 0x7fffc91c6350 T0) ==5669==The signal is caused by a READ memory access. ==5669==Hint: this fault was caused by a dereference of a high value address (see register values below). Dissassemble the provided pc to learn which register was used. #0 0x401290 in main (/tmp/program/prog+0x401290) #1 0x14ef9fd10139 in __libc_start_main (/lib64/libc.so.6+0x21139) #2 0x401559 in _start (/tmp/program/prog+0x401559) AddressSanitizer can not provide additional info. SUMMARY: AddressSanitizer: SEGV (/tmp/program/prog+0x401290) in main ==5669==ABORTING Command exited with non-zero status 1

Para información más útil, además hay que compilar con el flag -g, que genera un archivo con información de depuración basada en la numeración de líneas del programa.

Error estándar

Se puede ver que el error ocurrió en la línea 6.

AddressSanitizer:DEADLYSIGNAL ================================================================= ==2149==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x000000401290 bp 0x7ffd3e426ae0 sp 0x7ffd3e426a80 T0) ==2149==The signal is caused by a READ memory access. ==2149==Hint: this fault was caused by a dereference of a high value address (see register values below). Dissassemble the provided pc to learn which register was used. #0 0x401290 in main /tmp/out/main.cpp:6 #1 0x14a33b4b3139 in __libc_start_main (/lib64/libc.so.6+0x21139) #2 0x401559 in _start (/tmp/program/prog+0x401559) AddressSanitizer can not provide additional info. SUMMARY: AddressSanitizer: SEGV /tmp/out/main.cpp:6 in main ==2149==ABORTING Command exited with non-zero status 1

Ejemplo: arreglo fuera de rango

#include <bits/stdc++.h> using namespace std; int main() { int v[5]; cout << v[5] << endl; }

Con -fsanitize=address -g:

Error estándar
================================================================= ==2488==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffa870c164 at pc 0x00000040135b bp 0x7fffa870c120 sp 0x7fffa870c110 READ of size 4 at 0x7fffa870c164 thread T0 #0 0x40135a in main /tmp/out/main.cpp:6 #1 0x1523eeb67139 in __libc_start_main (/lib64/libc.so.6+0x21139) #2 0x401479 in _start (/tmp/program/prog+0x401479) Address 0x7fffa870c164 is located in stack of thread T0 at offset 52 in frame #0 0x40117f in main /tmp/out/main.cpp:4 This frame has 1 object(s): [32, 52) 'v' (line 5) <== Memory access at offset 52 overflows this variable HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-buffer-overflow /tmp/out/main.cpp:6 in main Shadow bytes around the buggy address: 0x1000750d97d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x1000750d97e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x1000750d97f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x1000750d9800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x1000750d9810: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x1000750d9820: 00 00 00 00 00 00 f1 f1 f1 f1 00 00[04]f3 f3 f3 0x1000750d9830: f3 f3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x1000750d9840: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x1000750d9850: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x1000750d9860: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x1000750d9870: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb Shadow gap: cc ==2488==ABORTING Command exited with non-zero status 1

-D_GLIBCXX_DEBUG

Esto activa el modo debug, que reemplaza cada contenedor de la STL por su contenedor de debug correspondiente.

Recursos
FuenteRecursoNotas
GCCUsing Debug Mode

documentación de -D_GLIBCXX_DEBUG

Recordar que el siguiente programa da un segmentation fault.

#include <bits/stdc++.h> using namespace std; int main() { vector<int> v; cout << v[-1] << endl; }

Con -D_GLIBCXX_DEBUG se produce la siguiente salida:

Debug
/usr/local/Cellar/gcc/9.2.0_1/include/c++/9.2.0/debug/vector:427: In function: std::__debug::vector<_Tp, _Allocator>::reference std::__debug::vector<_Tp, _Allocator>::operator[](std::__debug::vector<_Tp, _Allocator>::size_type) [with _Tp = int; _Allocator = std::allocator<int>; std::__debug::vector<_Tp, _Allocator>::reference = int&; std::__debug::vector<_Tp, _Allocator>::size_type = long unsigned int] Error: attempt to subscript container with out-of-bounds index -1, but container only holds 0 elements. Objects involved in the operation: sequence "this" @ 0x0x7ffee2503a50 { type = std::__debug::vector<int, std::allocator<int> >; } zsh: abort ./prog

Usar el depurador LLDB

Recursos
FuenteRecursoNotas
LLVMGDB to LLDB Command Map

Recordar el ejemplo de la sección Comprobar accesos fuera de rango donde la salida no contenía el número de línea en la que ocurrió el error de ejecución. Abajo mostramos cómo usar lldb para imprimir el número de línea. Supongamos que el código fuente C++ se llama prog.cpp y el ejecutable se llama prog.

  1. Agregar -g al comando de compilación y compilar.
  2. Iniciar el modo debug de prog con lldb prog.
  3. Empezar a ejecutar el programa con r.
  4. Mostrar el stack backtrace con bt.
Salida

En la salida de abajo se ve que el error de ejecución ocurrió en la línea 10.

benq:CF % g++-12 prog.cpp -g -o prog && lldb prog (lldb) target create "prog" Current executable set to '/Users/benq/Desktop/CF/prog' (x86_64). (lldb) r Process 86225 launched: '/Users/benq/Desktop/CF/prog' (x86_64) 1234 terminate called after throwing an instance of 'std::out_of_range' what(): vector::_M_range_check: __n (which is 1) >= this->size() (which is 1) Process 86225 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT frame #0: 0x00007ff80e01300e libsystem_kernel.dylib`__pthread_kill + 10 libsystem_kernel.dylib`__pthread_kill: -> 0x7ff80e01300e <+10>: jae 0x7ff80e013018 ; <+20> 0x7ff80e013010 <+12>: movq %rax, %rdi 0x7ff80e013013 <+15>: jmp 0x7ff80e00d1c5 ; cerror_nocancel 0x7ff80e013018 <+20>: retq Target 0: (prog) stopped. (lldb) bt * thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT * frame #0: 0x00007ff80e01300e libsystem_kernel.dylib`__pthread_kill + 10 frame #1: 0x00007ff80e0491ff libsystem_pthread.dylib`pthread_kill + 263 frame #2: 0x00007ff80df94d24 libsystem_c.dylib`abort + 123 frame #3: 0x0000000100655871 libstdc++.6.dylib`__gnu_cxx::__verbose_terminate_handler() (.cold) + 92 frame #4: 0x0000000100556456 libstdc++.6.dylib`__cxxabiv1::__terminate(void (*)()) + 6 frame #5: 0x0000000100556473 libstdc++.6.dylib`std::terminate() + 19 frame #6: 0x00000001005565f3 libstdc++.6.dylib`__cxa_throw + 67 frame #7: 0x0000000100658803 libstdc++.6.dylib`std::__throw_out_of_range_fmt(char const*, ...) (.cold) + 22 frame #8: 0x0000000100002897 prog`std::vector<int, std::allocator<int> >::_M_range_check(this=0x00007ff7bfeff3d0, __n=1) const at stl_vector.h:1153:28 frame #9: 0x0000000100002629 prog`std::vector<int, std::allocator<int> >::at(this=0x00007ff7bfeff3d0, __n=1) at stl_vector.h:1175:16 frame #10: 0x0000000100002421 prog`main at prog.cpp:10:17 frame #11: 0x000000010001552e dyld`start + 462 (lldb)