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 5Pero compilar y ejecutar con -std=c++14 da algo inesperado:
0 -1
1 -1
2 3
3 -1
4 5Tanto 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.
| Fuente | Recurso | Notas |
|---|---|---|
| CF | andreyv - Catching Silly Mistakes with GCC | Incluye todas las opciones de abajo. |
| GCC | GCC 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-opDamos 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
| Fuente | Recurso | Notas |
|---|---|---|
| LCPP | 6.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 139Con -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 139Ejemplo: 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
| Fuente | Recurso | Notas |
|---|---|---|
| GCC | 3.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 1Para 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 1Ejemplo: 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.
| Fuente | Recurso | Notas |
|---|---|---|
| GCC | Using 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 ./progUsar el depurador LLDB
| Fuente | Recurso | Notas |
|---|---|---|
| LLVM | GDB 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.
- Agregar
-gal comando de compilación y compilar. - Iniciar el modo debug de
progconlldb prog. - Empezar a ejecutar el programa con
r. - 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)