使用 ndk-stack 寻找Android程序Crash的原因

版权声明:本文为博主原创文章,转载请标明出处 ^M^。 https://blog.csdn.net/change_from_now/article/details/53436581

开篇废话

很久不研究cocos2d-x了,也不知道如今发展如何了。先前写游戏时会分几块。

  • 主要功能代码都是用C++编写,编辑器用vs
  • android平台相关功能使用 Java 编写,编辑器用 Eclipse
  • ios平台相关功能使用 Object-C 编写,编辑器用 Xcode

基本流程就是先在 VS上开发主要功能,等测试通过了,然后再针对不同平台做接入。所以调试方面

  • win32工程,可以在VS中调试
  • Xcode工程,可以在Xcode中调试
  • Android工程,无法调试

集成so包的Android工程是无法在eclipse中调试的,这里的调试主要是指在c/c++端打断点调试。只能通过logcat看输出。比较难搞的bug是,win32工程运行正常,但是打包装到android手机上运行时就崩溃。基本崩溃输出如下:一堆的堆栈信息。

I/DEBUG   (   31): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG   (   31): Build fingerprint: 'generic/google_sdk/generic/:2.2/FRF91/43546:eng/test-keys'
I/DEBUG   (   31): pid: 351, tid: 351  %gt;%gt;%gt; /data/local/ndk-tests/crasher <<<
I/DEBUG   (   31): signal 11 (SIGSEGV), fault addr 0d9f00d8
I/DEBUG   (   31):  r0 0000af88  r1 0000a008  r2 baadf00d  r3 0d9f00d8
I/DEBUG   (   31):  r4 00000004  r5 0000a008  r6 0000af88  r7 00013c44
I/DEBUG   (   31):  r8 00000000  r9 00000000  10 00000000  fp 00000000
I/DEBUG   (   31):  ip 0000959c  sp be956cc8  lr 00008403  pc 0000841e  cpsr 60000030
I/DEBUG   (   31):          #00  pc 0000841e  /data/local/ndk-tests/crasher
I/DEBUG   (   31):          #01  pc 000083fe  /data/local/ndk-tests/crasher
I/DEBUG   (   31):          #02  pc 000083f6  /data/local/ndk-tests/crasher
I/DEBUG   (   31):          #03  pc 000191ac  /system/lib/libc.so
I/DEBUG   (   31):          #04  pc 000083ea  /data/local/ndk-tests/crasher
I/DEBUG   (   31):          #05  pc 00008458  /data/local/ndk-tests/crasher
I/DEBUG   (   31):          #06  pc 0000d362  /system/lib/libc.so
I/DEBUG   (   31):

看到这一堆输出,基本很无头绪。这时就要用到 ndk 中的 ndk-stack 了。

正文

想要搞懂本篇内容,需要了解或者熟悉的知识。

  • 熟悉ndk
  • 了解cocos2d-x使用ndk 生成so包的过程
  • 熟悉android开发中的logcat
  • 了解android系统

过程

简单说明和解释

在生成apk之前,会通过ndk的 ndk-build 命令将c/c++源码编译成一个libxxx.so命名的共享库,共享库是在 Linux系统中使用的。
工程中有一个jni的文件夹,这个是每个使用ndk编译都要有的目录,里面主要有两个文件 Android.mk Application.mk 和你的c++源码。具体要编译那个c++源码是在Android.mk配置的,编译的cpu类型,设置相关是在Application.mk中设置的。
当编译成功后,工程中会生成两个目录,一个是libs,一个是obj。
libs中就是最终生成的so文件,不同平台的会放到不同文件夹里,常见的如 armeabi-v7a,x86。
obj中是一堆.a库文件。我们需要的就是obj目录下的.a库文件。

操作过程

为了方便操作,可以把obj拷贝到一个独立目录里,比如在工程根目录新建一个debug目录,然后将obj文件夹拷贝进去。
接下来让程序崩溃一次,获取崩溃日志。这里拿cocos2d-x的Hello World工程做实验。最简便的方式是将HelloWorldScene.cpp 中读取的 HelloWorld图片名字改一下,比如将 HelloWorld.png 首字母小写改成 helloWorld.png。这个图位于工程根目录下的Resources中。如果你运行win32 的话,程序还是会正常运行,重新打包安装到真机上运行就会崩溃。这是因为Android中文件命名是区分大小写的。最终崩溃log如下。

12-02 06:08:08.638 1193-1193/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
12-02 06:08:08.643 1193-1193/? A/DEBUG: Build fingerprint: 'Android/sdk_google_phone_x86_64/generic_x86_64:6.0/MASTER/2872745:userdebug/test-keys'
12-02 06:08:08.643 1193-1193/? A/DEBUG: Revision: '0'
12-02 06:08:08.644 1193-1193/? A/DEBUG: ABI: 'x86'
12-02 06:08:08.644 1193-1193/? A/DEBUG: pid: 2886, tid: 2906, name: GLThread 37  >>> com.good.xb <<<
12-02 06:08:08.646 1193-1193/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
12-02 06:08:08.665 1193-1193/? A/DEBUG:     eax 00000000  ebx e3c92524  ecx 00000000  edx 00000001
12-02 06:08:08.666 1193-1193/? A/DEBUG:     esi e3c92524  edi e3426f60
12-02 06:08:08.666 1193-1193/? A/DEBUG:     xcs 00000023  xds 0000002b  xes 0000002b  xfs 0000005f  xss 0000002b
12-02 06:08:08.666 1193-1193/? A/DEBUG:     eip e33f7c35  ebp e2eb2378  esp e2eb2210  flags 00210286
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #00 pc 00346c35  /data/app/com.good.xb-2/lib/x86/libMyGame.so (HelloWorld::init()+1861)
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #01 pc 00346432  /data/app/com.good.xb-2/lib/x86/libMyGame.so (HelloWorld::create()+210)
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #02 pc 00346330  /data/app/com.good.xb-2/lib/x86/libMyGame.so (HelloWorld::createScene()+48)
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #03 pc 003461c1  /data/app/com.good.xb-2/lib/x86/libMyGame.so (AppDelegate::applicationDidFinishLaunching()+977)
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #04 pc 003494cf  /data/app/com.good.xb-2/lib/x86/libMyGame.so (cocos2d::Application::run()+47)
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #05 pc 003505f3  /data/app/com.good.xb-2/lib/x86/libMyGame.so (Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit+307)
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #06 pc 000b1e76  /data/app/com.good.xb-2/oat/x86/base.odex (offset 0x67000) (void org.cocos2dx.lib.Cocos2dxRenderer.nativeInit(int, int)+122)
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #07 pc 000b3244  /data/app/com.good.xb-2/oat/x86/base.odex (offset 0x67000) (void org.cocos2dx.lib.Cocos2dxRenderer.onSurfaceCreated(javax.microedition.khronos.opengles.GL10, javax.microedition.khronos.egl.EGLConfig)+88)
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #08 pc 72989440  /data/dalvik-cache/x86/system@framework@boot.oat (offset 0x1eb2000)
12-02 06:08:08.706 1193-1193/? A/DEBUG:     #09 pc 000f6cb7  <unknown>

截图如下
这里写图片描述

我是用AndroidStudio中模拟器跑的程序。如果使用Eclipse的话,这段log应该是绿色的。仔细看这段log,会发现有两个特征

  1. Tag 名字是 DEBUG
  2. 以* * * * * * 开头

为了方便操作,将这段log复制到一个txt文本中。回到我们先前的debug目录,新建一个log.txt文本,将崩溃log复制进来,保存。
此时我们就有了查找bug的两个必备材料了,接下来就可以使用ndk-stack命令了。

ndk-stack 命令使用格式

$NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi
$NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi -dump foo.txt

解释一下:
$NDK代表ndk所在的路径。
$PROJECT_PATH 代表你工程路径
obj/local/armeabi 代表你编译的cpu类型,针对不同的cpu可以换成armeabi-v7a,x86等等。这些名字都可以在obj/local/下看到。

可以在命令行模式下执行ndk-stack命令。第一个命令是用于log在cmd面板的情况。即使用
adb logcat 命令启动命令行打印log的情况。这时使用ndk-stack 命令,可以将获取的结果显示到cmd面板。这个操作意义不大,忽略。
第二个命令是读取存有log的文本文件,这个是我们要使用的。

为了方便操作,可以将ndk的目录配置到path环境变量中,这样就可以在任意目录使用ndk-stack命令了。
P.S.在Path中配置ndk路径时,如果失败,可以在最后加一个斜线 \,比如我的 E:\android-ndk-r10c\

执行命令

因为我们会在obj所在的根目录执行 ndk-stack 命令,所以最后的命令要简单一些。
这里写图片描述

ndk-stack -sym obj/local/x86 -dump log.txt

因为我的模拟器是x86的,所以我local下选的是x86 。
执行成功后就会看到一大串输出。这个看起来比较费劲,我们还是让他输出到一个文件吧。稍微修改一下。

ndk-stack -sym obj/local/x86 -dump log.txt >trace.txt

这样执行成功后,就可以在当前目录下看到一个trace.txt,里面就是我们想要的内容。
这里写图片描述

从上往下看,注意出现的代码行数,像第一个 HelloWorldScene.cpp:71,就表示这个类的71行。查看源码,发现71行代码是

// position the sprite on the center of the screen
sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));

在这里崩溃了,嗯,这个sprite可能是空指针,那为啥是空指针呢,巴拉巴拉,就是查找原因的节奏了,最后发现,居然是图片命名大小写的问题,扑街,谁命名的,拉出来,打一顿。。。。。。

至此基本可以定位程序哪里出错了。

最后懒人大法。每次写这命令多麻烦呀,还是写个批处理好了,把上面的命令写到一个批处理文件里,每次执行一下就ok。

注意点

  • 每个版本程序输出的崩溃log要和对应版本的obj一起使用,你不能拿其他版本的obj来解析崩溃log,否则会解析失败。也就是 obj -> so -> log,等同是一个版本的。

  • 如果嫌麻烦,也可以仔细观察那段崩溃信息。里面显示的内容和调用顺序是相反的。仔细看的话也大概可以确定那个方法出错了,只是不能确定是哪行代码。

其实每次android程序崩溃,系统都会将崩溃信息写入到一个文件。

09.387 1532-1549/system_process I/BootReceiver: Copying /data/tombstones/tombstone_00 to DropBox (SYSTEM_TOMBSTONE)

看 log 输出,这有一个tombstones,翻译成中文就是墓碑,这下就懂了吧。
网上有研究tombstones的,请自行前去围观。

没有更多推荐了,返回首页