上一节,我们已经知道split_config.arm64_v8a.apk背后的含义,我们再整理下崩溃日志:

# GLThread 1371(29457) SIGSEGV(SEGV_MAPERR)
 #00 pc 0000000000aa3f58 split_config.arm64_v8a.apk
 #01 pc 0000000000aafb84 split_config.arm64_v8a.apk

GLThread 1371(29457)表示这是一个OpenGL相关的线程,线程ID为1371,进程ID为29457,这里又牵扯到线程和进程的概念,涉及到forkthread相关的编程知识,这里就不再展开了。

我们又发现一个知识盲区,就是这个SIGSEGV(SEGV_MAPERR),很有必要解读下这块的知识点。

SIGSEGV

当程序尝试访问超出其分配内存区域、未分配内存区域的地址时,操作系统会发送这个SIGSEGV(Segmentation Violation)信号给程序,以表明发生了访问错误。

而括号里面的SEGV_MAPERR,准确说是一个错误码,告诉你是因为什么原因导致的异常,不过这个错误码非常晦涩难懂,只能给你一个排查方向。

接下来就详细介绍几个常见的错误码:

SIGABRT

产生这个错误的可能原因:

1. assert

在cocos2dx中有一个Marco:

CCASSERT(false, "xxx");

通常我们是用来检查某些情况是否符合预期,如果不符合,程序就会主动断言挂起,这个Marco的内部实现使用的就是assert函数。

一般都是用在debug模式,在发布release版本的时候,Marco是一个空实现。

2. abort:

abort函数会主动终止程序,同时发送信号SIGABRT,cocos2dx中也有使用该函数。

3. 重复释放指针,或者释放野指针

这种指针的问题最难排查了。

SEGV_ACCERR

通常是因为程序试图访问一个无权访问的内存地址,例如尝试读取受保护的内存区域或未分配的内存区域。

这种错误通常发生在程序访问数组越界或使用已释放的指针时。

SEGV_MAPERR

通常是因为程序试图访问一个不合法的内存地址,在遇到该错误时,一般会提供相关的错误信息和堆栈跟踪,可以利用这些信息定位错误所在的代码行,并进行排查和修复。

这也是我们经常会遇到的崩溃错误码,排查方向为:

  • 使用未初始化的变量:这个属于c++编码习惯问题,成员变量一定要在构造函数进行初始化。
  • 野指针:多个指针指向同一块内存,当这块内存释放时,需要同步给相关的指针,否则就会出现野指针。
  • 数组越界:本质还是指针。
  • 堆栈溢出:一般我们不会出现这种情况。

具体代码场景不是很好描述,我这里有一种情况可以参考理解下:

class A{
public:
 void func(){}
};

class B{
public:
  B(A* ptr){
    this->_a = ptr;
  }
  void callA(){
    this->_a->func();
  }
private:
  A* _a=nullptr;
};

B* b=nullptr;

void test()
{
  A a;
  b = new B(&a); // a是一个局部变量,test函数结束就会被回收释放,但是b还在持有
}
test();
b->callA(); // 此时a不为nullptr,但是调用a->func()就会crash

对象在使用时已经释放,通俗点说:对象A用局部变量初始化,分配在栈上,函数结束就提前释放了。赋值给其他对象B,B在其他函数引用A时,出现崩溃,这种问题排查起来也很坑。

小结

到这里我们对SIGSEGV有了一个大概的认识,SIGSEGV(SEGV_MAPERR)是我们遇到最多的崩溃信号量,当看到这个SEGV_MAPERR,大部分情况下都是内存管理的问题,访问了无效的内存导致的。