播放WebRTC开源库回调出来的视频码流时遇到的内存越界问题排查

news2024/10/5 21:24:52

目录

1、执行memset操作时遇到了内存访问违例,导致程序崩溃

2、查看崩溃时的函数调用堆栈,初步怀疑是memset时有内存越界    

3、存放YUV数据的buffer在执行若干操作后出现内存越界

4、加载系统库的pdb之后,看到了更多的函数调用堆栈,看到发生异常的接口的调用    

5、设置系统库pdb文件在线下载服务器地址去加载系统库pdb的好处    

6、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N4P3https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N4P3https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具案例集锦(专栏文章正在更新中...)icon-default.png?t=N4P3https://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操作时遇到的内存越界问题的详细排查过程,问题的排查过程及相关细节有一定的参考或借鉴价值。
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/648900.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录二刷 day23 | 二叉树 之 669. 修剪二叉搜索树 108.将有序数组转换为二叉搜索树 538.把二叉搜索树转换为累加树

二刷day23 669. 修剪二叉搜索树108.将有序数组转换为二叉搜索树538.把二叉搜索树转换为累加树 669. 修剪二叉搜索树 题目链接 解题思路: 递归三部曲 确定递归函数的参数以及返回值 这里我们为什么需要返回值呢? 因为是要遍历整棵树,做修改…

SOLIDWORKS Electrical如何绘制电线

SOLIDWORKS Electrical 是一套计算机辅助工程 (CAE) 设计工具,他可以帮助电气设计工程师减少创新的内在风险,并允许公司减少对物理原型的需求,从而在更短时间内以更低成本将产品推向市场。通过一组强大、直观的电气设计功能,设计人…

互联网大佬们的成功绝非偶然

以下排名不分先后。 一、百度李彦宏 ,1987年阳泉市高考状元,考进北大。 互联网三巨头BAT的第一个字母,指的就是百度。创始人李彦宏是出生在普通家庭出生,他小时候的启蒙教育由三个姐姐负责,不过相比学习,更…

详解国产指纹芯片的工作原理以及应用领域

指纹芯片,是指内嵌指纹识别技术的芯片产品,能够片上实现指纹的图像采集、特征提取、特征比对的芯片,开发者可以方便的实现指纹识别的功能,大大降低了指纹识别行业的门槛,对指纹识别的推广具有十分积极的推动作用。 近几…

数据合规之监管关注要点暨风险防范建议

上市申报过程中证券监督部门最关注的三类问题为数据源合规、数据安全及数据使用合规,而这三类问题也是企业运营时最容易产生的问题,它们无疑应是拟上市企业数据合规治理的重中之重: 1、数据源合规之监管关注要点 1、数据信息 就获取的个人数…

RaaS(勒索软件即服务)是什么?这个模型是如何工作的?

Ransomware as a Service是一个英语术语,指的是一种商业模型,其中勒索软件开发者向感兴趣的恶意行为者提供工具,以便他们可以发起勒索软件攻击。使用者通过签约创建恶意软件即服务或加入联盟计划,并分发一系列勒索软件以换取一定比例的利润。…

字符串--字符串处理函数、向函数传递字符串

一、字符串处理函数 字符串处理函数库提供了很多有用的函数用于字符串处理操作&#xff08;如复制字符串和拼接字符串等&#xff09;以及确定字符串的长度。若要使用这些字符串处理函数&#xff0c;必须在程序的开头将头文件<string.h>包含到源头文件。 例题1&#xff1a…

什么是Vue的前端微服务架构(Micro Frontends)?

什么是Vue的前端微服务架构&#xff08;Micro Frontends&#xff09;&#xff1f; 前端微服务架构&#xff08;Micro Frontends&#xff09;是一种新型的前端架构风格&#xff0c;它借鉴了后端微服务架构的思想&#xff0c;将前端应用程序拆分为多个小型、独立的部分&#xff…

系统学习Spring,阿里最新产Spring全家桶进阶笔记真的很全

Spring是我们Java程序员面试和工作都绕不开的重难点。很多粉丝就经常跟我反馈说由Spring衍生出来的一系列框架太多了&#xff0c;根本不知道从何下手&#xff1b;大家学习过程中大都不成体系&#xff0c;但面试的时候都上升到源码级别了&#xff0c;你不光要清楚了解Spring源码…

混沌演练状态下,如何降低应用的MTTR(平均恢复时间) | 京东云技术团队

在企业业务领域&#xff0c;锦礼是针对福利、营销、激励等员工采购场景的一站式解决方案&#xff0c;包含面向员工、会员等弹性激励SAAS平台。由于其直接面向公司全体员工&#xff0c;其服务的高可用尤其重要&#xff0c;本文将介绍锦礼商城大促前夕&#xff0c;通过混沌工程实…

vue3 项目部署,Nginx配置https,重定向,详细流程

文章目录 前情提要应用场景安装使用 实战解析最后 前情提要 一个web项目完成后&#xff0c;我们需要打包部署上线&#xff0c;关于打包的实战在我的vite专栏里已经有过一些实践&#xff0c;今天我们来实践一些部署的过程&#xff0c;当然部署也可以由后端来完成&#xff1b; 应…

去面腾讯了(社招两年面试经验)

之前很多同学嚷嚷有没有社招经验&#xff0c;正好&#xff0c;我有个朋友去腾讯社招面试了。 他的面的是全栈开发岗位&#xff0c;工作两年&#xff0c;后端是Go&#xff0c;前端是 JavaScript Vue。 因为工作也没多久&#xff0c;就两年时间&#xff0c;所以大概率可能还是…

Linux:用户和用户组

Linux系统中可以&#xff1a; 配置多个用户配置多个用户组用户可以加入多个用户组 Linux系统关于权限的管控级别有两个&#xff0c;即&#xff1a; 针对用户的权限控制针对用户组的权限控制 1、用户组管理 创建用户组&#xff0c;语法&#xff1a; groupadd 用户组名 删除用户组…

NLP——Summarization

文章目录 Extractive summarisationSingle-documentcontent selectionTFIDF MethodLog Likelihood Ratio Method对数似然比Sentence Centrality Method 句子中心法 RST Parsing Multi-documentContent selectionMaximum Marginal Relevance 最大边际相关性Information Ordering…

详细介绍ROS中通过shell文件依次启动多个launch文件

本文主要介绍如何在ROS中通过shell文件依次启动多个launch文件&#xff0c;并介绍如何在同一个窗口的不同选项卡中依次启动多个launch文件。 一、先来看一个简单的示例&#xff1a; #!/bin/bashgnome-terminal -- bash -c "roslaunch bringup racecar_gazebo_rviz_znc.la…

Word控件Spire.Doc 【其他】教程(9):从 Word 文档中提取 OLE 对象

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

NestJs 管道(Pipe)

&#x1f384;Hi~ 大家好&#xff0c;我是小鑫同学&#xff0c;资深 IT 从业者&#xff0c;InfoQ 的签约作者&#xff0c;擅长前端开发并在这一领域有多年的经验&#xff0c;致力于分享我在技术方面的见解和心得 &#x1f680;技术&代码分享 我在 94Code 总结技术学习&…

Windows下安装运行Kafka(最底下有遇到的坑与解决方法)

注&#xff1a;安装kafka需要提前安装Zookeeper 一、Zookeeper安装 1. 下载安装包 https://zookeeper.apache.org/releases.html 注意&#xff1a;要下载带bin的安装包 2. 解压并进入ZooKeeper目录&#xff0c;如&#xff1a;D:\onworking\apache-zookeeper-3.7.0-bin&…

ansible剧本模式特殊模块使用

Nginx安装剧本 ansible-playbook test1.yaml //补充参数 -k&#xff08;-ask-pass&#xff09;&#xff1a;用来交互输入ssh密码 -K&#xff08;-ask-become-pass&#xff09;&#xff1a;用来交互输入sudo密码 -u&#xff1a;指定用户 -e:命令行指定变量 --syntax-check…

【服务器数据恢复】HP LeftHand存储raid5不可用的数据恢复案例

HP LeftHand存储简介&#xff1a; HP LeftHand存储支持搭建RAID5、RAID6、RAID10磁盘阵列&#xff0c;支持卷快照&#xff0c;卷动态扩容等。服务端和客户端分别如下&#xff1a; LeftHand存储共有三个级别&#xff1a;物理磁盘、基于多个物理磁盘组成的逻辑磁盘&#xff08;ra…