1. 初步分析
某进程崩溃必现。
打开崩溃dmp,结合c++源代码,崩溃大致发生在某dll代码里的这句:SAFE_DELETE(pContentData);
En_HP_HandleResult CTcpOperation::OnClintReceive(HP_Client pSender, HP_CONNID dwConnID, const BYTE * pdata, int iLength)
{
LOG_INFO(_T("Client OnReceive iLength is %d"), iLength);
if (iLength == sizeof(STcpTransferData))
{
LPVOID pVoidTemp = nullptr;
STcpTransferData cTransferData;
memmove(&cTransferData, pdata, sizeof(STcpTransferData));
if (CTcpOperation::GetInstance()->CheckAndSetData(ETransferType::SenddataLength, cTransferData, dwConnID, pVoidTemp))//单条数据总大小
{
HP_Client_SetExtra(pSender, pVoidTemp);
LOG_INFO(_T("OnClintReceive new message set length"));
return HR_OK;
}
}
//获取服务端发送的数据,累加接收,直到得到完整数据,发送
LPVOID pSendDataExtra = HP_Client_GetExtra(pSender);
CVMsgSendData* pSendData = (CVMsgSendData*)pSendDataExtra;
if (pSendData)
{
pSendData->SetOffset(iLength);
pSendData->AddData(pdata);
if (pSendData->GetLeftLength() <= 0)
{
const size_t nDataSize = pSendData->GetLength();
BYTE* pContentData = new BYTE[nDataSize];
memmove_s(pContentData, nDataSize, pSendData->GetData(), nDataSize);
//TODO 释放数据,后续用智能指针代替
#define SAFE_DELETE(p) if ( p ) { delete p; p = nullptr; }
SAFE_DELETE(pSendData);
//防止OnClintReceive堵塞,提高发送效率,起线程调用回调函数。
THREAD_CREATE(
[pContentData, iLength]()mutable
{
GetInstance()->GetTcpClient()->DoReceiveCallBack(true, pContentData, iLength);
SAFE_DELETE(pContentData);
}
);
}
}
return En_HP_HandleResult::HR_OK;
}
若编译此dll时,选择不优化,而不是默认的O2 max speed优化,则必不崩溃。
2. 反汇编分析
查看反汇编,崩溃在这句:mov eax, [edi]时候edi是0x10。
看起来dword ptr[edi]里存的就是pContentData的值,因为之后代码判断是否为0然后delete()之。
一开始的mov edi, this实际上是mov edi,ecx。ecx应该是这个lambda函数用ecx传参传进来的pContentData(后来ecx才是采用做TcpOperation类的this指针。这里ida的笔记有点偏差)。
3. 实时调试
在mov edi, ecx处和mov eax,[edi]处下断点并查看edi的值。第一次未见异常,第二次则发现mov edi, ecx时edi时0xXXXXXXXX,但是走到mov eax,[edi]就edi变成了0x10。
4. 分析和解法
从汇编代码来看,edi在mov edi, ecx处和mov eax,[edi]之间是没有改动的。虽然这期间也有函数调用,但函数体内的头尾部一般会把edi存在栈上然后恢复回去。
由于没法给edi寄存器下硬件断点,所以也不知道edi究竟在哪里有发生变化。
解法就是使用c语言的关键字volatile,使之delete的时候要从栈上找处pContentData,而不是从dword ptr[edi]里找。经验证,不会崩溃。
如下:
THREAD_CREATE(
[pContentData, iLength]()mutable {
BYTE* volatile p2 = pContentData;
GetInstance()->GetTcpClient()->DoReceiveCallBack(true, pContentData, iLength);
SAFE_DELETE(p2);
}
查看此段落的汇编代码如下:它把临时变量p2存在栈上esp+2Ch+p2处。届时从此而不是dword ptr[edi]处取回p2的值。
5.关于volatile和指针
若代码改为
volatile BYTE* p2 = pContentData;
也不会崩溃,但是看汇编,它依然没有把p2放在栈上,而是放在ebx里。具体原因参见https://blog.csdn.net/turkeyzhou/article/details/8953911
其实这样改也是没有解决隐患的。