目录
1、执行memset操作时遇到了内存访问违例,导致程序崩溃
2、查看崩溃时的函数调用堆栈,初步怀疑是memset时有内存越界
3、存放YUV数据的buffer在执行若干操作后出现内存越界
4、加载系统库的pdb之后,看到了更多的函数调用堆栈,看到发生异常的接口的调用
5、设置系统库pdb文件在线下载服务器地址去加载系统库pdb的好处
6、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具案例集锦(专栏文章正在更新中...)https://blog.csdn.net/chenlycly/category_12279968.html 最近音视频编解码模块在播放WebRTC开源库回调出来的I420视频码流时遇到了崩溃问题,取来了dump文件,使用Windbg打开,结合着源代码进行分析。本文详细讲述整个问题的排查分析过程,并对相关的细节进行展开。
1、执行memset操作时遇到了内存访问违例,导致程序崩溃
最近软件在执行某个重要业务时会频繁地崩溃,于是取来其中一次的dump文件进行分析。打开dump文件,看到产生了Access violation - code c0000005内存访问违例的异常:
使用.ecxr切换到发生异常的线程上下文,看到发生异常的汇编指令:
没有明显的异常,即汇编指令中的内存地址没有明显的异常,不是很小或者很大的内存地址,好像都在可访问的地址范围中。
看到汇编指令位于C运行时库的memset函数中,memset主要是清理一段buffer内存的,比如我们在定义一个结构体对象时习惯先去memset,如下:
STARTUPINFO startInfo;
memset(&startInfo, 0, sizeof(startInfo));
这个时候一般是不太可能产生异常的。
memset内存时产生异常,是不是memse时访问了不该访问的内存?有内存越界的情况存在?尝试使用!analyze -v命令去分析一下,分析结果显示:Attempt to write to address 364e6000,尝试去写364e6000内存时产生了内存访问违例。所以,可能是遇到了内存越界了。
2、查看崩溃时的函数调用堆栈,初步怀疑是memset时有内存越界
于是使用kn命令查看函数调用堆栈:
此时的函数调用堆栈主要与xxxxxplayer、xxxxmpdll有关,于是使用lm命令查看这两个库的时间戳,查找对应时间点的pdb文件:
根据图中显示的时间戳,找到xxxxxplayer、xxxxmpdll两个模块的pdb文件,然后将pdb文件路径设置到Windbg中,这样可以在函数调用堆栈中看到具体的函数和代码的行号了,如下所示:
函数调用堆栈中只看到了CVideoPlay::SetPlayInfo接口,但该接口中并没有调用memset接口,如下:
是不是46行中的m_pcPlayer->SetPlayInfo接口中调用memset?go到对应的接口中,确实看到对memset的调用,如下所示:
这段代码是为了清理m_tLastFrm.pAddr[0]、m_tLastFrm.pAddr[1]和m_tLastFrm.pAddr[2]三个存放YUV数据的buffer的内存,清理成黑色的YUV数据。这三个指针变量的定义如下:
unsigned char* pAddr[3]; // 3个存放buffer首地址的指针变量
初步怀疑是执行memset时产生内存越界了,那下面要看看m_tLastFrm.pAddr[0]、m_tLastFrm.pAddr[1]和m_tLastFrm.pAddr[2]三个buffer的内存是何时分配的。
3、存放YUV数据的buffer在执行若干操作后出现内存越界
于是查阅代码,发现是在WebRTC库回调I420视频数据时申请了一大块连续的内存(WebRTC开源库将采集到的本段视频和收到的远端视频数据通过回调接口回调给本视频播放模块),然后分配给m_tLastFrm.pAddr[0]、m_tLastFrm.pAddr[1]和m_tLastFrm.pAddr[2]这三个指针,对应的buffer用来存放I420数据的Y、U、V分量数据,如下:
先是申请了MAX_VID_FRAME_WIDTH*MAX_VID_FRAME_HEIGHT*2字节的一块连续的内存,依次给这三个指针变量赋地址值,如果根据此处的赋值,执行上面的memset操作,是不会越界的。其中两个宏的定义如下:
// 视频最大分辨率目前定义为1080P
#define MAX_VID_FRAME_WIDTH (int)1920
#define MAX_VID_FRAME_HEIGHT (int)1088 // 回调过来的1080P分辨率的高度值不一定是1080,可能是1088,所以此处按较大值1088来定义
就此处分配的内存,针对调用memset产生问题的代码进行分析:
首先,对于m_tLastFrm.pAddr[0],m_tLastFrm.pAddr[0]开始的内存区域有MAX_VID_FRAME_WIDTH*MAX_VID_FRAME_HEIGHT*2,所以对该内存区域执行下列操作不会越界:
memset(m_tLastFrm.pAddr[0], 0x00, MAX_VID_FRAME_WIDTH * MAX_VID_FRAME_HEIGHT * 2);
然后,对于m_tLastFrm.pAddr[1]和m_tLastFrm.pAddr[2]这两个地址开始的内存区域,肯定还在MAX_VID_FRAME_WIDTH*MAX_VID_FRAME_HEIGHT*2地址范围内的,且长度肯定是大于MAX_VID_FRAME_WIDTH*MAX_VID_FRAME_HEIGHT,所以对他们执行:
memset(m_tLastFrm.pAddr[1], 0x80, MAX_VID_FRAME_WIDTH * MAX_VID_FRAME_HEIGHT);
memset(m_tLastFrm.pAddr[2], 0x80, MAX_VID_FRAME_WIDTH * MAX_VID_FRAME_HEIGHT);
所以,分析到此处,这三个对象均不会越界。
后来发现,问题是出在下面的下面的CpyVidFrm接口中,在该接口中会重新对m_tLastFrm.pAddr[1]和m_tLastFrm.pAddr[2]两个指针变量进行赋值,正是这个函数中的重新赋值,引发了memset时的崩溃。CpyVidFrm接口的实现代码如下所示:
// 将WebRTC回调过来的视频I420 YUV视频数据拷贝到本模块的内存中
int CpyVidFrm(VIDEO_FRAME* pSrc, VIDEO_FRAME* pDst)
{
if (!pSrc || !pSrc->pAddr[0] || !pSrc->pAddr[1] || !pSrc->pAddr[2] || \
!pDst || !pDst->pAddr[0] || !pDst->pAddr[1] || !pDst->pAddr[2] || \
!pSrc->dwWidth || !pSrc->dwHeight)
{
MError("CpyVidFrm Error\n");
return -1;
}
pDst->dwWidth = pSrc->dwWidth;
pDst->dwHeight = pSrc->dwHeight;
pDst->enPixelFormat = pSrc->enPixelFormat;
pDst->enPercent = pSrc->enPercent;
pDst->isLeftRight = pSrc->isLeftRight;
pDst->dwTimeRef = pSrc->dwTimeRef;
pDst->dwReserve = pSrc->dwReserve;
//memcpy(pDst->dwStride, pSrc->dwStride, 3 * sizeof(u32));
pDst->dwStride[0] = pSrc->dwStride[0];
pDst->dwStride[1] = pSrc->dwStride[1];
pDst->dwStride[2] = pSrc->dwStride[2];
// WebRTC开源库回调的视频数据是I420 YUV数据,占比是4:1:1,目前最大分辨率是1080P视频数
// 据(1920*1080),以最大的1080P视频尺寸为例,Y分量的宽是1920,即Y分量的1920*1080占用
// 其中的4份空间,U和V的分量共占用2份,所以之前申请的1920*1080*2的内存空间是够用的,最
// 开始的那个1920*1080空间存放Y分量用,余下的1920*1080内存空间存放U和V分量数据,足够了
// 实际只会使用1920*1080/2的内存空间(MAX_VID_FRAME_WIDTH=1920,MAX_VID_FRAME_HEIGHT=1080)
// 此时的pSrc->dwStride[0]应该是1920
memcpy(pDst->pAddr[0], pSrc->pAddr[0], pSrc->dwStride[0] * pSrc->dwHeight); // 拷贝Y分量数据
// pDst->pAddr[0]已经占用了MAX_VID_FRAME_WIDTH* MAX_VID_FRAME_HEIGHT内存,pDst->pAddr[1]使用
// 接下来的内存区域,正好可以占到MAX_VID_FRAME_WIDTH* MAX_VID_FRAME_HEIGHT长度的内存,所以对
// pDst->pAddr[1]执行memset(m_tLastFrm.pAddr[1], 0x80, MAX_VID_FRAME_WIDTH * MAX_VID_FRAME_HEIGHT)
// 是不会有内存越界的
pDst->pAddr[1] = pDst->pAddr[0] + pSrc->dwStride[0] * pSrc->dwHeight;
memcpy(pDst->pAddr[1], pSrc->pAddr[1], pSrc->dwStride[1] * pSrc->dwHeight / 2); // 拷贝U分量数据
// 但pDst->pAddr[2]现对于pDst->pAddr[1]做了内存偏移,那么pDst->pAddr[2]可用的内存将小于
// MAX_VID_FRAME_WIDTH* MAX_VID_FRAME_HEIGHT长度,所以对pDst->pAddr[2]执行
// memset(m_tLastFrm.pAddr[2], 0x80, MAX_VID_FRAME_WIDTH * MAX_VID_FRAME_HEIGHT)操作,会
// 产生内存越界
pDst->pAddr[2] = pDst->pAddr[1] + pSrc->dwStride[1] * pSrc->dwHeight;
memcpy(pDst->pAddr[2], pSrc->pAddr[2], pSrc->dwStride[2] * pSrc->dwHeight / 2); // 拷贝V分量数据
return 0;
}
具体为什么会导致后续的memset会产生越界,见上述代码注释部分的详细说明。
本问题的解决办法是,在对m_tLastFrm.pAddr[1]和m_tLastFrm.pAddr[2]这两个指针指向的内存进行memset时,不能固定地使用MAX_VID_FRAME_WIDTH * MAX_VID_FRAME_HEIGHT长度,应该使用实际的U和V分量的长度。
4、加载系统库的pdb之后,看到了更多的函数调用堆栈,看到发生异常的接口的调用
函数调用堆栈中只看到了CVideoPlay::SetPlayInfo接口,没有看到该接口中调用的CPlayer::SetPlayInfo接口调用记录,这有点奇怪,调用堆栈直接从CVideoPlay::SetPlayInfo接口跳到memset接口的调用,好像直接跳过了CPlayer::SetPlayInfo接口。难道是CPlayer::SetPlayInfo接口在编译时被优化掉了?
但CPlayer::SetPlayInfo接口内部的实现代码比较长,不太可能被编译器优化掉的。后来使用IDA工具打开当前模块的dll文件查看汇编代码,汇编代码中有去call这个CPlayer::SetPlayInfo接口的:
所以可以确定对CPlayer::SetPlayInfo接口的调用在编译时并没有被优化掉。
后来想到,是不是没有没有加载系统库的pdb引起的?以前就发现,有时如果加载了系统pdb文件,可能会显示更多的函数调用堆栈。如果设置了微软系统pdb在线服务器地址,Windbg会自动去微软服务器上下载对应版本的pdb文件并加载进来。于是在Windbg的符号库路径中追加系统pdb在线下载路径:
微软系统pdb在线服务器地址的配置格式如下:
srv*f:\mss0616*http://msdl.microsoft.com/download/symbols
其中,http://msdl.microsoft.com/download/symbols是在线系统库pdb下载服务器地址,f:\mss0616为下载系统库pdb文件时的临时存放目录。
设置系统pdb下载服务器地址之后,Windbg自动去下载系统库的pdb文件并加载进来,果然看到了更多的函数调用堆栈,如下所示:
在上述函数调用堆栈中看到了CPlayer::SetPlayInfo接口的调用。有了这个接口,看到了具体的代码行号:
01 158af7dc 797b7492 xxxxxplayer!CPlayer::SetPlayInfo+0x1dc [k:\xxxxxx\xxxxxx\xxxxxxplayer\source\player.cpp @ 852]
对应的player.cpp文件的852行如下所示:
就是我们上面分析的这行产生的异常,这和我们分析的完全一致。有了更详细的函数调用堆栈,可以更快更精准的定位问题。本例中是执行memset时有内存越界,如果不是memset引发的,函数调用堆栈中不显示CPlayer::SetPlayInfo接口调用,定位可能会更麻烦一点。
5、设置系统库pdb文件在线下载服务器地址去加载系统库pdb的好处
加载系统库的pdb符号库文件之后,不仅能看到具体的系统接口的调用,甚至还可以看到更多行的函数调用。看到的可能是更多的系统层函数的调用记录,也可能是更多的应用层函数的调用记录,本例中就看到了更多的应用层函数的调用。有时从具体的系统函数中能得到一定的线索。看到更多行的函数调用,可能更有利于快速定位发生崩溃的具体位置,在本例中,如果看不到中间函数CPlayer::SetPlayInfo,可以通过memset函数的调用去他估计,如果没有memset调用,要确定发生异常的具体位置可能要困难一些。
6、最后
本文详细讲解了执行memset操作时遇到的内存越界问题的详细排查过程,问题的排查过程及相关细节有一定的参考或借鉴价值。