Empresas
Empregos
  • Sobre nós
  • Soluções
    • Publicação de vagas
      Publique sua vaga e receba candidatos qualificados em 48h.
    • Avaliações de candidatos
      Mais de 500 testes técnicos e psicológicos, mais anti-fraude.
    • Headhunting
      Busca executiva personalizada do início ao fim.
    • Folha de Pagamento + EOR
      Dispersão de folha e EOR em mais de 15 países da LATAM.
  • Preços
  • Empregos

0

240
Visualizações
Analizando falla de segmentación sin archivo central

Supongamos que mis archivos binarios se ejecutan en el sitio de un cliente donde no puedo habilitar la generación core dump mediante ulimit -c . ¿Cómo depuran los ingenieros las segmentation faults en tales escenarios del mundo real? ¿Existe algún otro método para depurar o identificar fallas sin generar core dumps ?

over 4 years ago · Santiago Trujillo
1 Respostas
Responde à pergunta

0

En el pasado, tuve que lidiar con este tipo de restricciones en varias ocasiones. Una falla de segmentación o, más generalmente, una terminación anormal del proceso tuvo que ser investigada con la advertencia de que no había un volcado de memoria disponible.

Para Linux, nuestra plataforma elegida para este tutorial, se me ocurren algunas razones:

  • La generación de volcado del núcleo está deshabilitada por completo (usando limits.conf o ulimit )
  • El directorio de destino (directorio de trabajo actual o un directorio en /proc/sys/kernel/core_pattern ) no existe o es inaccesible debido a los permisos del sistema de archivos o SELinux
  • El sistema de archivos de destino no tiene suficiente espacio en disco, lo que genera un volcado parcial

Para todos ellos, el resultado neto es el mismo: no hay un volcado de núcleo (válido) para usar en el análisis. Afortunadamente, existe una solución para la depuración post-mortem que tiene el potencial de salvar el día, pero dadas sus limitaciones inherentes, su kilometraje puede variar de un caso a otro.

Identificación de la instrucción fallida

El siguiente ejemplo contiene un error de memoria clásico de usar después de liberar:

 #include <iostream> struct Test { const std::string &m_value; Test(const std::string &value): m_value(value) { } void print() { std::cout << m_value << std::endl; } }; int main() { std::string *value = new std::string("this is a test"); Test test(*value); delete value; test.print(); return 0; }

Después de delete value , la referencia std::string Test::m_value apunta a una memoria inaccesible. Por lo tanto, ejecutarlo da como resultado un error de segmentación:

 $ ./a.out Segmentation fault

Cuando un proceso finaliza debido a una infracción de acceso, el kernel de Linux crea una entrada de registro accesible a través de dmesg y, según la configuración del sistema, el syslog (generalmente /var/log/messages ). El ejemplo (compilado con -O0 ) crea la siguiente entrada:

 $ dmesg | grep segfault [80440.957955] a.out[7098]: segfault at ffffffffffffffe8 ip 00007f9f2c2b56a3 sp 00007ffc3e75bc48 error 5 in libstdc++.so.6.0.19[7f9f2c220000+e9000]

La fuente del kernel de Linux correspondiente de arch/x86/mm/fault.c :

 printk("%s%s[%d]: segfault at %lx ip %px sp %px error %lx", loglvl, tsk->comm, task_pid_nr(tsk), address, (void *)regs->ip, (void *)regs->sp, error_code);

El error ( error_code ) revela cuál fue el desencadenante. Es un conjunto de bits específico de la CPU ( x86 ). En nuestro caso, el valor 5 ( 101 en binario) indica que la página representada por la dirección de falla 0xffffffffffffffe8 fue mapeada pero inaccesible debido a la protección de la página y se intentó una lectura.

El mensaje de registro identifica el módulo que ejecutó la instrucción fallida: libstdc++.so.6.0.1 . La muestra se compiló sin optimización, por lo que la llamada a std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) no estaba en línea:

 400bef: e8 4c fd ff ff callq 400940 <_ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RK SbIS4_S5_T1_E@plt>

El STL realiza el acceso de lectura. Conociendo esos conceptos básicos, ¿cómo podemos identificar dónde ocurrió exactamente la falla de segmentación? La entrada de registro presenta dos direcciones esenciales que necesitamos para hacerlo:

 ip 00007f9f2c2b56a3 [...] error 5 in ^^^^^^^^^^^^^^^^ libstdc++.so.6.0.19[7f9f2c220000+e9000] ^^^^^^^^^^^^

El primero es el puntero de instrucción ( rip ) en el momento de la infracción de acceso, el segundo es la dirección a la que está asignada la sección .text de la biblioteca. Al restar la dirección base .text de rip , obtenemos la dirección relativa de la instrucción en la biblioteca y podemos desensamblar la implementación usando objdump (simplemente puede buscar el desplazamiento):

 0x7f9f2c2b56a3-0x7f9f2c220000=0x956a3
 $ objdump --demangle -d /usr/lib64/libstdc++.so.6 [...] 00000000000956a0 <std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, s td::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<ch ar>, std::allocator<char> > const&)@@GLIBCXX_3.4>: 956a0: 48 8b 36 mov (%rsi),%rsi 956a3: 48 8b 56 e8 mov -0x18(%rsi),%rdx ^^^^^ 956a7: e9 24 4e fc ff jmpq 5a4d0 <std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)@plt> 956ac: 0f 1f 40 00 nopl 0x0(%rax) [...]

¿Es esa la instrucción correcta? Podemos consultar a GDB para confirmar nuestro análisis:

 Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7b686a3 in std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) () from /lib64/libstdc++.so.6 Missing separate debuginfos, use: debuginfo-install glibc-2.17-323.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64 (gdb) disass Dump of assembler code for function _ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKSbIS4_S5_T1_E: 0x00007ffff7b686a0 <+0>: mov (%rsi),%rsi => 0x00007ffff7b686a3 <+3>: mov -0x18(%rsi),%rdx 0x00007ffff7b686a7 <+7>: jmpq 0x7ffff7b2d4d0 <_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l@plt> End of assembler dump.

GDB muestra la misma instrucción. También podemos usar una sesión de depuración para verificar la dirección de lectura:

 (gdb) print /x $rsi-0x18 $2 = 0xffffffffffffffe8

Este valor coincide con la dirección de lectura en la entrada de registro.

Identificación de las personas que llaman

Entonces, a pesar de la ausencia de un volcado del núcleo, la salida del kernel nos permite identificar la ubicación exacta de la falla de segmentación. Sin embargo, en muchos escenarios, eso está lejos de ser suficiente. Por un lado, nos falta la lista de llamadas que nos llevaron a ese punto: la pila de llamadas o el seguimiento de la pila.

Sin un volcado en la mochila, tiene dos opciones para comunicarse con las personas que llaman: puede iniciar su proceso usando catchsegv (una utilidad glibc) o puede implementar su propio controlador de señales.

catchsegv sirve como contenedor, genera el seguimiento de la pila y también vuelca los valores de registro y el mapa de memoria:

 $ catchsegv ./a.out *** Segmentation fault Register dump: RAX: 0000000002158040 RBX: 0000000002158040 RCX: 0000000002158000 [...] Backtrace: /lib64/libstdc++.so.6(_ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKSbIS4_S5_T1_E+0x3)[0x7f1794fd36a3] ??:?(_ZN4Test5printEv)[0x400bf4] ??:?(main)[0x400b2d] /lib64/libc.so.6(__libc_start_main+0xf5)[0x7f179467a555] ??:?(_start)[0x4009e9] Memory map: 00400000-00401000 r-xp 00000000 08:02 50331747 /home/user/a.out [...] 7f1794f3e000-7f1795027000 r-xp 00000000 08:02 33600977 /usr/lib64/libstdc++.so.6.0.19 7f1795027000-7f1795227000 ---p 000e9000 08:02 33600977 /usr/lib64/libstdc++.so.6.0.19 7f1795227000-7f179522f000 r--p 000e9000 08:02 33600977 /usr/lib64/libstdc++.so.6.0.19 7f179522f000-7f1795231000 rw-p 000f1000 08:02 33600977 /usr/lib64/libstdc++.so.6.0.19 [...]

¿Cómo funciona catchsegv ? Básicamente, inyecta un controlador de señal usando LD_PRELOAD y la biblioteca libSegFault.so . Si su aplicación ya instala un controlador de señal para SIGSEGV y tiene la intención de aprovechar libSegFault.so , su controlador de señal debe reenviar la señal al controlador original (tal como lo devuelve sigaction(SIGSEGV, NULL) ).

La segunda opción es implementar la funcionalidad de seguimiento de pila usted mismo utilizando un controlador de señal personalizado y backtrace() . Esto le permite personalizar la ubicación de salida y la salida en sí.

Según esa información, podemos hacer esencialmente lo mismo que hicimos antes ( 0x7f1794fd36a3-0x7f1794f3e000=0x956a3 ). Esta vez, podemos volver a las personas que llamaron para profundizar más. El segundo cuadro está representado por la siguiente línea:

 ??:?(_ZN4Test5printEv)[0x400bf4]

0x400bf4 es la dirección a la que regresa la persona que llama después de Test::print() , se encuentra en el ejecutable. Podemos visualizar el sitio de la llamada de la siguiente manera:

 $ objdump --demangle -d ./a.out [...] 400bea: bf a0 20 60 00 mov $0x6020a0,%edi 400bef: e8 4c fd ff ff callq 400940 <std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std: :char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_trai ts<char>, std::allocator<char> > const&)@plt> 400bf4: be 70 09 40 00 mov $0x400970,%esi ^^^^^^ 400bf9: 48 89 c7 mov %rax,%rdi 400bfc: e8 5f fd ff ff callq 400960 <std::ostream::operator<<(std::ostream& (*)(std::ostream&))@plt> [...]

Tenga en cuenta que la salida de objdump coincide con la dirección en esta instancia porque la ejecutamos contra el ejecutable, que tiene una dirección base predeterminada de 0x400000 en x86_64; objdump lo tiene en cuenta. Con la aleatorización del diseño del espacio de direcciones (ASLR) habilitada (compilada con -fpie , vinculada con -pie ), la dirección base debe tenerse en cuenta como se describe anteriormente.

Retroceder más implica los mismos pasos:

 ??:?(main)[0x400b2d]
 $ objdump --demangle -d ./a.out [...] 400b1c: e8 af fd ff ff callq 4008d0 <operator delete(void*)@plt> 400b21: 48 8d 45 d0 lea -0x30(%rbp),%rax 400b25: 48 89 c7 mov %rax,%rdi 400b28: e8 a7 00 00 00 callq 400bd4 <Test::print()> 400b2d: b8 00 00 00 00 mov $0x0,%eax ^^^^^^ 400b32: eb 2a jmp 400b5e <main+0xb1> [...]

Hasta ahora, hemos estado traduciendo manualmente la dirección absoluta a una dirección relativa. En su lugar, la dirección base del módulo se puede pasar a objdump a través --adjust-vma=<base-address> . De esa manera, el valor de rip o la dirección de la persona que llama se puede usar directamente.

Adición de símbolos de depuración

Hemos recorrido un largo camino sin un vertedero. Sin embargo, para que la depuración sea efectiva, falta otra pieza fundamental del rompecabezas: los símbolos de depuración. Sin ellos, puede resultar difícil asignar el ensamblado al código fuente correspondiente. La compilación de la muestra con -O3 y sin información de depuración ilustra el problema:

 [98161.650474] a.out[13185]: segfault at ffffffffffffffe8 ip 0000000000400a4b sp 00007ffc9e738270 error 5 in a.out[400000+1000]

Como consecuencia de la inserción, la entrada de registro ahora apunta a nuestro ejecutable como disparador. El uso de objdump nos lleva a lo siguiente:

 400a3e: e8 dd fe ff ff callq 400920 <operator delete(void*)@plt> 400a43: 48 8b 33 mov (%rbx),%rsi 400a46: bf a0 20 60 00 mov $0x6020a0,%edi 400a4b: 48 8b 56 e8 mov -0x18(%rsi),%rdx ^^^^^^ 400a4f: e8 4c ff ff ff callq 4009a0 <std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)@plt> 400a54: 48 89 c5 mov %rax,%rbp 400a57: 48 8b 00 mov (%rax),%rax

Parte de la implementación de la secuencia estaba en línea, lo que dificultaba la identificación del código fuente asociado. Sin símbolos, debe usar símbolos de exportación, llamadas (como operator delete(void*) ) y las instrucciones que lo rodean ( mov $0x6020a0 carga la dirección de std::cout : 00000000006020a0 <std::cout@@GLIBCXX_3.4> ) con fines de orientación.

Con los símbolos de depuración ( -g ), hay más contexto disponible al llamar a objdump con --source :

 400a43: 48 8b 33 mov (%rbx),%rsi operator<<(basic_ostream<_CharT, _Traits>& __os, const basic_string<_CharT, _Traits, _Alloc>& __str) { // _GLIBCXX_RESOLVE_LIB_DEFECTS // 586. string inserter not a formatted function return __ostream_insert(__os, __str.data(), __str.size()); 400a46: bf a0 20 60 00 mov $0x6020a0,%edi 400a4b: 48 8b 56 e8 mov -0x18(%rsi),%rdx ^^^^^^ 400a4f: e8 4c ff ff ff callq 4009a0 <std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)@plt> 400a54: 48 89 c5 mov %rax,%rbp

Eso funcionó como se esperaba. En el mundo real, los símbolos de depuración no están incrustados en los archivos binarios; se administran en paquetes de información de depuración separados. En esas circunstancias, objdump ignora los símbolos de depuración incluso si están instalados. Para abordar esta limitación, los símbolos deben volver a agregarse al binario afectado. El siguiente procedimiento crea símbolos separados y los vuelve a agregar utilizando eu-unstrip de elfutils en beneficio de objdump:

 # compile with debug info g++ segv.cxx -O3 -g # create detached debug info objcopy --only-keep-debug a.out a.out.debug # remove debug info from executable strip -g a.out # re-add debug info to executable eu-unstrip ./a.out ./a.out.debug -o ./a.out-debuginfo # objdump with executable containing debug info objdump --demangle -d ./a.out-debuginfo --source

Usando GDB en lugar de objdump

Hasta ahora, hemos estado usando objdump porque generalmente está disponible, incluso en sistemas de producción. ¿Podemos usar GDB en su lugar? Sí, ejecutando gdb con el módulo de interés. Uso 0x0x400a4b como en la invocación anterior de objdump:

 $ gdb ./a.out [...] (gdb) disass 0x400a4b Dump of assembler code for function main(): [...] 0x0000000000400a43 <+67>: mov (%rbx),%rsi 0x0000000000400a46 <+70>: mov $0x6020a0,%edi 0x0000000000400a4b <+75>: mov -0x18(%rsi),%rdx 0x0000000000400a4f <+79>: callq 0x4009a0 <_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l@plt> 0x0000000000400a54 <+84>: mov %rax,%rbp

A diferencia de objdump, GDB puede manejar información de símbolos externos sin problemas. disass /m corresponde a objdump --source :

 (gdb) disass /m 0x400a4b Dump of assembler code for function main(): [...] 21 Test test(*value); 22 delete value; 0x0000000000400a25 <+37>: test %rbx,%rbx 0x0000000000400a28 <+40>: je 0x400a43 <main()+67> 0x0000000000400a3b <+59>: mov %rbx,%rdi 0x0000000000400a3e <+62>: callq 0x400920 <_ZdlPv@plt> 23 test.print(); 24 return 0; 25 } 0x0000000000400a88 <+136>: add $0x18,%rsp [...] End of assembler dump.

En el caso de un binario optimizado, GDB podría omitir instrucciones en este modo si el código fuente no se puede mapear sin ambigüedades. Nuestra instrucción en 0x400a4b no aparece en la lista. objdump nunca omite instrucciones y podría omitir el contexto de origen en su lugar, un enfoque que prefiero para la depuración en este nivel. Esto no significa que GDB no sea útil para esta tarea, es solo algo a tener en cuenta.

Pensamientos finales

Motivo de terminación, registros, mapa de memoria y seguimiento de pila. Todo está allí sin ni siquiera un rastro de un volcado del núcleo. Si bien definitivamente es útil (arreglé bastantes bloqueos de esa manera), debe tener en cuenta que todavía se está perdiendo información valiosa al seguir esa ruta, sobre todo la pila y el montón, así como los datos por subproceso (metadatos de subproceso, registros, pila).

Por lo tanto, cualquiera que sea el escenario, debe considerar seriamente habilitar la generación de volcados del núcleo y asegurarse de que los volcados se puedan generar con éxito si llega el momento. La depuración en sí misma es lo suficientemente compleja, la depuración sin información que técnicamente podría tener aumenta innecesariamente la complejidad y el tiempo de respuesta y, lo que es más importante, reduce significativamente la probabilidad de que la causa raíz se pueda encontrar y abordar de manera oportuna.

over 4 years ago · Santiago Trujillo Relatório
Responde à pergunta
Encontrar trabalhos remotos

Descubra a nova forma de encontrar um emprego!

melhores empregos
Principais categorias de trabalho
Empresas
Postar vaga Preços Comercial
Jurídico
Termos e Condições Política de privacidade
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomende algumas ofertas para mim
Preciso de ajuda