Catalog
  1. 1. 描述
  2. 2. 原因
  3. 3. 测试 Demo
  4. 4. 总结
一行注释引发的惨案

这场惨案的代价是,浪费了一个同事2个多月的工作量,不可谓不惨重。这次事件,我也明白了一个道理。如果触及到知识边界了,除了去补这块的知识,否则无论花多少时间,做多少调试,都无法解决这样的问题。这也更加坚定了我的想法,作为一个开发者,至少计算机相关的基础知识一定要扎实并且深入, 这些东西可能用的不是那么频繁,但是是非常有用的,这应该是一个开发者的基本素养。

描述

大概描述一下问题。我们组有个搞安卓的小伙伴,一个问题卡了非常久,我实在是等不下去了,就去帮他一起搞。经过排查下来后,发现这样的一个现象。项目使用到一个动态库,我们叫他 A.so 吧。
A 有个对象, 就叫他 ASon,ASon 有很多方法。现象是这样的,调用 ASon::FuncA() 是正常的,但是调用 ASon::FuncB()确不正常。经过我仔细的排查,我非常确定是动态库函数函数地址不对导致的,但是我定位不到原因。多次尝试之后,我们发现,我们自己的工程中包含的 ASon.h 文件与动态库源码中的 ASon.h 头文件有一点差异,一行虚函数的声明被注释了(原则上讲,不应该出现这种情形,但是现实就出现了这种奇葩事情,整个项目代码是其他人提供的, 也不知道为何会去修改三方库的头文件)。我们把那行注释放开,问题就解决了。但是这个现象我目前还无法理解,所以我要研究一下(写到这里的时候,我还是一脸懵逼,时间问题,先记录一下,等待后面有时间了破案)。

原因

看了部分资料后,我确定是虚函数的问题了。就遇到的情形来说,涉及到虚函数表以及函数在虚表中的位置有关。
这里不讨论虚表相关的概念,相关的文章也有很多。大家也都知道,有虚函数的类会有一张对应的虚表,实际上是一个指针数组,保存了这个类中虚函数的地址。我就有一个问题,如果只是指针数组的话,那么调用虚函数时是如何在表中索引的,并且我们的问题很有可能与这个有关。查了一些资料,了解到原理是这样的:

1
2
3
4
5
// main.cpp 中的代码
f->Name()

// 虚拟函数的调用会被转变
(*(f->vptr[0]))(f)

索引 0 表示 Name() 在 virtual table 中的固定索引。那么还有一个问题, 这个 0 是如何得来的。我在网上找了一下,并且也看了一些书,并没有找到相关的说明。但是通过测试结果来看,这个索引值应该是按照声明顺序来的。

测试 Demo

为此我特意写了一个 Demo。有兴趣的可以拉下来测试一下。我在这里贴一下,不同情形下的不同结果。

原始代码的结果

1
2
3
4
5
son
170 cm
hello I am nobody
a = 998
a = 0

可以手动注释 main.cpp 包含 的 father.h Weight 后, 在看一下结果

1
2
3
4
5
son
100kg
hello I am nobody
a = 998
a = 0

可以看到, 只有虚函数部分的调用出现了问题。因为, 修改后, Height() 函数的索引变成了 1, 而在动态库的虚函数表中,索引为 1 的函数指针实际上是 Weight() 函数。

总结

所以结论就是这样的。虚表是在动态库里的,其虚函数表槽中的函数的地址,在编译期间也已经确定下来了,这个是没有问题的。而调用动态库的代码,在编译时,虚函数的调用会被重新改写为上面所说的,以索引值来索引函数的真实地址,而这个索引值是由声明顺序确定的, 也就是被调用方包含的头文件中的声明。那就很好理解了,如果有一个虚函数被注释了,那么它后面的所有的虚函数的索引都会改变,而与库中的索引产生了偏差,最后导致函数调用错误了。

写的比较粗糙,没有给出更多的资料以及测试结果来做支撑,有错误请提出。

Author: 42
Link: http://blog.ikernel.cn/2020/03/16/%E4%B8%80%E8%A1%8C%E6%B3%A8%E9%87%8A%E5%BC%95%E5%8F%91%E7%9A%84%E6%83%A8%E6%A1%88/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment