【2018-07-18】iOS崩溃问题复盘

1.问题出现

问题始于iOS运行时的崩溃,在多个版本的系统中均有出现,重现率较低,崩溃占比0.19%。

有46台iOS设备都出现了崩溃。然而蛋疼的是,由于是一个月前未解决的问题,所以只有程序闪退时的dump文件可以看,没有日志可以参考,再加上重现率低,所以入手点只有密密麻麻的抽象的线程crash的寄存器信息和运行的函数名,见下图。

【2018-07-18】iOS崩溃问题复盘

看到这些东西,差点晕了过去,找个bug还要会看寄存器汇编什么的吗?没办法,有问题就得解决,头铁吧!

2.着手分析

除了这句符合人类思维的源文件代码行号,我实在找不到这个dump的好处。

【2018-07-18】iOS崩溃问题复盘

好吧,只能用人肉编译器去看看这段代码附近有没有什么危险操作了。

到达FlvStreamHandler.cpp的716行发现,这是一行memcpy()操作的代码。得,最容易出现问题的函数来了!估计是哪个地方算错了,导致内存拷贝的时候出现了越界。那究竟是哪里了,人肉编译器启动,一步一步地看吧。

3.突破瓶颈

看了好久,终于把这个Flv的解析操作弄了个明白。我画了这张图:

【2018-07-18】iOS崩溃问题复盘

每次解包时,都会创建一个m_parsingFlvData指向TAG块的首地址(其实只是用std::tring来保存而已)。然而还是没有发现为啥子memcpy()出现了异常,每个计算出来的值似乎都是合理正确的。肯定的,要是计算方法出了问题,崩溃率也不会这么低。于是猜想估计是哪个值抽风了,不知道变异成什么数字了吧。

还是继续看哪个蛋疼的dump文件,这些寄存器保留的现场肯定是有用的,应该就保留着当时崩溃时的变量值!但是怎么确定哪个寄存器保存的是哪个变量的值呢?灵机一动,祭出了神器IDA!!!把静态库.a文件反汇编,看看C++代码到底会怎么用这些寄存器。为了看懂汇编,去了解了arm64指令集,了解寄存器的用途。终于又一道灵光闪过,看看我发现了什么!

【2018-07-18】iOS崩溃问题复盘 【2018-07-18】iOS崩溃问题复盘

没错!比照了半天,终于对上号了!所以pAudioPacket->m_dataLength就是W23寄存器,dataLen就是W22寄存器。而在arm64中,Wn寄存器就是Xn寄存器的低32位,所以再回看dump文件:

【2018-07-18】iOS崩溃问题复盘

原来如此,程序挂掉的时候pAudioPacket->m_dataLength == 0x00000003,dataLen == 0xfffffffc。

而memcpy(xxx, m_parsingFlvData.c_str(), dataLen),拷贝0xfffffffc这么多字节的内存,都不知道越界越哪儿去了,不挂掉才怪!

回去看705行,没错就是这里,0xfffffffc + 7,果断发生了整数上溢。所以算出来pAudioPacket->m_dataLength == 0x00000003.

为啥子dataLen会这么大,相当于4个G的大小,一个Flv的Tag不可能有这么大,网速都不支持呢。继续往上分析。

dataLen = m_parsingFlvData.length() - 4;

瞧瞧我又发现了什么: 如果 dataLen = 0xfffffffc,那么肯定是因为m_parsingFlvData.length() == 0。没错,抽风的就是这个string类型的变量。

为啥抽风了?它erase()了自己长度为 15 + uidSize 字节的部分,就删这么一点,怎么就删成0了,不应该吧?我想你已经猜到了,又又又又又发生了溢出,这次是下溢!uidSize如果是一个小于 -15 的负数,那么这个string就会erase一段超长的部分。你问我为什么string.erase()越界不报错?去问发明STL的人吧,你不管删多长,它就是不报错,最后会导致string.length() == 0。

症结原来在这里,在于uidSize错了,本来应该表示dse部分的uid个数,应该是个比较小的正整数。结果不知道是服务器没填这个值、还是填错了、还是在硬件路上抽风了(这个可能性不大),导致uid变成了有符号负数(无符号超大整数)。

4.得出结论

整数溢出、越界访问内存,真是C++开发人员这辈子的噩梦!

5.解决方案

6.总结