在JNI/OpenGL ES加载代码过程中发生了非常规且不可靠的Android崩溃



赏金

由于这对我来说是一个重要的问题,我一直在寻找赏金。我不想寻找确切的答案——任何能让我解决这个问题的答案都会得到赏金。请确保您已看到下面的编辑。

编辑:从那以后,我成功地在Gdb中发现了崩溃(通过"adbshellsetprop-debug.dbuid32767"),并注意到这与谷歌群组上的这篇文章中提到的问题完全相同。显示的回溯与我崩溃的线程相同(除了精确的地址)。我承认,我不是调试工具向导,所以如果你对我应该寻找什么有任何想法,请告诉我。

快速而肮脏的破败

我已经精简了我的大部分相当大的应用程序代码,以便该应用程序执行以下操作:通过JNI’d包装器(来自C++-->Java)加载一堆纹理,以便Java库为我处理解码,用它们制作OpenGL纹理,并将屏幕清除为相当漂亮但令人嘲弄的深蓝色。它在libc中正在消亡,但每十次中只有一次。

更糟糕的是,它看起来甚至与我编写的任何代码都没有关系——它似乎是以延迟的方式发生的,但它似乎与垃圾收集器这样便于指责的东西无关。在我自己的代码中没有崩溃发生的特定点——它似乎在每次运行的基础上发生变化。

长话短说

我最终得到了一个标准的崩溃转储,其中的堆栈告诉我,因为它有两个条目,一个到libc,另一个到看起来无效或空的堆栈帧。libc中解析的符号是pthread_mutex_unlock。我自己甚至不再使用这个函数,因为我已经去掉了对多线程的需求。(本机代码在表面视图中调用,仅进行渲染。)

pthread_mutex_unlock导致分段错误,通常在地址0处,但有时是一个小值(小于0x200)而不是0。Bionic中默认的(也是最常见的)互斥体只有一个指针可以进行segfault,那就是指向pthread_mutex_t结构本身的指针。然而,更复杂的互斥体(有几个选项)可能会使用额外的指针。因此,libc很可能很好,而libdvm也有问题(假设我可以信任我的堆栈跟踪)。

让我注意到,只有当我做以下两件事中的一件时,这个问题似乎才是可再现的:禁止加载图像的数据部分(但仍读取格式/维度信息),并使我用于将纹理加载到OpenGL的缓冲区未初始化,或者通过仅禁用最终的glTexImage2D调用来禁用OpenGL纹理的创建。

请注意,前面提到的用于将纹理加载到OpenGL中的缓冲区只创建一次并销毁一次。我试过放大它,并确定我不会被特定于该缓冲区的缓冲区溢出问题所困扰。

我能想到的罪魁祸首是:

  • 我没有正确使用JNI,它对堆栈做了一些讨厌的事情
  • 我在某个地方有一个错误,正在损坏堆栈帧
  • 我正在传递OpenGL ES一些糟糕的东西,它正在做同样糟糕的事情(tm)
  • 我的自定义滚动内存分配器工作不正常

我已经在我的代码中搜寻这些罪犯(甚至更多!)好几天了。我很犹豫是否使用调试器,因为这次崩溃似乎对时间敏感。然而,我仍然可以在启用调试选项的情况下,完全不优化自己的本机代码,从而导致崩溃。(gdb本身运行缓慢,应用程序连接时也是如此)

我做过的事

  • 已使用CheckJNI
  • 尽可能多地删除代码,直到它停止崩溃
  • 编写了一个信号处理程序,并对一个小型日志系统进行了编码,以转储在抛出信号之前完成的最后一件事
  • 试图(但失败了)加剧问题
  • 两端用金丝雀填充本机堆数组。他们从未改变
  • 已审核代码路径中100%的代码。(我只是没看到这个问题。)
  • 我认为这个问题神奇地消失了,当我修复了一个小错误,运行了50次代码以确保是这样,然后在第二天第一次运行时崩溃了。(哦,我以前从来没有对虫子这么生气过!)

以下是LogCat:中常见的本地崩溃信息片段

I/DEBUG   ( 5818): signal 11 (SIGSEGV), fault addr 00000000
I/DEBUG   ( 5818):  r0 0000006e  r1 00000080  r2 fffffc5e  r3 100ffe58
I/DEBUG   ( 5818):  r4 00000000  r5 00000000  r6 00000000  r7 00000000
I/DEBUG   ( 5818):  r8 00000000  r9 8054f999  10 10000000  fp 0013e768
I/DEBUG   ( 5818):  ip 3b9aca00  sp 100ffe58  lr afd10640  pc 00000000  cpsr 60000010
I/DEBUG   ( 5818):  d0  643a64696f72646e  d1  6472656767756265
I/DEBUG   ( 5818):  d2  8083297880832965  d3  8083298880832973
I/DEBUG   ( 5818):  d4  8083291080832908  d5  8083292080832918
I/DEBUG   ( 5818):  d6  8083293080832928  d7  8083294880832938
I/DEBUG   ( 5818):  d8  0000000000000000  d9  0000000000000000
I/DEBUG   ( 5818):  d10 0000000000000000  d11 0000000000000000
I/DEBUG   ( 5818):  d12 0000000000000000  d13 0000000000000000
I/DEBUG   ( 5818):  d14 0000000000000000  d15 0000000000000000
I/DEBUG   ( 5818):  d16 0000000000000000  d17 3fe999999999999a
I/DEBUG   ( 5818):  d18 42eccefa43de3400  d19 3fe00000000000b4
I/DEBUG   ( 5818):  d20 4008000000000000  d21 3fd99a27ad32ddf5
I/DEBUG   ( 5818):  d22 3fd24998d6307188  d23 3fcc7288e957b53b
I/DEBUG   ( 5818):  d24 3fc74721cad6b0ed  d25 3fc39a09d078c69f
I/DEBUG   ( 5818):  d26 0000000000000000  d27 0000000000000000
I/DEBUG   ( 5818):  d28 0000000000000000  d29 0000000000000000
I/DEBUG   ( 5818):  d30 0000000000000000  d31 0000000000000000
I/DEBUG   ( 5818):  scr 80000012
I/DEBUG   ( 5818): 
I/DEBUG   ( 5818):          #00  pc 00000000  
I/DEBUG   ( 5818):          #01  pc 0001063c  /system/lib/libc.so
I/DEBUG   ( 5818): 
I/DEBUG   ( 5818): code around pc:
I/DEBUG   ( 5818): 
I/DEBUG   ( 5818): code around lr:
I/DEBUG   ( 5818): afd10620 e1a01008 e1a02007 e1a03006 e1a00005 
I/DEBUG   ( 5818): afd10630 ebfff95d e1a05000 e1a00004 ebffff46 
I/DEBUG   ( 5818): afd10640 e375006e 03a0006e 13a00000 e8bd81f0 
I/DEBUG   ( 5818): afd10650 e304cdd3 e3043240 e92d4010 e341c062 
I/DEBUG   ( 5818): afd10660 e1a0e002 e24dd008 e340300f e1a0200d 
I/DEBUG   ( 5818): 
I/DEBUG   ( 5818): stack:
I/DEBUG   ( 5818):     100ffe18  00000000  
I/DEBUG   ( 5818):     100ffe1c  00000000  
I/DEBUG   ( 5818):     100ffe20  00000000  
I/DEBUG   ( 5818):     100ffe24  ffffff92  
I/DEBUG   ( 5818):     100ffe28  100ffe58  
I/DEBUG   ( 5818):     100ffe2c  00000000  
I/DEBUG   ( 5818):     100ffe30  00000080  
I/DEBUG   ( 5818):     100ffe34  8054f999  /system/lib/libdvm.so
I/DEBUG   ( 5818):     100ffe38  10000000  
I/DEBUG   ( 5818):     100ffe3c  afd10640  /system/lib/libc.so
I/DEBUG   ( 5818):     100ffe40  00000000  
I/DEBUG   ( 5818):     100ffe44  00000000  
I/DEBUG   ( 5818):     100ffe48  00000000  
I/DEBUG   ( 5818):     100ffe4c  00000000  
I/DEBUG   ( 5818):     100ffe50  e3a07077  
I/DEBUG   ( 5818):     100ffe54  ef900077  
I/DEBUG   ( 5818): #01 100ffe58  00000000  
I/DEBUG   ( 5818):     100ffe5c  00000000  
I/DEBUG   ( 5818):     100ffe60  00000000  
I/DEBUG   ( 5818):     100ffe64  00000000  
I/DEBUG   ( 5818):     100ffe68  00000000  
I/DEBUG   ( 5818):     100ffe6c  00000000  
I/DEBUG   ( 5818):     100ffe70  00000000  
I/DEBUG   ( 5818):     100ffe74  00000000  
I/DEBUG   ( 5818):     100ffe78  00000000  
I/DEBUG   ( 5818):     100ffe7c  00000000  
I/DEBUG   ( 5818):     100ffe80  00000000  
I/DEBUG   ( 5818):     100ffe84  00000000  
I/DEBUG   ( 5818):     100ffe88  00000000  
I/DEBUG   ( 5818):     100ffe8c  00000000  
I/DEBUG   ( 5818):     100ffe90  00000000  
I/DEBUG   ( 5818):     100ffe94  00000000  
I/DEBUG   ( 5818):     100ffe98  00000000  
I/DEBUG   ( 5818):     100ffe9c  00000000  

使用ndk r6,Android平台2.2(API第8级),使用-Wall-Werror编译,仅限ARM模式。

我正在研究任何想法,尤其是那些以确定性的方式可验证的想法。如果更多信息会有所帮助,请留下评论(如果不能,请回答),我会尽快更新我的问题。谢谢你读到这里!

JNI接口

既有j2n调用,也有n2j调用。目前唯一的j2n呼叫在这里:

private static class Renderer implements GLSurfaceView.Renderer {
public void onDrawFrame(GL10 gl) {
GraphicsLib.graphicsStep();
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
GraphicsLib.graphicsInit(width, height);
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// Do nothing.
}
}

此代码通过以下接口:

public class GraphicsLib {
static {
System.loadLibrary("graphicslib");
}
public static native void graphicsInit(int width, int height);
public static native void graphicsStep();
}

在原生端看起来像:

extern "C" {
JNIEXPORT void JNICALL FN(graphicsInit)(JNIEnv* env, jobject obj,  jint width, jint height);
JNIEXPORT void JNICALL FN(graphicsStep)(JNIEnv* env, jobject obj);
};

函数定义本身以原型的副本开始。

graphicsInit只是存储了它传递的维度,并设置了OpenGL,没有任何特别有趣的东西。graphicsStep将屏幕清除为漂亮的颜色,并调用LoadSprites(env)

更复杂的一侧由LoadSprites()中使用的n2j调用组成,该调用每帧加载一个sprite。这不是一个优雅的解决方案,但它一直在工作,这次崩溃除外。

LoadSprites的工作原理如下:

GameAssetsInfo gai;
void LoadSprites(JNIEnv* env)
{
InitGameAssets(gai, env);
CatchJNIException(env, "j0");
...
static int z = 0;
if (z < numSprites)
{
CatchJNIException(env, "j1");
OpenGameImage(gai, SpriteIDFromNumber(z));
CatchJNIException(env, "j2");
unsigned int actualWidth = GetGameImageWidth(gai);
CatchJNIException(env, "j3");
unsigned int actualHeight = GetGameImageHeight(gai);
CatchJNIException(env, "j4");
...
jint i;
int r = 0;
CatchJNIException(env, "j5");
do {
CatchJNIException(env, "j6");
i = ReadGameImage(gai);
CatchJNIException(env, "j7");
if (i > 0)
{
// Deal with the pure data chunk -- One line at a time.
CatchJNIException(env, "j8");
StoreGameImageChunk(gai, (int*)sprites[z].data + r, 0, i);
...
r += sprites[z].width;
CatchJNIException(env, "j9");
UnreadGameImage(gai);
CatchJNIException(env, "j10");
} else {
break;
}
} while (true);
CatchJNIException(env, "j11");
CloseGameImage(gai);
CatchJNIException(env, "j12");
... OpenGL ES calls ...
glTexImage2D( ... );
z++;
}
CatchJNIException(env, "j13");
}

CatchJNIException是什么(从不为我打印任何内容):

void CatchJNIException(JNIEnv* env, const char* str)
{
jthrowable exc = env->ExceptionOccurred();
if (exc) {
jclass newExcCls;
env->ExceptionDescribe();
env->ExceptionClear();
newExcCls = env->FindClass( 
"java/lang/IllegalArgumentException");
if (newExcCls == NULL) {
// Couldn't find the exception class.. Uuh..
LOGE("Failed to catch JNI exception entirely -- could not find exception class.");
return;
abort();
}
LOGE("Caught JNI exception. (%s)", str);
env->ThrowNew( newExcCls, "thrown from C code");
//      abort();
}
}

GameAssetInfo的相关部分和相关代码只能从本地代码中调用,其工作方式如下:

void InitGameAssets(GameAssetsInfo& gameasset, JNIEnv* env)
{
CatchJNIException(env, "jS0");
FST;
char str[64];
sprintf(str, "%s/GameAssets", ROOTSTR);
gameasset.env = env;
CatchJNIException(gameasset.env, "jS1");
gameasset.cls = gameasset.env->FindClass(str);
CatchJNIException(gameasset.env, "jS2");
gameasset.openAsset = gameasset.env->GetStaticMethodID(gameasset.cls, "OpenAsset", "(I)V");
CatchJNIException(gameasset.env, "jS3");
gameasset.readAsset = gameasset.env->GetStaticMethodID(gameasset.cls, "ReadAsset", "()I");
CatchJNIException(gameasset.env, "jS4");
gameasset.closeAsset = gameasset.env->GetStaticMethodID(gameasset.cls, "CloseAsset", "()V");
CatchJNIException(gameasset.env, "jS5");
gameasset.buffID = gameasset.env->GetStaticFieldID(gameasset.cls, "buff", "[B");
CatchJNIException(gameasset.env, "jS6");
gameasset.openImage = gameasset.env->GetStaticMethodID(gameasset.cls, "OpenImage", "(I)V");
CatchJNIException(gameasset.env, "jS7");
gameasset.readImage = gameasset.env->GetStaticMethodID(gameasset.cls, "ReadImage", "()I");
CatchJNIException(gameasset.env, "jS8");
gameasset.closeImage = gameasset.env->GetStaticMethodID(gameasset.cls, "CloseImage", "()V");
CatchJNIException(gameasset.env, "jS9");
gameasset.buffIntID = gameasset.env->GetStaticFieldID(gameasset.cls, "buffInt", "[I");
CatchJNIException(gameasset.env, "jS10");
gameasset.imageWidth = gameasset.env->GetStaticFieldID(gameasset.cls, "imageWidth", "I");
CatchJNIException(gameasset.env, "jS11");
gameasset.imageHeight = gameasset.env->GetStaticFieldID(gameasset.cls, "imageHeight", "I");
CatchJNIException(gameasset.env, "jS12");
gameasset.imageHasAlpha = gameasset.env->GetStaticFieldID(gameasset.cls, "imageHasAlpha", "I");
CatchJNIException(gameasset.env, "jS13");
}
void OpenGameAsset(GameAssetsInfo& gameasset, int rsc)
{
FST;
CatchJNIException(gameasset.env, "jS14");
gameasset.env->CallStaticVoidMethod(gameasset.cls, gameasset.openAsset, rsc);
CatchJNIException(gameasset.env, "jS15");
}
void CloseGameAsset(GameAssetsInfo& gameasset)
{
FST;
CatchJNIException(gameasset.env, "jS16");
gameasset.env->CallStaticVoidMethod(gameasset.cls, gameasset.closeAsset);
CatchJNIException(gameasset.env, "jS17");
}
int ReadGameAsset(GameAssetsInfo& gameasset)
{
FST;
CatchJNIException(gameasset.env, "jS18");
int ret = gameasset.env->CallStaticIntMethod(gameasset.cls, gameasset.readAsset);
CatchJNIException(gameasset.env, "jS19");
if (ret > 0)
{
CatchJNIException(gameasset.env, "jS20");
gameasset.obj = gameasset.env->GetStaticObjectField(gameasset.cls, gameasset.buffID);
CatchJNIException(gameasset.env, "jS21");
gameasset.arr = reinterpret_cast<jbyteArray*>(&gameasset.obj);
}
return ret;
}
void UnreadGameAsset(GameAssetsInfo& gameasset)
{
FST;
CatchJNIException(gameasset.env, "jS22");
gameasset.env->DeleteLocalRef(gameasset.obj);
CatchJNIException(gameasset.env, "jS23");
}
void StoreGameAssetChunk(GameAssetsInfo& gameasset, void* store, int offset, int length)
{
FST;
CatchJNIException(gameasset.env, "jS24");
gameasset.env->GetByteArrayRegion(*gameasset.arr, offset, length, (jbyte*)store);
CatchJNIException(gameasset.env, "jS25");
}
void OpenGameImage(GameAssetsInfo& gameasset, int rsc)
{
FST;
CatchJNIException(gameasset.env, "jS26");
gameasset.env->CallStaticVoidMethod(gameasset.cls, gameasset.openImage, rsc);
CatchJNIException(gameasset.env, "jS27");
gameasset.l_imageWidth = (int)gameasset.env->GetStaticIntField(gameasset.cls, gameasset.imageWidth);
CatchJNIException(gameasset.env, "jS28");
gameasset.l_imageHeight = (int)gameasset.env->GetStaticIntField(gameasset.cls, gameasset.imageHeight);
CatchJNIException(gameasset.env, "jS29");
gameasset.l_imageHasAlpha = (int)gameasset.env->GetStaticIntField(gameasset.cls, gameasset.imageHasAlpha);
CatchJNIException(gameasset.env, "jS30");
}
void CloseGameImage(GameAssetsInfo& gameasset)
{
FST;
CatchJNIException(gameasset.env, "jS31");
gameasset.env->CallStaticVoidMethod(gameasset.cls, gameasset.closeImage);
CatchJNIException(gameasset.env, "jS32");
}
int ReadGameImage(GameAssetsInfo& gameasset)
{
FST;
CatchJNIException(gameasset.env, "jS33");
int ret = gameasset.env->CallStaticIntMethod(gameasset.cls, gameasset.readImage);
CatchJNIException(gameasset.env, "jS34");
if ( ret > 0 )
{
CatchJNIException(gameasset.env, "jS35");
gameasset.obj = gameasset.env->GetStaticObjectField(gameasset.cls, gameasset.buffIntID);
CatchJNIException(gameasset.env, "jS36");
gameasset.arrInt = reinterpret_cast<jintArray*>(&gameasset.obj);
}
return ret;
}
void UnreadGameImage(GameAssetsInfo& gameasset)
{
FST;
CatchJNIException(gameasset.env, "jS37");
gameasset.env->DeleteLocalRef(gameasset.obj);
CatchJNIException(gameasset.env, "jS38");
}
void StoreGameImageChunk(GameAssetsInfo& gameasset, void* store, int offset, int length)
{
FST;
CatchJNIException(gameasset.env, "jS39");
gameasset.env->GetIntArrayRegion(*gameasset.arrInt, offset, length, (jint*)store);
CatchJNIException(gameasset.env, "jS40");
}
int GetGameImageWidth(GameAssetsInfo& gameasset) { return gameasset.l_imageWidth; }
int GetGameImageHeight(GameAssetsInfo& gameasset) { return gameasset.l_imageHeight; }
int GetGameImageHasAlpha(GameAssetsInfo& gameasset) { return gameasset.l_imageHasAlpha; }

它在Java方面得到了支持:

public class GameAssets {
static public Resources res = null;
static public InputStream is = null;
static public byte buff[];
static public int buffInt[];
static public final int buffSize = 1024;
static public final int buffIntSize = 2048;
static public int imageWidth;
static public int imageHeight;
static public int imageHasAlpha;
static public int imageLocX;
static public int imageLocY;
static public Bitmap mBitmap;
static public BitmapFactory.Options decodeResourceOptions = new BitmapFactory.Options();
public GameAssets(Resources r) {
res = r;
buff = new byte[buffSize];
buffInt = new int[buffIntSize];
decodeResourceOptions.inScaled = false;
}
public static final void OpenAsset(int id) {
is = res.openRawResource(id);
}
public static final int ReadAsset() {
int num = 0;
try {
num = is.read(buff);
} catch (Exception e) {
;
}
return num;
}
public static final void CloseAsset() {
try {
is.close();
} catch (Exception e) {
;
}
is = null;
}
// We want all the advantages that BitmapFactory can provide -- reading
// images of compressed image formats -- so we provide our own interface
// for it.
public static final void OpenImage(int id) {
mBitmap = BitmapFactory.decodeResource(res, id, decodeResourceOptions);
imageWidth = mBitmap.getWidth();
imageHeight = mBitmap.getHeight();
imageHasAlpha = mBitmap.hasAlpha() ? 1 : 0;
imageLocX = 0;
imageLocY = 0;
}
public static final int ReadImage() {
if (imageLocY >= imageHeight) return 0;
int numReadPixels = buffIntSize;
if (imageLocX + buffIntSize >= imageWidth)
{
numReadPixels = imageWidth - imageLocX;
mBitmap.getPixels(buffInt, 0, imageWidth, imageLocX, imageLocY, numReadPixels, 1);
imageLocY++;
}
else
{
mBitmap.getPixels(buffInt, 0, imageWidth, imageLocX, imageLocY, numReadPixels, 1);
imageLocX += numReadPixels;
}
return numReadPixels;
}
public static final void CloseImage() {
}
}

请注意,游戏资产代码中明显缺乏线程安全性。

让我知道更多的信息是否有用。

从我之前的评论中发布。"可能发生了JNI异常,由于您在异常后没有返回,这可能会导致崩溃。我不知道Android的日志记录是如何工作的,但在C中,一个简单的printf不需要立即输出日志。因此,在崩溃发生的情况下,可能发生了异常,但在输出日志之前系统崩溃了">
在线几天。希望车祸不会再发生。。我讨厌一些问题在没有明确解释的情况下神奇地消失了。它们通常会马上回来咬你;-)无论如何,希望你不要被咬

我无法回答:(,我刚刚遇到了一个类似的问题,并注意到某个地方的java文档说什么时候有线程(如果你在做OpenGL,那么就有线程)。您需要小心第一个参数(env)和第二个参数(jobject)。您不能在不同的线程上共享,因为它们是特定于线程的。

在我的例子中,有事件线程和渲染线程。我有一个全球性的env&jself变量,这是调用jni时传入的两个参数。我更改了代码,以确保只有渲染线程接触env/jself变量。在事件线程中,我传递原始数据,并简单地记下需要做什么,从而不需要env/jself变量。当然,我使用互斥锁来锁定我的事件结构。

看起来你在这里设置env可能是全局的gameasset.env=env;

如果gameasset是全局的,或者被不同的线程使用,那么通过互斥/锁定简单地共享env或jobject类变量是行不通的(它们是特定于线程的)。

TL:DR;当从java调用jni方法时,我只访问env变量和渲染线程上的jobject第二个变量,而不访问其他地方,这已经缓解了我的问题。

一开始可能看起来很愚蠢,但您的问题让我想起了我们在一个Android应用程序中遇到的问题。我们通过使用"静态"对象来实现最佳化,我们确信这些对象应该只存在一次,并且我们只想被创建一次。这似乎和我们在安卓系统中的活动生命周期不一致(经过大量的调试和头痛),所以我们切换到使用实例,并允许操作系统处理活动的清理和优化。这解决了我们的问题,并且我们的应用程序足够稳定。

冒着指出极其明显的。。。你确定你没有溢出str吗?

char str[64];
sprintf(str, "%s/GameAssets", ROOTSTR);

最新更新