Dalvik虚拟机异常处理机制
在写这篇文章之前首先提几个问题,try catch的时候虚拟机到底做了些什么,Thread的UncaughtExceptionHandler是怎么回事?jni函数的异常是如何抛出的,又是如何被虚拟机捕获的?我们以抛出一个异常为入口,来分析Dalvik虚拟机的异常处理机制,由于throw是关键字,执行时肯定为字节码,所以我们需要到虚拟机的解释器中查看,如果对解释器不太了解的话可以参考我之前的文章:
在写这篇文章之前首先提几个问题,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)