在写这篇文章之前首先提几个问题,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虚拟机的异常处理机制就讲完了,是不是有豁然开朗的感觉。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐