Android C++调试
Java代码出错的时候,我们会在第一时间想到调试。通过一番调试之后之后,问题就迎刃而解。我们现在很多项目中都会用到JNI调用,其中.so文件的c++代码出错了,什么办呢?
我们还是从一个简单的例子说起吧。现在.so文件有如下一个代码:
Jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,jobject thiz ){
int *t;
*t = 1;
return (*env)->NewStringUTF(env, "Hello from JNI !");
}
分析上面代码: 由于指针t没有分配内存,所以给它赋值回报错. 接下来我们开始运行调用该so文件的Android项目,在Logcat下会发现如下错误信息:
10-14 10:31:34.666: INFO/DEBUG(551): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
10-14 10:31:34.675: INFO/DEBUG(551): Build fingerprint: 'generic/sdk/generic/:1.5/CUPCAKE/147336:eng/test-keys'
10-14 10:31:34.675: INFO/DEBUG(551): pid: 6071, tid: 6071 >>> com.example.hellojni <<<
10-14 10:31:34.675: INFO/DEBUG(551): signal 7 (SIGBUS), fault addr 00000000
10-14 10:31:34.685: INFO/DEBUG(551): r0 0000a9d0 r1 43736118 r2 41049d64 r3 00000001
10-14 10:31:34.685: INFO/DEBUG(551): r4 befd8540 r5 00000004 r6 804002e5 r7 41049d3c
10-14 10:31:34.695: INFO/DEBUG(551): r8 befd8520 r9 41049d34 10 41049d20 fp 00000000
10-14 10:31:34.695: INFO/DEBUG(551): ip 804002e5 sp befd8510 lr ad00e3b8 pc 804002e4 cpsr 20000030
10-14 10:31:34.746: INFO/DEBUG(551): #00 pc 000002e4 /data/data/com.example.hellojni/lib/libhello-jni.so
10-14 10:31:34.756: INFO/DEBUG(551): #01 pc 0000e3b4 /system/lib/libdvm.so
10-14 10:31:34.765: INFO/DEBUG(551): stack:
观察上面的日志文件,我们会发现c++代码调用的堆栈信息. 在最顶端堆栈的是libhello-jni.so(上面代码编译在此so文件里面),以及用红色标识出来的地址000002e4, 我们正是通过该地址找到出错的函数。
ndk 提供了windows 对c++二进制分析的工具。最新已经是ndk1.6了,可以到网上很容易获得。其中arm-eabi-objump.exe 工具是可以用于分析so文件。如下所示:
arm-eabi-objump.exe –S libhello-jni.so > test.txt 输出重定向到test.txt中。
打开test.txt,我们看到如下信息:
libhello-jni.so: file format elf32-littlearm
Disassembly of section .text:
000002e4 <Java_com_example_hellojni_HelloJni_stringFromJNI>:
2e4: b510 push {r4, lr}
2e6: 2301 movs r3, #1
2e8: 601b str r3, [r3, #0]
2ea: 4905 ldr r1, [pc, #20] (300 <.text+0x1c>)
2ec: 6804 ldr r4, [r0, #0]
2ee: 4a05 ldr r2, [pc, #20] (304 <.text+0x20>)
2f0: 23a7 movs r3, #167
2f2: 4479 add r1, pc
2f4: 009b lsls r3, r3, #2
2f6: 58e3 ldr r3, [r4, r3]
2f8: 1889 adds r1, r1, r2
2fa: 4798 blx r3
2fc: bd10 pop {r4, pc}
2fe: 0000 lsls r0, r0, #0
300: 10ae asrs r6, r5, #2
302: 0000 lsls r0, r0, #0
304: ef64 ffff undefined
我们可以看到红色的编译地址2e4. 和我们之前看到地址2e4一样。基本上可以定位是Java_com_example_hellojni_HelloJni_stringFromJNI函数的某一位置出错。仔细分析,可以定位到之前的问题。
接下来我还要介绍下面常用的二进制分析工具
AR 用来建立、修改、提取静态库文件.
NM 列出目标文件的符号表中定义的符号。常见的链接或者运行时发生的unresolved symbol类型的错误可以用NM来辅助调试
OBJDUMP objdump是所有二进制工具之母,能够显示一个目标文件中所有的信息,通常我们用它来反汇编.text节中的二进制指令
READELF readelf可用来显示ELF格式可执行文件的信息
SIZE
size命令可以列出目标文件每一段的大小以及总体的大小。默认情况下,对于每个目标文件或者一个归档文件中的每个模块只产生一行输出。size可以用来简单快速的了解ELF文件各个段的情况
ldd可用来显示执行文件需要哪些共享库, 共享库装载管理器在哪里找到了需要的共享库
工具的使用我这边就不想详细多说,网上对这些工具介绍有海量的信息。我想强调的是,要想深入解决c++的问题,我们要熟练掌握这些工具的使用,这对我们调试Android native code有非常大的帮助。
后面介绍些NDK的一些知识
1. 加载so 文件.
只要在项目工程上建立 如下目录结构:
在java代码中如下调用 System.loadlibrary(“ds”); 而不需要像我之前所说的要从Assert目录底下拷贝到应用程序目录下。
2.介绍Application.mk一些参数
APP_MODULES
这个变量必须有,用来描述你应用程序需要的所有原生模块(由Android.mk文件描述),
这是用空格分割的模块名字列表,这个模块名字与在Android.mk文件中LOCAL_MODULE定义是一样的。
APP_PROJECT_PATH
这个变量必须有,应该给出你的程序项目的绝对路径,这是用于复制或安装剥离的共享库的版本到APK生成器所知道的位置。
APP_OPTIM
这个变量是可选的,可以定义成两个值'release' or 'debug'. 用于修改编译程序模块时的优化层级。
'release'模式是默认的,会产生高优化的文件,’debug’模式会生成不优化的文件,使得调试更容易进行。
主意,调试’release’和’debug’文件都是可能的,但是'release'版在调试节提高的信息很少,一些变量被优化输出,无法检查,代码被重排序,使得跟踪代码很困难,堆栈追踪也不可靠,等等…
APP_CFLAGS
一套编译器选项,在编译模块中的C代码时选用。这可能用于根据应用程序来改变模块的生成,以代替修改Android.mk文件本身。