多线程 C++ 应用程序崩溃的崩溃转储分析

问题描述

这是一个关于通用场景的问题。我有一个多线程 C++ 应用程序崩溃了,我有崩溃转储。可能有数百个线程在运行,其中任何一个都可能导致崩溃。

  1. 开始分析故障转储的好方法是什么。
  2. 在许多线程中(已在转储文件下记录信息)如何找到导致崩溃的任何特定线程。我是否应该寻找任何特定标准,因为我无法继续分析所有线程及其堆栈。
  3. 您想建议的任何其他有用信息/线索。

非常感谢您提前

解决方法

我们将以下代码称为 minimum reproducible example,您应该在以后的问题中提供它。

它创建了 100 个线程,同步它们,以便它们同时开始运行(需要 C++ 20)。其中一个线程会随机产生一个异常,所以我们不知道是哪一个。

#include <random>
#include <thread>
#include <vector>
using namespace std::chrono_literals;

std::random_device rd;
std::mt19937 twister(rd());
std::uniform_int_distribution<int> dist(0,100);
std::counting_semaphore<100> synchronizer(0);

void randomCrash()
{
    synchronizer.acquire();
    if (dist(twister) < 2)
    {
        throw std::exception();
    }
    std::this_thread::sleep_for(1000ms); // Ensure the thread is still there when we analyze the dump
}

int main()
{
    std::vector<std::thread> threads;
    for (int i = 0; i < 100; i++)
    {
        std::thread t(&randomCrash);
        threads.push_back(std::move(t)); // Threads can't be copied,so move it
    }
    std::cout << "Created 100 threads.\r\n";
    
    synchronizer.release(100);
    std::cout << "100 threads running now.\r\n";
    
    for (std::thread& th : threads)
    {
        if (th.joinable())
        {
            th.join();
        }
    }
    std::cout << "Done. Ooops ... no exception happened? Well,that's randomness.\r\n";
}

如果我们现在打开崩溃转储,我们可以通过查看提示看到它已经切换到导致异常的线程 92:

0:092>

但是让我们假设使用命令 ~0s 不起作用,所以我们回到主线程。

0:092> ~0s
ntdll!NtWaitForSingleObject+0x14:
00007ffc`cbcacc94 c3              ret
0:000> 

使用 ~ 命令,您可以识别导致异常的线程:

0:000> ~
.  0  Id: 3edc.cc8 Suspend: 1 Teb: 0000004a`fb029000 Unfrozen
[...]
  91  Id: 3edc.38d0 Suspend: 1 Teb: 0000004a`fb0df000 Unfrozen
# 92  Id: 3edc.2418 Suspend: 1 Teb: 0000004a`fb0e1000 Unfrozen
  93  Id: 3edc.4788 Suspend: 1 Teb: 0000004a`fb0e3000 Unfrozen
[...]
 103  Id: 3edc.43e4 Suspend: 1 Teb: 0000004a`fb0f7000 Unfrozen

当前线程有一个点(.),有异常的线程有一个散列(#)。请注意,如果当前线程是引发异常的线程,则该点可能会隐藏哈希。所以你可以轻松切换到线程

0:000> ~92s
ucrtbase!abort+0x4e:
00007ffc`c960286e cd29            int     29h

并查看调用堆栈

0:092> k
 # Child-SP          RetAddr               Call Site
00 0000004a`80efe500 00007ffc`c9601f9f     ucrtbase!abort+0x4e
01 0000004a`80efe530 00007ffc`b6e01aab     ucrtbase!terminate+0x1f
02 0000004a`80efe560 00007ffc`b6e02317     VCRUNTIME140_1!FindHandler<__FrameHandler4>+0x45b [D:\...\frame.cpp @ 693] 
03 0000004a`80efe730 00007ffc`b6e040d9     VCRUNTIME140_1!__InternalCxxFrameHandler<__FrameHandler4>+0x267 [D:\...\frame.cpp @ 357] 
04 0000004a`80efe7d0 00007ffc`cbcb1f6f     VCRUNTIME140_1!__CxxFrameHandler4+0xa9 [D:\...\risctrnsctrl.cpp @ 306] 
05 0000004a`80efe840 00007ffc`cbc61454     ntdll!RtlpExecuteHandlerForException+0xf
06 0000004a`80efe870 00007ffc`cbcb0a9e     ntdll!RtlDispatchException+0x244
07 0000004a`80efef80 00007ffc`c96bd759     ntdll!KiUserExceptionDispatch+0x2e
08 0000004a`80eff6b0 00007ffc`a9f36480     KERNELBASE!RaiseException+0x69
09 0000004a`80eff790 00007ff7`49ec13fd     VCRUNTIME140!_CxxThrowException+0x90 [D:\...\throw.cpp @ 75] 
0a 0000004a`80eff7f0 00007ff7`49ec1ecb     WhichThreadCrashes!randomCrash+0x1bd [C:\...\WhichThreadCrashes.cpp @ 19] 
0b (Inline Function) --------`--------     WhichThreadCrashes!std::invoke+0x2 [C:\...\type_traits @ 1585] 
0c 0000004a`80eff850 00007ffc`c95b1bb2     WhichThreadCrashes!std::thread::_Invoke<std::tuple<void (__cdecl*)(void)>,0>+0xb [C:\...\thread @ 55] 
0d 0000004a`80eff880 00007ffc`cb7c7034     ucrtbase!thread_start<unsigned int (__cdecl*)(void *),1>+0x42
0e 0000004a`80eff8b0 00007ffc`cbc62651     kernel32!BaseThreadInitThunk+0x14
0f 0000004a`80eff8e0 00000000`00000000     ntdll!RtlUserThreadStart+0x21

所以我们可以看到它在 randomCrash() 中崩溃了。

一旦你知道它是如何工作的,你也可以使用~#s直接切换到有异常的线程:

0:092> ~0s
ntdll!NtWaitForSingleObject+0x14:
00007ffc`cbcacc94 c3              ret
0:000> ~#s
ucrtbase!abort+0x4e:
00007ffc`c960286e cd29            int     29h
0:092>

另外,!analyze -v 应该给你

0:000> !analyze -v
[...]
STACK_COMMAND:  ~92s ; .ecxr ; kb