¿Cómo puedo interrumpir los informes UBSan en gdb y continuar?

11 minutos de lectura

avatar de usuario
Lekensteyn

Las versiones recientes de GCC y Clang cuentan con Undefined Behavior Sanitizer (UBSan), que es un indicador de compilación (-fsanitize=undefined) que agrega código de instrumentación en tiempo de ejecución. En los errores, se muestra una advertencia como esta:

package-ber.c:1917:23: error de tiempo de ejecución: el desplazamiento a la izquierda de 54645397829836991 por 8 lugares no se puede representar en el tipo ‘long int’

Ahora me gustaría depurar esto y obtener un descanso de depuración en dicha línea. Para Address Sanitizer (ASAN) hay ASAN_OPTIONS=abort_on_error=1 lo que da como resultado un error fatal que es detectable. La única opción UBSan que parece utilizable es UBSAN_OPTIONS=print_stacktrace=1 lo que da como resultado un volcado de seguimiento de llamadas para los informes. Sin embargo, esto no me permite inspeccionar las variables locales y luego continuar con el programa. Uso de -fsanitize-undefined-trap-on-error por lo tanto no es posible.

¿Cómo debo ingresar gdb en los informes UBSan? Tiempo break __sanitizer::SharedPrintfCode parece funcionar, el nombre parece bastante interno.

  • Creo que hasta que se implemente y documente una API, una buena manera de recibir una llamada a la biblioteca de tiempo de ejecución de UBSan con la intención de continuar con su programa es hacer rbreak ^__ubsan_handle_, que detendrá la ejecución antes de que la biblioteca se adentre en el territorio de C++ en el que asigna instancias de la clase Diag. Explora todo lo que quieras, luego escribe return para continuar su programa.

    –Mark Plotnick

    15 de junio de 2015 a las 19:03


  • Para futura referencia, abort_on_error parece no implementado para UBSAN. Usa esto en su lugar: UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1

    – Lekenstein

    7 julio 2015 a las 17:08

  • Ver también ¿Cómo interrumpir el depurador cuando -fsanitize=undefined imprime algo? en la lista de correo de Clang Dev.

    – jww

    16 de julio de 2015 a las 11:53

  • ¿Qué versión de GCC/Clang estás usando, precisamente?

    – Iwillnotexist Idonotexist

    16/07/2015 a las 21:01


  • @IwillnotexistIdonotexist Versiones muy recientes en Arch Linux, GCC 5.1.0, Clang 3.6.1.

    – Lekenstein

    16 de julio de 2015 a las 23:51

avatar de usuario
Lekensteyn

Si bien interrumpir las funciones de detección (según lo descrito por @Mark Plotnick y @Iwillnotexist Idonotexist) es una opción, un mejor enfoque es interrumpir las funciones que informan estos problemas después de la detección. Este enfoque también se usa para ASAN donde uno rompería en __asan_report_error.

Resumen: puede detenerse en un informe ubsan a través de un punto de interrupción en __ubsan::ScopedReport::~ScopedReport o __ubsan::Diag::~Diag. Sin embargo, estos son detalles de implementación privados que podrían cambiar en el futuro. Probado con GCC 4.9, 5.1.0, 5.2.0 y Clang 3.3, 3.4, 3.6.2.

Para GCC 4.9.2 desde ppa:ubuntu-toolchain-r/pruebanecesitas libubsan0-dbg para que los puntos de interrupción anteriores estén disponibles. Ubuntu 14.04 con Clang 3.3 y 3.4 no es compatible con __ubsan::ScopedReport::~ScopedReport puntos de interrupción, por lo que solo puede interrumpir antes de imprimir el mensaje usando __ubsan::Diag::~Diag.

Ejemplo de código fuente con errores y una sesión gdb:

$ cat undef.c
int main(void) { return 1 << 1000; }
$ clang --version
clang version 3.6.2 (tags/RELEASE_362/final)
Target: x86_64-unknown-linux-gnu
Thread model: posix
$ clang -w -fsanitize=undefined undef.c -g
$ gdb -q -ex break\ __ubsan::ScopedReport::~ScopedReport -ex r ./a.out 
Reading symbols from ./a.out...done.
Breakpoint 1 at 0x428fb0
Starting program: ./a.out 
undef.c:1:27: runtime error: shift exponent 1000 is too large for 32-bit type 'int'

Breakpoint 1, 0x0000000000428fb0 in __ubsan::ScopedReport::~ScopedReport() ()
(gdb) bt
#0  0x0000000000428fb0 in __ubsan::ScopedReport::~ScopedReport() ()
#1  0x000000000042affb in handleShiftOutOfBoundsImpl(__ubsan::ShiftOutOfBoundsData*, unsigned long, unsigned long, __ubsan::ReportOptions) ()
#2  0x000000000042a952 in __ubsan_handle_shift_out_of_bounds ()
#3  0x000000000042d057 in main () at undef.c:1

A continuación se presenta un análisis detallado. Tenga en cuenta que tanto ASAN como ubsan se originan en un proyecto LLVM, compilador-rt. Esto es utilizado por Clang y también termina en GCC. Los enlaces en las siguientes secciones apuntan al código del proyecto compiler-rt, versión 3.6.

ASAN ha hecho su interna __asan_report_error parte de interfaz pública documentada. Esta función se llama cada vez que se detecta una violación, su flujo continúa en lib/asan/asan_report.c:938:

void __asan_report_error(uptr pc, uptr bp, uptr sp, uptr addr, int is_write,
                         uptr access_size) {
  // Determine the error type.
  const char *bug_descr = "unknown-crash";
  ...

  ReportData report = { pc, sp, bp, addr, (bool)is_write, access_size,
                        bug_descr };
  ScopedInErrorReport in_report(&report);

  Decorator d;
  Printf("%s", d.Warning());
  Report("ERROR: AddressSanitizer: %s on address "
             "%p at pc %p bp %p sp %p\n",
             bug_descr, (void*)addr, pc, bp, sp);
  Printf("%s", d.EndWarning());

  u32 curr_tid = GetCurrentTidOrInvalid();
  char tname[128];
  Printf("%s%s of size %zu at %p thread T%d%s%s\n",
         d.Access(),
         access_size ? (is_write ? "WRITE" : "READ") : "ACCESS",
         access_size, (void*)addr, curr_tid,
         ThreadNameWithParenthesis(curr_tid, tname, sizeof(tname)),
         d.EndAccess());

  GET_STACK_TRACE_FATAL(pc, bp);
  stack.Print();

  DescribeAddress(addr, access_size);
  ReportErrorSummary(bug_descr, &stack);
  PrintShadowMemoryForAddress(addr);
}

ubsan, por otro lado, no tiene una interfaz pública, pero su implementación actual también es mucho más simple y limitada (menos opciones). En caso de errores, se puede imprimir un stacktrace cuando el UBSAN_OPTIONS=print_stacktrace=1 se establece la variable de entorno. Por lo tanto, al buscar el código fuente para print_stacktracese encuentra la función Tal vezImprimirStackTrace que se llama aunque el ScopedReport destructor:

ScopedReport::~ScopedReport() {
  MaybePrintStackTrace(Opts.pc, Opts.bp);
  MaybeReportErrorSummary(SummaryLoc);
  CommonSanitizerReportMutex.Unlock();
  if (Opts.DieAfterReport || flags()->halt_on_error)
    Die();
}

Como puede ver, hay un método para matar el programa en caso de errores, pero desafortunadamente no hay un mecanismo incorporado para activar una trampa del depurador. Entonces busquemos un punto de interrupción adecuado.

El comando GDB info functions <function name> permitió identificar MaybePrintStackTrace como función en la que se puede establecer un punto de interrupción. Ejecución de info functions ScopedReport::~ScopedReport dio otra función: __ubsan::ScopedReport::~ScopedReport. Si ninguna de estas funciones parece estar disponible (incluso con los símbolos de depuración instalados), puede intentar info functions ubsan o info functions sanitizer para obtener todas las funciones relacionadas con (UndefinedBehavior) Sanitizer.

  • +1. Me viene a la mente que su pregunta expone una necesidad, y esta necesidad se cumpliría si los desinfectantes tuvieran una fuerza-no-inline, extern-enlace, void-devolver una función interna vacía que un depurador podría interrumpir, y que esta función se llame con argumentos útiles para el depurador al informar locura. Alguna cosa similar a la interfaz de registro JIT.

    – Iwillnotexist Idonotexist

    23 de julio de 2015 a las 19:03


  • Esta respuesta sería más útil si la respuesta a la pregunta real se hiciera más prominente.

    – xaxxon

    9 de marzo de 2018 a las 6:08


  • @xaxxon Estoy abierto a sugerencias, ¿podría aclarar qué parte necesita mejorar?

    – Lekenstein

    9 de marzo de 2018 a las 12:55

  • pasos específicos claros para lo que no se intercala con un montón de cosas no relacionadas sobre asan.

    – xaxxon

    9 de marzo de 2018 a las 18:26

  • @xaxxon La primera parte contiene toda la información que necesita para establecer un descanso en los informes UBSAN. La segunda parte es solo informativa, puede ignorarla si no necesita entender cómo funcionan las cosas. Si la implementación interna cambia en el futuro, la segunda parte debería ayudar al lector a descubrir cómo adaptarse. ASAN y UBSan están estrechamente relacionados, así que pensé que tendría sentido comparar ambos.

    – Lekenstein

    11 de marzo de 2018 a las 15:41

avatar de usuario
Noexistiré Idonotexist

Como señala @Mark Plotnick, la forma de hacerlo es romper el punto de UBSan manipuladores.

UBSan tiene una serie de controladores, o puntos de entrada de funciones mágicas, que se llaman para un comportamiento indefinido. El compilador instrumenta el código inyectando controles según corresponda; Si el código de verificación detecta UB, llama a estos controladores. Todos ellos comienzan con __ubsan_handle_ y se definen en libsanitizer/ubsan/ubsan_handlers.h. Aquí está un enlace a la copia de GCC de ubsan_handlers.h.

Aquí están los bits relevantes del encabezado UBSan (punto de interrupción en cualquiera de estos):

#define UNRECOVERABLE(checkname, ...) \
  extern "C" SANITIZER_INTERFACE_ATTRIBUTE NORETURN \
    void __ubsan_handle_ ## checkname( __VA_ARGS__ );

#define RECOVERABLE(checkname, ...) \
  extern "C" SANITIZER_INTERFACE_ATTRIBUTE \
    void __ubsan_handle_ ## checkname( __VA_ARGS__ ); \
  extern "C" SANITIZER_INTERFACE_ATTRIBUTE NORETURN \
    void __ubsan_handle_ ## checkname ## _abort( __VA_ARGS__ );

/// \brief Handle a runtime type check failure, caused by either a misaligned
/// pointer, a null pointer, or a pointer to insufficient storage for the
/// type.
RECOVERABLE(type_mismatch, TypeMismatchData *Data, ValueHandle Pointer)

/// \brief Handle an integer addition overflow.
RECOVERABLE(add_overflow, OverflowData *Data, ValueHandle LHS, ValueHandle RHS)

/// \brief Handle an integer subtraction overflow.
RECOVERABLE(sub_overflow, OverflowData *Data, ValueHandle LHS, ValueHandle RHS)

/// \brief Handle an integer multiplication overflow.
RECOVERABLE(mul_overflow, OverflowData *Data, ValueHandle LHS, ValueHandle RHS)

/// \brief Handle a signed integer overflow for a unary negate operator.
RECOVERABLE(negate_overflow, OverflowData *Data, ValueHandle OldVal)

/// \brief Handle an INT_MIN/-1 overflow or division by zero.
RECOVERABLE(divrem_overflow, OverflowData *Data,
            ValueHandle LHS, ValueHandle RHS)

/// \brief Handle a shift where the RHS is out of bounds or a left shift where
/// the LHS is negative or overflows.
RECOVERABLE(shift_out_of_bounds, ShiftOutOfBoundsData *Data,
            ValueHandle LHS, ValueHandle RHS)

/// \brief Handle an array index out of bounds error.
RECOVERABLE(out_of_bounds, OutOfBoundsData *Data, ValueHandle Index)

/// \brief Handle a __builtin_unreachable which is reached.
UNRECOVERABLE(builtin_unreachable, UnreachableData *Data)
/// \brief Handle reaching the end of a value-returning function.
UNRECOVERABLE(missing_return, UnreachableData *Data)

/// \brief Handle a VLA with a non-positive bound.
RECOVERABLE(vla_bound_not_positive, VLABoundData *Data, ValueHandle Bound)

/// \brief Handle overflow in a conversion to or from a floating-point type.
RECOVERABLE(float_cast_overflow, FloatCastOverflowData *Data, ValueHandle From)

/// \brief Handle a load of an invalid value for the type.
RECOVERABLE(load_invalid_value, InvalidValueData *Data, ValueHandle Val)

RECOVERABLE(function_type_mismatch,
            FunctionTypeMismatchData *Data,
            ValueHandle Val)

/// \brief Handle returning null from function with returns_nonnull attribute.
RECOVERABLE(nonnull_return, NonNullReturnData *Data)

/// \brief Handle passing null pointer to function with nonnull attribute.
RECOVERABLE(nonnull_arg, NonNullArgData *Data)

ASan es aún más fácil. si miras adentro libsanitizer/include/sanitizer/asan_interface.hque debes navegar aquíse puede leer un regalo muerto de un comentario:

  // This is an internal function that is called to report an error.
  // However it is still a part of the interface because users may want to
  // set a breakpoint on this function in a debugger.
  void __asan_report_error(void *pc, void *bp, void *sp,
                           void *addr, int is_write, size_t access_size);

Muchas otras funciones en este encabezado se comentan explícitamente como si se hubieran hecho públicas para poder llamarlas desde un depurador.

Definitivamente te aconsejo que explores otros encabezados de libsanitizer/include/sanitizer aquí. Hay numerosas golosinas que se tenían allí.


Los puntos de interrupción para UBSan y ASan se pueden agregar de la siguiente manera:

(gdb) rbreak ^__ubsan_handle_ __asan_report_error
(gdb) commands
(gdb) finish
(gdb) end

Esto hará un punto de interrupción en los controladores, y finish inmediatamente después. Esto permite que se imprima el informe, pero el depurador obtiene el control justo después de que se imprima.

  • Esperaba que existiera un único punto de interrupción o variable de entorno para esto. De todos modos, ¿puede mostrar un ejemplo sobre el uso de esta función con gdb? Sería genial tener un enfoque (o simple gdbinit macro) que actúa como ASAN. Es decir, mostrar el mensaje y romper.

    – Lekenstein

    16 de julio de 2015 a las 19:12

  • @Lekensteyn Las variables de entorno no pueden inyectar puntos de interrupción; y para que exista una sola función de punto de interrupción, UBSan tendría que usar una sola función de varargs de multiplexación (que no es necesariamente un buen diseño y le hace perder la capacidad de punto de interrupción de manera eficiente solo en algunos tipos de UB). Con respecto a gdb/gdbinitclaro, lo agregaré en un par de horas.

    – Iwillnotexist Idonotexist

    16 de julio de 2015 a las 20:44

  • @Lekensteyn Parece que (gdb) rbreak __asan_report_error y (gdb) rbreak ^__ubsan interrumpa sin rodeos todas las funciones que coincidan con esas expresiones regulares. No hay ninguna función llamada después __asan_report_error regresa, por lo que me parece que de alguna manera tendría que programar gdb inmediatamente (gdb) finish después de alcanzar uno de estos puntos de interrupción.

    – Iwillnotexist Idonotexist

    16/07/2015 a las 21:57


  • Hay env vars como UBSAN_OPTIONS que son compatibles con libsanitizer (no gdb o similar), esos vars a los que me refiero por “variables de entorno”. En cuanto al punto de interrupción, intenté rbreak ^__ubsan_handle_ commands finish endpero de alguna manera esto se comporta de manera extraña con un programa de prueba muy simple (la función aún está bloqueada en el __ubsan_handle_... marco) mientras que un siguiente info breakpoints de alguna manera activa la impresión y cambia el marco. (GDB 7.9.1)

    – Lekenstein

    16 de julio de 2015 a las 23:55

  • UBSan tiene una verificación de desbordamiento de enteros sin signo. ¿Hay alguna manera de decir que una expresión dada con desbordamiento de enteros sin signo está bien? (¿Para que se ignore del informe?).

    – gnzlbg

    26/10/2015 a las 17:23

Un punto de interrupción establecido en __asan_report_error no me afecta y el programa simplemente existe después de imprimir los diagnósticos sin que se active el depurador. __asan::ReportGenericError antes de imprimir los diagnósticos y __sanitizer::Die después de imprimir los diagnósticos se golpean como se describe en el wiki de asan.

¿Ha sido útil esta solución?