Dalvik虚拟机异常处理机制

来源:互联网 发布:mac电脑查看ip地址 编辑:程序博客网 时间:2024/04/19 23:20

在写这篇文章之前首先提几个问题,try catch的时候虚拟机到底做了些什么,Thread的UncaughtExceptionHandler是怎么回事?jni函数的异常是如何抛出的,又是如何被虚拟机捕获的?

我们以抛出一个异常为入口,来分析Dalvik虚拟机的异常处理机制,由于throw是关键字,执行时肯定为字节码,所以我们需要到虚拟机的解释器中查看,如果对解释器不太了解的话可以参考我之前的文章:Dalvik虚拟机线程初始化及函数执行流程

HANDLE_OPCODE(OP_THROW /*vAA*/)    {        Object* obj;        EXPORT_PC();        vsrc1 = INST_AA(inst);        obj = (Object*) GET_REGISTER(vsrc1);        dvmSetException(self, obj);        GOTO_exceptionThrown();    }OP_END#define EXPORT_PC()         (SAVEAREA_FROM_FP(fp)->xtra.currentPc = pc)INLINE void dvmSetException(Thread* self, Object* exception) {    self->exception = exception;}

这里面做了三件事,首先通过EXPORT_PC将抛出异常时的当前pc保存起来,然后获取到异常对象并设置到线程中,最后跳转到异常处理的标签处。

GOTO_TARGET(exceptionThrown)    {        Object* exception;        int catchRelPc;        catchRelPc = dvmFindCatchBlock(self, pc - curMethod->insns,                    exception, false, (void*)&fp);        if (catchRelPc < 0) {            dvmSetException(self, exception);            GOTO_bail();        }        curMethod = SAVEAREA_FROM_FP(fp)->method;        methodClassDex = curMethod->clazz->pDvmDex;        pc = curMethod->insns + catchRelPc;        FINISH(0);    }bail:    interpState->retval = retval;    return false;

这里首先通过dvmFindCatchBlock找到能catch住该异常的的handler的pc偏移,因为能catch住该异常的不一定是当前函数,可能是上层,所以这里传入fp的地址,说明寻找catch block时肯定会设置fp,指向最终能catch住该异常的函数的栈帧。通过栈帧对应的Method的insns加上这个偏移就能得到最终异常处理的handler的指令地址,然后调用FIINSH(0)开始一条条执行指令。如果没有发现能catch住该异常的block,那么整个线程的执行就终止了。

int dvmFindCatchBlock(Thread* self, int relPc, Object* exception,    bool scanOnly, void** newFrame){    void* fp = self->curFrame;    int catchAddr = -1;    while (true) {        StackSaveArea* saveArea = SAVEAREA_FROM_FP(fp);        catchAddr = findCatchInMethod(self, saveArea->method, relPc,                        exception->clazz);        if (catchAddr >= 0)            break;        if (dvmIsBreakFrame(saveArea->prevFrame)) {            break;        } else {            fp = saveArea->prevFrame;            relPc = saveArea->savedPc - SAVEAREA_FROM_FP(fp)->method->insns;        }    }    self->curFrame = fp;    *newFrame = fp;    return catchAddr;}

这里从当前栈帧开始找起,在一个while循环中,退出条件是在当前函数中找到了catch点或者遇到了break frame。值得注意的是要不断更新relPc。因为在查找catch点的时候有一个原则,catch点必须在try block里。而这里saveArea->savedPc保存的是当前函数的返回地址,或者说是被上层函数调用的入口地址。这个地址减去insns的地址就是相对函数起始的偏移地址了。我们重点看看findCatchInMethod是怎么实现的,

static int findCatchInMethod(Thread* self, const Method* method, int relPc,    ClassObject* excepClass){    DvmDex* pDvmDex = method->clazz->pDvmDex;    const DexCode* pCode = dvmGetMethodCode(method);    DexCatchIterator iterator;    if (dexFindCatchHandler(&iterator, pCode, relPc)) {        for (;;) {            DexCatchHandler* handler = dexCatchIteratorNext(&iterator);            if (handler == NULL) {                break;            }            ClassObject* throwable =                dvmDexGetResolvedClass(pDvmDex, handler->typeIdx);            if (throwable == NULL) {                throwable = dvmResolveClass(method->clazz, handler->typeIdx,                    true);                if (throwable == NULL) {                    continue;                }            }            if (dvmInstanceof(excepClass, throwable)) {                return handler->address;            }        }    }    return -1;}

这里查找catch点的范围仅限于当前函数,首先获取函数的DexCode:

INLINE bool dvmIsBytecodeMethod(const Method* method) {    return (method->accessFlags & (ACC_NATIVE | ACC_ABSTRACT)) == 0;}INLINE const DexCode* dvmGetMethodCode(const Method* meth) {    if (dvmIsBytecodeMethod(meth)) {        return (const DexCode*)            (((const u1*) meth->insns) - offsetof(DexCode, insns));    } else {        return NULL;    }}

对于每个解释成Bytecode的函数,都有一个DexCode结构体与之对应,

typedef struct DexCode {    u2  registersSize;    u2  insSize;    u2  outsSize;    u2  triesSize;    u4  debugInfoOff;       /* file offset to debug info stream */    u4  insnsSize;          /* size of the insns array, in u2 units */    u2  insns[1];} DexCode;

可见这里记录了该函数的参数和try个数等信息,且最后一个成员对应的就是该函数的Bytecode码数组。所以拿到了函数的insns地址,就可以通过偏移计算出DexCode的地址。再来看看dexFindCatchHandler是做什么的。

DEX_INLINE bool dexFindCatchHandler(DexCatchIterator *pIterator,        const DexCode* pCode, u4 address) {    u2 triesSize = pCode->triesSize;    int offset = -1;    switch (triesSize) {        case 0: {            break;        }        case 1: {            const DexTry* tries = dexGetTries(pCode);            u4 start = tries[0].startAddr;            if (address < start) {                break;            }            u4 end = start + tries[0].insnCount;            if (address >= end) {                break;            }            offset = tries[0].handlerOff;            break;        }        default: {            offset = dexFindCatchHandlerOffset0(triesSize, dexGetTries(pCode),                    address);        }    }    if (offset < 0) {        dexCatchIteratorClear(pIterator); // This squelches warnings.        return false;    } else {        dexCatchIteratorInit(pIterator, pCode, offset);        return true;    }}

这里首先获取函数的triesSize,表示函数中try的个数,当个数是0时表示没有任何try,则直接返回false。如果是1就看看这个异常抛出点是否在try block中,如果有多个try点就调用dexFindCatchHandlerOffset0依次遍历这些try点看看到底哪个能捕获当前异常。这里offset是这个try对应的handler的偏移。因为函数中可能有多个try,每个try有多个handler。我们先看看dexGetTries的实现:

DEX_INLINE const DexTry* dexGetTries(const DexCode* pCode) {    const u2* insnsEnd = &pCode->insns[pCode->insnsSize];    if ((((u4) insnsEnd) & 3) != 0) {        insnsEnd++;    }    return (const DexTry*) insnsEnd;}

看来DexCode的insns结尾处还摆着一个DexTry数组,表示这个函数中有哪些try点。我们来看看DexTry的数据结构:

typedef struct DexTry {    u4  startAddr;          /* start address, in 16-bit code units */    u2  insnCount;          /* instruction count, in 16-bit code units */    u2  handlerOff;         /* offset in encoded handler data to handlers */} DexTry;

这个startAddr是try的起点地址,insnCount应该是try block的指令条数,handlerOff是try的handler的偏移。我们再来看看dexFindCatchHandlerOffset0的实现,如下:

int dexFindCatchHandlerOffset0(u2 triesSize, const DexTry* pTries,        u4 address) {    int min = 0;    int max = triesSize - 1;    while (max >= min) {        int guess = (min + max) >> 1;        const DexTry* pTry = &pTries[guess];        u4 start = pTry->startAddr;        if (address < start) {            max = guess - 1;            continue;        }        u4 end = start + pTry->insnCount;        if (address >= end) {            min = guess + 1;            continue;        }        return (int) pTry->handlerOff;    }    return -1;}

这里用二分查找法来定位异常抛出点到底在哪个catch block中,可见这个DexTry数组是按try点的起始地址排好序的。我们回到dexFindCatchHandler中,找到catch点后,要调用dexCatchIteratorInit初始化iterator,这个应该是为了之后遍历Exception的handler做准备的,因为try可能对应着若干个handler。

DEX_INLINE void dexCatchIteratorInit(DexCatchIterator* pIterator,    const DexCode* pCode, u4 offset){    dexCatchIteratorInitToPointer(pIterator,            dexGetCatchHandlerData(pCode) + offset);}DEX_INLINE void dexCatchIteratorInitToPointer(DexCatchIterator* pIterator,    const u1* pEncodedData){    s4 count = readSignedLeb128(&pEncodedData);    if (count <= 0) {        pIterator->catchesAll = true;        count = -count;    } else {        pIterator->catchesAll = false;    }    pIterator->pEncodedData = pEncodedData;    pIterator->countRemaining = count;}DEX_INLINE const u1* dexGetCatchHandlerData(const DexCode* pCode) {    const DexTry* pTries = dexGetTries(pCode);    return (const u1*) &pTries[pCode->triesSize];}

这个dexGetCatchHandlerData是要获取handler的起始地址,这里每个函数的insns数组中,首先是函数的ByteCode,然后是DexTry数组,接下来是Handler。因为可能有多个try,每个try都有自己的handler地址,这里通过offset就可以得到try对应的handler地址。再来看dexCatchIteratorInitToPointer,这里要在handler地址处读取一个整数,表示该try的handler的个数。我们来看看是如何通过iterator获得DexCatchHandler的。

DEX_INLINE DexCatchHandler* dexCatchIteratorNext(DexCatchIterator* pIterator) {    if (pIterator->countRemaining == 0) {        if (! pIterator->catchesAll) {            return NULL;        }        pIterator->catchesAll = false;        pIterator->handler.typeIdx = kDexNoIndex;    } else {        u4 typeIdx = readUnsignedLeb128(&pIterator->pEncodedData);        pIterator->handler.typeIdx = typeIdx;        pIterator->countRemaining--;    }    pIterator->handler.address = readUnsignedLeb128(&pIterator->pEncodedData);    return &pIterator->handler;}

这里首先读取handler的异常typeIdx,然后读取该异常处理的指令入口,设置iterator中的handler后返回。我们回到findCatchInMethod函数,拿到DexCatchHandler后,根据handler的typeIdx得到对应的异常的ClassObject,并看看这个异常是否和抛出的异常是同类型的,如果是就返回handler的指令地址,然后调用FINISH(0)从第一条指令开始执行。

至此,Dalvik虚拟机的异常处理机制我们已经走通了,其实核心就是寻找能catch住该异常的try block,当前函数找不到就跳到上层函数去找。找到之后再到try block的handlers中去找看有不有和这个异常匹配的handler,如果有就跳转到handler的指令入口处执行异常处理,否则继续跳到上层函数去找。

接下来,我们看看如果一直没有catch住这个异常会发生什么呢?我们回到exceptionThrown标签:

if (catchRelPc < 0) {    dvmSetException(self, exception);    GOTO_bail();}

当没有找到能捕获该异常的catch block时,就会GOTO_bail,这里其实就是退出线程执行了。我在文章Dalvik虚拟机线程初始化及函数执行流程里提到过,线程的入口函数是interpThreadStart,这个dvmCallMethod调的就是线程的run函数,最终会进入到Bytecode解释器的执行流程,当抛出异常且没有catch住时,该解释器执行就会提前返回,线程也就会退出了。

static void* interpThreadStart(Thread* self) {    prepareThread(self);    self->jniEnv = dvmCreateJNIEnv(self);    Method* run = self->threadObj->clazz->vtable[gDvm.voffJavaLangThread_run];    dvmCallMethod(self, run, self->threadObj, &unused);    dvmDetachCurrentThread();}

我们来看看dvmDetachCurrentThread的实现,里面是线程结束的收尾工作。

void dvmDetachCurrentThread(void) {    ..........    if (dvmCheckException(self)) {        threadExitUncaughtException(self, group);    }    ..........}

首先检查线程是否有未捕获的异常,如果有就调用threadExitUncaughtException处理该异常。

static void threadExitUncaughtException(Thread* self, Object* group){    Object* exception;    Object* handlerObj;    Method* uncaughtHandler = NULL;    InstField* threadHandler;    threadHandler = dvmFindInstanceField(gDvm.classJavaLangThread,            "uncaughtHandler", "Ljava/lang/Thread$UncaughtExceptionHandler;");    if (threadHandler == NULL) {        return;    }    handlerObj = dvmGetFieldObject(self->threadObj, threadHandler->byteOffset);    if (handlerObj == NULL)        handlerObj = group;    uncaughtHandler = dvmFindVirtualMethodHierByDescriptor(handlerObj->clazz,            "uncaughtException", "(Ljava/lang/Thread;Ljava/lang/Throwable;)V");    if (uncaughtHandler != NULL) {        JValue unused;        dvmCallMethod(self, uncaughtHandler, handlerObj, &unused,            self->threadObj, exception);    } else {        dvmSetException(self, exception);        dvmLogExceptionStackTrace();    }}

这里代码不少,但是做的事情很简单,就是找到uncaughtHandler对应的Method,然后调用它。

我们接下来看看如果是native中抛出了异常会如何?jni提供了接口可以用于抛出异常,如下:

static jint Throw(JNIEnv* env, jthrowable jobj) {    jint retval;    if (jobj != NULL) {        Object* obj = dvmDecodeIndirectRef(env, jobj);        dvmSetException(_self, obj);        retval = JNI_OK;    } else {        retval = JNI_ERR;    }    return retval;}

可见所谓的抛出其实就是设置了一下线程的异常,那么理论上这个jni函数返回时应该检查线程是否有异常,如果有就回溯栈帧看谁能捕获该异常。如下:

GOTO_TARGET(invokeMethod, bool methodCallRange, const Method* _methodToCall,    u2 count, u2 regs) {        u4* outs;        int i;    {        StackSaveArea* newSaveArea;        u4* newFp;        newFp = (u4*) SAVEAREA_FROM_FP(fp) - methodToCall->registersSize;        newSaveArea = SAVEAREA_FROM_FP(newFp);        newSaveArea->prevFrame = fp;        newSaveArea->savedPc = pc;        newSaveArea->method = methodToCall;        if (!dvmIsNativeMethod(methodToCall)) {            curMethod = methodToCall;            methodClassDex = curMethod->clazz->pDvmDex;            pc = methodToCall->insns;            fp = self->curFrame = newFp;            FINISH(0);                              // jump to method start        } else {#ifdef USE_INDIRECT_REF            newSaveArea->xtra.localRefCookie = self->jniLocalRefTable.segmentState.all;#else            newSaveArea->xtra.localRefCookie = self->jniLocalRefTable.nextEntry;#endif            self->curFrame = newFp;            (*methodToCall->nativeFunc)(newFp, &retval, methodToCall, self);            dvmPopJniLocals(self, newSaveArea);            self->curFrame = fp;            if (dvmCheckException(self)) {                GOTO_exceptionThrown();            }            FINISH(3);        }    }GOTO_TARGET_END

果然,在Jni函数调用完毕后,会调用dvmCheckException检查是否有抛出异常,如果有就去处理异常。

到这里Dalvik虚拟机的异常处理机制就讲完了,是不是有豁然开朗的感觉。

0 0
原创粉丝点击