开启代码优化分析dump

背景

前面都是讲解程序未开启优化时的问题定位。当程序未开启优化时,通过‘local’,‘watch’,’kv’ 的参数信息都是正确的,所以很容易可以通过变量的内容确定问题的原因。

但是如果开启了程序优化功能,那么编译器会优化代码的结构,windbg还是按照原来的方法来解析变量,那么大多情况会出现不正确的,比如最重要的this指针,未优化时都是固定在ecx寄存器中,但是开启了优化后就需要自己来查找对应的this地址。

前段时间川麻服务出现崩溃,就遇到了这么一个问题。

dump信息

下载链接:链接:https://pan.baidu.com/s/1nQOksK2QLUZsQsl7PhgDCw
提取码:4c1o

c/c++的调用约定

C 语言: cdecl、stdcall、fastcall、naked、pascal。

C++ 语言: cdecl、stdcall、fastcall、naked、pascal、thiscall,比 C 语言多出一种 thiscall 调用方式。

thiscall 调用方式是唯一一种不能显示指定的修饰符。它是C++类成员函数缺省的调用方式。由于成员函数调用还有一个this指针,因此必须用这种特殊的调用方式。

thiscall调用方式意味着:

(1)参数从右向左压入栈。

(2)如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压入栈后被压入栈。参数个数不定的,由调用者清理堆栈,否则由函数自己清理堆栈。

可以看到,对于参数个数固定的情况,它类似于stdcall,不定时则类似于cdecl。

分析dump

下载指定的dump,并使用windbg打开,加载好符号文件。
输入命令:”.ecxr” ,显示异常上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:043& .ecxr
eax=0000ffff ebx=00435a18 ecx=04921dc4 edx=060de888 esi=0d7ed468 edi=00000001
eip=00b9d4e7 esp=070efde8 ebp=070efdf4 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202
xzmoSvr!CCommonBaseServer::DealApplyBaseWelfare+0x37:
00b9d4e7 ff30 push dword ptr [eax] ds:002b:0000ffff=????????
0:043& kv
*** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr Args to Child
070efdf4 00bf54dc 00435f8c 070efe74 0c8bc458 xzmoSvr!CCommonBaseServer::DealApplyBaseWelfare+0x37 (FPO: [Non-Fpo]) (CONV: thiscall) [d:\jenkins\workspace\publish_xzmosvr\gamesvr\commonbase\commonbaseserver.cpp @ 218]
070efe88 733dc01d 00000000 76fc49d0 00000000 xzmoSvr!CMainServer::SoapThreadProc+0x29c (FPO: [Non-Fpo]) (CONV: thiscall) [d:\jenkins\workspace\publish_gametplserver2.0\tcgame2.0\trunk\tcgsvr.cpp @ 9565]
070efec0 733dc001 00000000 070efed8 75ad338a msvcr120!_callthreadstartex+0x1b (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 376]
070efecc 75ad338a 04981c78 070eff18 77b89f72 msvcr120!_threadstartex+0x7c (FPO: [Non-Fpo]) (CONV: stdcall) [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 354]
070efed8 77b89f72 04981c78 75257f9b 00000000 kernel32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo])
070eff18 77b89f45 733dbfb4 04981c78 ffffffff ntdll!__RtlUserThreadStart+0x70 (FPO: [Non-Fpo])
070eff30 00000000 733dbfb4 04981c78 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])

大致可以猜测是访问非法指针导致的异常。
然后查看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BOOL CCommonBaseServer::DealApplyBaseWelfareEx(LPSOAP_SERVICE pSoapService, IXYSoapClientPtr& pSoapClient, LPCONTEXT_HEAD lpContext, LPREQUEST lpRequest)
{
BOOL bResult = __super::DealApplyBaseWelfare(pSoapService, pSoapClient, lpContext, lpRequest);
LPAPPLY_BASEWELFARE_EX lpApplyWelfare = (LPAPPLY_BASEWELFARE_EX)lpRequest-&pDataPtr;

if (bResult &amp;&amp; lpApplyWelfare &amp;&amp; 0 <= lpApplyWelfare-&nSoapReturn)
{
if (m_pDataStats)
{
m_pDataStats->m_datastats_onapplywelfare(lpApplyWelfare-&nRoomID, lpApplyWelfare-&nUserID); ---------& 这行出现问题
}
}

return bResult;
}

并查看异常期间的参数信息:

刚开始主程序分析就以为是m_pDataStats = NULL 导致崩溃,原因可能是多线程导致,接着就开始review代码。 但是reivew之后发现m_pDataStats是不能为NULL的(除非进程关闭),然后又是一阵思考、联想、分析…….. 无果。
接着猜测是否是踩内存导致(这个review代码难度就有点大了)………

但实际这个dump发生异常的原因不是因为m_pDataStats = NULL导致,而是某个指针的值=0x0000ffff导致,这个在dump中有明确说明:

1
2
xzmoSvr!CCommonBaseServer::DealApplyBaseWelfare+0x37:
00b9d4e7 ff30 push dword ptr [eax] ds:002b:0000ffff=????????

ds:002b::0000ffff 在访问0000ffff地址时发生异常!!!!!!

但为何看this指针中的m_pDataStats 明明是 NULL啊? 这就是代码开启优化的原因导致this指针不正确。

如何找到正确的this指针地址呢?通过汇编代码,看优化后的入参方式.使用命令“alt+7”可以打开汇编代码:
函数原型:

1
BOOL CCommonBaseServer::DealApplyBaseWelfareEx(LPSOAP_SERVICE pSoapService, IXYSoapClientPtr&amp; pSoapClient, LPCONTEXT_HEAD lpContext, LPREQUEST lpRequest)

首先我们先查看下this 指针存放在哪里

1
2
3
4
0:043> dv /V this
@ecx @ecx this = 0x04921dc4

表明是放置在 ecx寄存器中

然后查看汇编代码:

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
    xzmoSvr!CCommonBaseServer::DealApplyBaseWelfare:
00b9d4b0 55 push ebp
00b9d4b1 8bec mov ebp, esp
00b9d4b3 53 push ebx
00b9d4b4 56 push esi
00b9d4b5 8b7514 mov esi, dword ptr [ebp+14h]
00b9d4b8 8bd9 mov ebx, ecx --- 临时保存
00b9d4ba 57 push edi
00b9d4bb 56 push esi
00b9d4bc ff7510 push dword ptr [ebp+10h]
00b9d4bf ff750c push dword ptr [ebp+0Ch]
00b9d4c2 ff7508 push dword ptr [ebp+8]
00b9d4c5 e806b10300 call xzmoSvr!CMainServer::DealApplyBaseWelfare (00bd85d0)
00b9d4ca 8bf8 mov edi, eax
00b9d4cc 8b461c mov eax, dword ptr [esi+1Ch]
00b9d4cf 85ff test edi, edi
00b9d4d1 741e je xzmoSvr!CCommonBaseServer::DealApplyBaseWelfare+0x41 (00b9d4f1)
00b9d4d3 85c0 test eax, eax
00b9d4d5 741a je xzmoSvr!CCommonBaseServer::DealApplyBaseWelfare+0x41 (00b9d4f1)
00b9d4d7 83784000 cmp dword ptr [eax+40h], 0
00b9d4db 7c14 jl xzmoSvr!CCommonBaseServer::DealApplyBaseWelfare+0x41 (00b9d4f1)
00b9d4dd 8b8bf8af0000 mov ecx, dword ptr [ebx+0AFF8h] --- 被重新赋值
00b9d4e3 85c9 test ecx, ecx
00b9d4e5 740a je xzmoSvr!CCommonBaseServer::DealApplyBaseWelfare+0x41 (00b9d4f1) -- 崩溃的地方
00b9d4e7 ff30 push dword ptr [eax]
00b9d4e9 ff7004 push dword ptr [eax+4]

可以看到ecx的地址被临时保存到ebx中,在后续出现异常之前,ecx又被赋值成了其他值,通过之前异常的寄存器信息可以看到ebx的值

1
2
3
4
5
6
0:043& .ecxr
eax=0000ffff ebx=00435a18 ecx=04921dc4 edx=060de888 esi=0d7ed468 edi=00000001
eip=00b9d4e7 esp=070efde8 ebp=070efdf4 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202
xzmoSvr!CCommonBaseServer::DealApplyBaseWelfare+0x37:
00b9d4e7 ff30 push dword ptr [eax] ds:002b:0000ffff=????????

ebx=00435a18, 使用’watch’查看this指针内容:

upload successful

可以确定m_pDataStats 不仅指针有效,而且里面的内容也是正确的。所以异常并不是m_pDataStats异常导致。

那么问题就是lpApplyWelfare 这个指针导致。 这时候查看入参lpRequest 内容

upload successful

pDataPtr=0x0000ffff和dump提示的异常指针内容相符,所以可以确定是这个指针导致的。

所以重新对lpRequest进行代码review,发现问题的本质原因:
__super::DealApplyBaseWelfare(pSoapService, pSoapClient, lpContext, lpRequest);
在执行过程中,会将lpRequest 指针post到B线程中执行,而B线程是会释放lpRequest指针的。

所以在低概率情况下,会出现多线程崩溃问题。

修改方式:先copy内存到局部变量,再执行super逻辑。

结论

因为开启了代码优化,导致一开始在分析dump时走向了错误的道路,浪费了时间。在碰到开启代码优化时,查找正确的指针就变的有点困难。

所以不是很建议新上线的服务开启代码优化,反而增加了问题定位的难度。