目录
1、问题说明
2、代码段地址与数据段地址
3、使用空指针调用BindWindow函数(非虚函数),没有崩在BindWindow函数的调用处,而是崩在函数内部
3.1、虚函数调用的二次寻址
3.2、崩溃在被调用函数内部
4、总结
C++软件异常排查从入门到精通系列教程(核心精品专栏,订阅量已达8000多个,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++实战专栏(重点专栏,专栏文章已更新500多篇,订阅量已达6000多个,欢迎订阅,持续更新中...)
https://blog.csdn.net/chenlycly/article/details/140824370C++ 软件开发从入门到实战(重点专栏,专栏文章已更新300多篇,欢迎订阅,持续更新中...)
https://blog.csdn.net/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)
https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_2276111.html 同事那边最近遇到了一个空指针引发的崩溃,使用空指针调用一个成员函数,但并没有崩溃在函数调用处,而是崩溃在被调用函数内部。同事搞不懂这个细节,找到我,让我帮忙看看,给详细讲解一下具体的原因。今天我就把相关的细节详细说明一下,以供大家参考。
1、问题说明
最近同事遇到一个关于程序崩溃点的问题,找到我,想问一下是原因。发生崩溃的代码上下文如下所示:
代码中使用iter.second.pVideDec指针调用了BindWindow函数。同事可以确定的是,iter.second.pVideDec指针的值为NULL导致的崩溃,但令同事感到疑惑的是,为什么没有崩溃在使用空指针调用BindWindow函数的函数调用处,而是崩溃在BindWindow函数内部(代码运行到BindWindow函数内部才触发了崩溃)?更进一步,进入BindWindow函数内部后,没有去call函数CallMedia就发生了崩溃,这又是什么原因呢?
这两个问题其实很简单,但实际上大部分C++开发人员并不懂,也讲不清楚!
2、代码段地址与数据段地址
我们在看问题之前,我们需要弄清楚代码段地址与数据段地址的区别。C++程序在运行时所占用的内存区域,一般可分为栈内存区、堆内存区、全局/静态内存区、文字常量内存区及程序代码区5大分区,如下所示:
程序中变量占用的内存属于数据段内存区,包含栈内存区、堆内存区、全局/静态内存区、文字常量内存区。
程序启动时会将依赖的各个二进制模块以及程序主模块加载到进程空间中,这些二进制模块要占用内存空间,这些内存空间是从进程的内存空间划拨的。具体的是,二进制模块中的二进制代码占用内存空间,这些内存空间中存放的都是二进制代码,所以叫代码段内存区。C++代码中调用函数,从汇编代码的角度去看,就是去call函数的地址(代码段地址)。程序一旦启动起来,加载起来的所有模块中的函数地址就确定下来了。
关于C++程序的五大内存分区的详细说明,可以查看我的文章:
实例详解C++程序的五大内存分区https://blog.csdn.net/chenlycly/article/details/120958761
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:(该精品技术专栏的订阅量已达到10000多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,已经更新到210篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,详细介绍分析C++软件问题的常用分析工具,以图文并茂的方式给出具体的项目问题实战分析实例(详细讲述分析排查过程,很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到300篇以上!
专栏2:(本专栏涵盖了C++多方面的内容,是当前重点打造的专栏,订阅量已达8000多个,专栏文章已经更新到500多篇,持续更新中...)
C/C++实战进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、数据结构与算法、C++11及以上新特性(开源代码中可能会用到很多新特性(比如WebRTC开源库),日常编码中也会用到部分新特性,面试时也会频繁地涉及到,学习新特性很有必要)、常用C++开源库的介绍与使用(比如SQLite、libcurl、libwebsockets、libevent、jsoncpp/RapidJson、Redis、RabbitMQ、MongoDB、MQTT、ZooKeeper、OpenCV、FFmpeg、SDL、GStreamer、Live555、ReactOS等)、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(引发C++软件异常的常见原因分析与总结、排查C++软件异常的手段与方法、分析C++软件异常的基础知识、使用常用软件分析工具分析C++软件问题、多个项目实战问题分析案例分享等)、设计模式(单例模式、工厂模式、观察者模式、状态模式等)、网络基础知识与网络问题分析进阶内容(实战问题分析实例分享)等。本专栏的内容都是建立在项目实践的基础上,来源于项目实战,服务于项目实战,很有实战参考价值!
专栏3:
C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795
常用的C++软件辅助分析工具有SPY++、PE工具、Dependency Walker、GDIView、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏4:
VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:
C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了C/C++软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
3、使用空指针调用BindWindow函数(非虚函数),没有崩在BindWindow函数的调用处,而是崩在函数内部
使用空指针去调用BindWindow函数,是去call函数代码段地址,这个操作不会导致崩溃,因为这个过程中没访问不该访问的内存。BindWindow函数在程序启动起来后就确定了,去call这个函数地址没有问题,不会发生异常。但如果这个BindWindow是虚函数,则会崩溃在函数调用处。
3.1、虚函数调用的二次寻址
如果这个BindWindow是虚函数,调用虚函数时要找到该虚函数的地址然后去call这个地址完成虚函数调用,在获取虚函数地址的过程中会涉及到二次寻址:(此处讲的寻址,是从汇编的角度去看的,从汇编代码的角度可以清晰地看到二次寻址的完整过程)
1)读出类对象中虚函数表指针中的值,就是虚函数表的首地址,这是第一次寻址。读取虚函数表指针变量的值,就是读取虚函数表指针变量所在内存中的内容,首先要拿到虚函数表指针变量的内存地址,有了地址,才能读取内存中的内容。对于C++类(对象),从类中成员变量的内存排布上看,作为成员变量的虚函数表指针变量是排在首位的,所以虚函数表指针变量的首地址,就是当前C++类对象的首地址。如果当前C++对象地址为NULL,就会把NULL作为虚函数表指针的首地址去读取虚函数表指针的值,就会访问地址为0的内存,这个小地址是禁止访问的,所以会触发内存访问违例引发崩溃。
2)第一步中拿到虚函数表的首地址,找到虚函数表的位置,然后根据目标虚函数在当前类对象中的排位,得到地址偏移,然后到虚函数表中找到目标虚函数的地址,然后去call这个地址就完成虚函数调用了。这是第二次寻址。
关于虚函数调用的二次寻址的详细说明,可以查看我的文章:(从汇编的角度去看)
几秒读懂C++虚函数调用的汇编代码实现https://blog.csdn.net/chenlycly/article/details/121046234
3.2、崩溃在被调用函数内部
上面讲了,程序没有崩溃在BindWindow函数的调用处,这样代码就运行到这个BindWindow函数内部了。
BindWindow函数内部并没有做什么复杂的事情,就是去调用另一个函数CallMedia。程序的崩溃发生在调用CallMedia的这行代码上,为什么呢?因为在调用CallMedia函数时访问当前类的成员变量m_dwChanId,把该成员变量作为参数传递给CallMedia,这就意味着要访问到成员变量m_dwChanId,那具体是怎么访问到m_dwChanId成员变量的呢?还是要从汇编代码的角度去看,因为成员变量m_dwChanId是调用CallMedia的一个参数,所以在call这个CallMedia函数之前需要将成员变量m_dwChanId的值从内存中读出来,然后将该值push压到栈上,通过栈传递给被调用函数CallMedia。而当前传入的C++类对象的地址为NULL,而做成类成员变量的m_dwChanId的地址是相对所在类对象地址的偏移,所以此时m_dwChanId变量的首地址是个很小的地址,而很小的地址是禁止访问的,从而引发内存访问违例,导致程序崩溃。
上面我们多次讲到小地址内存区,关于小地址内存区的详细说明,可以查看我的文章:(在文章的第8节有详细的说明)
引发C++程序内存错误的常见原因分析与总结https://blog.csdn.net/chenlycly/article/details/139596707如果BindWindow函数中没有访问所在类的成员变量的值,只是访问函数中的局部变量,则不会导致内存访问违例,不会引发程序崩溃。
上面我们多次讲到了从汇编代码的角度去看问题分析问题,对于C++程序员,是很有必要了解并学习汇编。汇编代码不仅能帮我们理解很多高级语言不好理解的代码执行细节,而且可以辅助分析排查C++软件异常问题。关于为什么要学习汇编代码以及如何学习汇编,可以查看我的文章:
C/C++程序员为什么要了解汇编?了解汇编有哪些好处?如何学习汇编?https://blog.csdn.net/chenlycly/article/details/142795872
4、总结
本例中遇到的问题场景,大部分C++开发人员搞不清楚引发崩溃的根本原因,只是知道空指针或者野指针可能会引发程序崩溃(访问空指针或野指针可不是一定会引发程序崩溃的,要从内存的角度的分析)。但作为有经验的开发人员,是需要搞清楚引发崩溃的根本原因的,必要时要结合汇编代码去理解去分析。
有些问题看上去似乎很简单,甚至理所当然,但是进一步深究的话,可能可以挖掘出一些细节的!遇到问题要多思考!