Valorant requires a restart or loads slowly when entering the game. What can I do? I have an idea.

Valorant is a free first -person tactical shooting game developed by Riot Games, which combines the exact system of arms shooting and heroism. Players will be transformed into agents and will compete for target points using team collaboration, tactical layout and skills cooperation 5V5 offensive and defense stroke. The game has a low economic system and high competitiveness. Before each game start, buy weapons and armor according to tactical needs and use a combination of heroic skills to create the benefits of the battlefield. Its solid ballistic mechanism and map design require players for accurate purposes and tactical decision -making. At the same time, the rich character pool and skill combination give the game a strategic depth. If you have the problems you need to restart or slowly load while playing a "Varorant", try the following systematic solution: Network environment optimization is the main solution. Because valante servers can be introduced abroad, physical dist...

如何快速调试并找到在std::thread子线程中抛出的C++异常问题!

C++11标准库新增的std::thread类可以方便地开启子线程。然而有个奇怪的现象是,如果在这些子线程中抛出了未处理的C++异常而导致程序崩溃,那么在生成的dump文件中将还原不出异常发生时的调用栈。可以通过下面的方法来展示这个现象。

首先使用以下代码生成一个控制台程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <thread>
#include <vector>

std::thread* g_thread;

void ThreadEntry() {

std::vector<int> v;
v.at(0);
}

int main() {

g_thread = new std::thread(ThreadEntry);
g_thread->join();
}

这段代码很简单,就是通过std::thread创建一个子线程,并且在这个子线程中访问一个空的std::vector中的元素,让它抛出C++异常。务必要使用Release配置来生成程序,不能使用Debug配置。

接下来,在资源管理器中直接运行该程序,注意不要通过调试器来运行。一般会在第二次运行的时候,出现下面的Windows错误报告窗口:

在详细信息中的C:\Users\Zplutor\AppData\Local\Temp\WERBF98.tmp.mdmp文件即是Windows错误报告为崩溃的程序生成的dump文件,里面包含了程序崩溃时的一些信息,例如函数调用栈。该文件在关闭了错误报告窗口时即被删除,所以要先把这个文件复制出来。

最后,用WinDbg打开这个dump文件,先用.ecxr命令切换到异常环境,再用k命令显示调用栈,结果显示如下:

1
2
3
4
5
6
7
8
9
10
11
0:002> k
*** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr
0095f07c 6ba8dc5f msvcr120!abort+0x38 [f:\dd\vctools\crt\crtw32\misc\abort.c @ 90]
0095f0ac 6b99f353 msvcr120!terminate+0x33 [f:\dd\vctools\crt\crtw32\eh\hooks.cpp @ 96]
0095f8fc 6ba1c01d msvcp120!_Call_func+0x2e [f:\dd\vctools\crt\crtw32\stdcpp\thr\threadcall.cpp @ 35]
0095f934 6ba1c001 msvcr120!_callthreadstartex+0x1b [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 376]
0095f940 7685ee6c msvcr120!_threadstartex+0x7c [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 354]
0095f94c 77053ab3 kernel32!BaseThreadInitThunk+0xe
0095f98c 77053a86 ntdll!__RtlUserThreadStart+0x70
0095f9a4 00000000 ntdll!_RtlUserThreadStart+0x1b

可以看到,显示出来的调用栈几乎没有用处,只能看出子线程在开始之后就调用了terminate函数来终止程序,完全看不出来是什么原因导致的。

在调试的时候,如果遇到难以逾越的问题,不妨大胆地进行推测,并根据这些推测进行尝试。推测不一定是正确的,但是在尝试的过程中很可能会发现新的解法。在当前这个例子中,即使不了解std::thread子线程的具体实现,也不难根据上面的调用栈推测出来。下面是一种可能的实现方式:

1
2
3
4
5
6
7
8
9
void _Call_func() {

try {
ThreadEntry();
}
catch (...) {
terminate();
}
}

_Call_func函数用来调用在std::thread的构造函数中传进来的入口函数,在本例中即是ThreadEntry。该入口函数通过一对try/catch包裹起来,凡是在它里面抛出来的未处理异常都会被捕获,继而调用terminate函数来终止程序。由于terminate是在_Call_func函数中调用的,所以从调用栈上来看,terminate的上一个栈帧必然是_Call_func,ThreadEntry内部的所有栈帧都被跳过了。

根据以上的推测可知,异常发生时的调用栈还原不出来,只是因为它的栈帧被跳过了而已,假如这些栈帧还保留着,那肯定还是能还原出来的。继续观察上述的调用栈,发现_Call_func和terminate的栈帧之间尚有大约2KB(根据ChildEBP计算得来,0095f8fc - 0095f0ac)的内容,因此可以有八九分把握确定被跳过的栈帧就在这里面。

接下来要通过手工方式寻找那些被跳过的栈帧。从terminate栈帧的ChildEBP开始,使用dps命令逐步向上寻找。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
0:002> dps 0095f0ac l32
0095f0ac 0095f8fc
0095f0b0 6b99f353 msvcp120!_Call_func+0x2e [f:\dd\vctools\crt\crtw32\stdcpp\thr\threadcall.cpp @ 35]
0095f0b4 6ba097f2 msvcr120!_NLG_Return [f:\dd\vctools\crt\crtw32\eh\i386\lowhelpr.asm @ 64]
0095f0b8 0095f8f0
0095f0bc 0095f37c
0095f0c0 0095f0d0
0095f0c4 0095f8f0
0095f0c8 00000000
0095f0cc 0095f8fc
0095f0d0 0095f0fc
0095f0d4 6ba09861 msvcr120!_CallCatchBlock2+0x4f [f:\dd\vctools\crt\crtw32\eh\i386\trnsctrl.cpp @ 502]
0095f0d8 6b99f34d msvcp120!_Call_func+0x28 [f:\dd\vctools\crt\crtw32\stdcpp\thr\threadcall.cpp @ 35]
0095f0dc 0095f8f0
0095f0e0 00000100
0095f0e4 0095f158
0095f0e8 6ba09ffc msvcr120!CatchGuardHandler [f:\dd\vctools\crt\crtw32\eh\i386\trnsctrl.cpp @ 535]
0095f0ec c29105eb
0095f0f0 6b9b4bc0 msvcp120!_CTA4?AVsystem_errorstd+0x7c
0095f0f4 0095f8f0
0095f0f8 00000001
0095f0fc 0095f168
0095f100 6ba0999c msvcr120!CallCatchBlock+0x87 [f:\dd\vctools\crt\crtw32\eh\frame.cpp @ 1400]
0095f104 0095f8f0
0095f108 6b9b4bc0 msvcp120!_CTA4?AVsystem_errorstd+0x7c
0095f10c 6b99f34d msvcp120!_Call_func+0x28 [f:\dd\vctools\crt\crtw32\stdcpp\thr\threadcall.cpp @ 35]
0095f110 00000000
0095f114 00000100
0095f118 c2910467
0095f11c 0095f8f0
0095f120 6b9b4bac msvcp120!_CTA4?AVsystem_errorstd+0x68
0095f124 0095f8f0
0095f128 0095f898
0095f12c 00000000
0095f130 00000000
0095f134 00000000
0095f138 00000000
0095f13c 0095f128
0095f140 0095f8dc
0095f144 6b9ff756 msvcr120!_getptd+0x6 [f:\dd\vctools\crt\crtw32\startup\tidtable.c @ 337]
0095f148 00000000
0095f14c 6b99f34d msvcp120!_Call_func+0x28 [f:\dd\vctools\crt\crtw32\stdcpp\thr\threadcall.cpp @ 35]
0095f150 0095f118
0095f154 c291048b
0095f158 0095f2a8
0095f15c 6ba0a0d5 msvcr120!_except_handler4 [f:\dd\vctools\crt\crtw32\misc\i386\chandler4gs.c @ 84]
0095f160 a9a46ccf
0095f164 00000001
0095f168 0095f1a4
0095f16c 6ba09a50 msvcr120!CatchIt+0x69 [f:\dd\vctools\crt\crtw32\eh\frame.cpp @ 1211]
0095f170 0095f37c

这个寻找过程需要耐心和运气,因为dps命令输出的结果非常多,而且有些并不是真正的栈帧。在不了解C++异常处理的情况下只能逐个排查,排查的方法是,使用k命令从该栈帧开始回溯调用栈,看看最终能不能到达KiUserExceptionDispatcher的栈帧。例如,可以从上面结果的最后一个栈帧CatchIt开始回溯。要注意,根据栈帧的结构,0095f16c存放的是返回地址,上一个0095f168存放的才是ebp,所以应使用0095f168来回溯。结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
0:002> k = 0095f168
ChildEBP RetAddr
0095f07c 6ba8dc5f msvcr120!abort+0x38 [f:\dd\vctools\crt\crtw32\misc\abort.c @ 90]
0095f168 6ba09a50 msvcr120!terminate+0x33 [f:\dd\vctools\crt\crtw32\eh\hooks.cpp @ 96]
0095f1a4 6ba095ab msvcr120!CatchIt+0x69 [f:\dd\vctools\crt\crtw32\eh\frame.cpp @ 1211]
0095f220 6ba09638 msvcr120!FindHandler+0x27b [f:\dd\vctools\crt\crtw32\eh\frame.cpp @ 689]
0095f254 6ba096ba msvcr120!__InternalCxxFrameHandler+0xd6 [f:\dd\vctools\crt\crtw32\eh\frame.cpp @ 439]
0095f290 770372b9 msvcr120!__CxxFrameHandler3+0x26 [f:\dd\vctools\crt\crtw32\eh\i386\trnsctrl.cpp @ 301]
0095f2b4 7703728b ntdll!ExecuteHandler2+0x26
0095f2d8 7700f9d7 ntdll!ExecuteHandler+0x24
0095f364 77037117 ntdll!RtlDispatchException+0x127
0095f364 00000000 ntdll!KiUserExceptionDispatcher+0xf

这个调用栈最终到达了KiUserExceptionDispatcher,也就是我们要找的栈帧。为什么要找KiUserExceptionDispatcher的栈帧呢?这是因为在Windows下所有异常都是通过KiUserExceptionDispatcher这个函数抛出来的,这个函数具有两个类型分别为PEXCEPTION_RECORD和PCONTEXT的参数,分别指向异常信息以及异常发生时的线程环境信息。

既然找到了KiUserExceptionDispatcher的栈帧,那么可以换用kb命令,显示出它的参数列表:

1
2
3
4
0:002> kb = 0095f168
ChildEBP RetAddr Args to Child
(略)
0095f364 00000000 0095f37c 0095f39c 0095f37c ntdll!KiUserExceptionDispatcher+0xf

可知PEXCEPTION_RECORD的值是0095f37c,PCONTEXT的值是0095f39c。

WinDbg提供了.exr命令来显示PEXCEPTION_RECORD的内容,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
0:002> .exr 0095f37c
ExceptionAddress: 74c4812f (KERNELBASE!RaiseException+0x00000058)
ExceptionCode: e06d7363 (C++ EH exception)
ExceptionFlags: 00000001
NumberParameters: 3
Parameter[0]: 19930520
Parameter[1]: 0095f898
Parameter[2]: 6b9b5734
pExceptionObject: 0095f898
_s_ThrowInfo : 6b9b5734
Type : class std::out_of_range
Type : class std::logic_error
Type : class std::exception

可以看到这是一个C++异常,并且类型是std::out_of_range。

WinDbg亦提供了.cxr命令,可以切换到指定PCONTEXT的线程环境,如下所示:

1
2
3
4
5
6
0:002> .cxr 0095f39c
eax=0095f7f8 ebx=00431ee8 ecx=00000003 edx=00000000 esi=6b9b5734 edi=0095f898
eip=74c4812f esp=0095f7f8 ebp=0095f848 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
KERNELBASE!RaiseException+0x58:
74c4812f c9 leave

至此,异常发生时的各种寄存器信息一览无余。此时使用k命令即可得到异常发生时的调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:002> k
*** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr
0095f848 6ba09339 KERNELBASE!RaiseException+0x58
0095f888 6b983a3a msvcr120!_CxxThrowException+0x5b [f:\dd\vctools\crt\crtw32\eh\throw.cpp @ 152]
0095f8a4 00c2104c msvcp120!std::_Xout_of_range+0x2e [f:\dd\vctools\crt\crtw32\stdcpp\xthrow.cpp @ 24]
0095f8cc 00c211ac ConsoleApplication!ThreadEntry+0x4c [c:\users\zplutor\documents\projects\tests\consoleapplication\main.cpp @ 9]
0095f8d4 6b99f33c ConsoleApplication!std::_LaunchPad<std::_Bind<1,void,void (__cdecl*const)(void)> >::_Go+0xc [c:\program files\microsoft visual studio 12.0\vc\include\thr\xthread @ 187]
0095f8fc 6ba1c01d msvcp120!_Call_func+0x17 [f:\dd\vctools\crt\crtw32\stdcpp\thr\threadcall.cpp @ 28]
0095f934 6ba1c001 msvcr120!_callthreadstartex+0x1b [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 376]
0095f940 7685ee6c msvcr120!_threadstartex+0x7c [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 354]
0095f94c 77053ab3 kernel32!BaseThreadInitThunk+0xe
0095f98c 77053a86 ntdll!__RtlUserThreadStart+0x70
0095f9a4 00000000 ntdll!_RtlUserThreadStart+0x1b

本文开头提到的现象并不是std::thread特有的。事实上,如果在捕获了一个异常之后的处理过程中又抛出了一个新的异常,那么旧异常的调用栈会被新异常的调用栈覆盖。在本文的例子中,_Call_func捕获了第一个异常之后,调用了terminate函数,terminate继而又调用了abort函数来抛出新的异常。使用本文提到的方法,就可以应对这种情况,还原出旧异常的调用栈。


原文来自:如何调试在std::thread子线程中抛出的C++异常 | Zplutor's

Comments

Popular posts from this blog

干翻 nio ,王炸 io_uring 来了 ,史上最详细说明及最全图解!!

Google谷歌镜像网址/网站大全,亲测可用!

V2rayN 电脑客户端如何在 win7/win10/win11上 实现全局代理