ART异常处理机制(2)

来源:互联网 发布:管家婆进销存软件 价格 编辑:程序博客网 时间:2024/06/07 03:02

在本篇介绍 StackOverflowError 在 ART种的实现。本篇基础:ART异常处理机制(1) - SIGSEGV信号的拦截和处理

在 ART异常处理机制1 中,我们已经知道了 ART会注册信号处理函数,优先尝试处理 SIGSEGV信号。ART中总共有 4 种 handler 享有优先处理 SIGSEGV信号的权利。而StackOverflowHandler排在第二位,由于SuspendHandler是 Disable的,所以实际上第一个处理 SIGSEGV的是 StackOverflowHandler。今天我们主要分析下它的实现以及 StackOverflowError的检测和抛出。

先看下StackOverflowHandler实现的注释:

// Stack overflow fault handler.//// This checks that the fault address is equal to the current stack pointer// minus the overflow region size (8K typically).  The instruction sequence// that generates this signal is://// sub r12,sp,#8192// ldr.w r12,[r12,#0]//// The second instruction will fault if r12 is inside the protected region// on the stack.//// If we determine this is a stack overflow we need to move the stack pointer// to the overflow region below the protected region.
说的实际就是 stack overflow检测方法,意思是如果 sp-8k在 stack上的 protected region 空间内,那么在执行第二条指令 ldr.w r12, [r12, #0] 时,就会产生一个 SIGSEGV信号。这个8k 取决于 stack reserved page的大小,也有可能是 16k,说到这里,我们还是需要一个清晰的 java stack的布局,才能更明了的分析这个话题了。
下图是一个 ART thread的布局:

图的说明:

  1. ART中的thread也是通过 pthread创建的,所以 pthread 开头的标注都是pthread内部的数据成员
  2. 以ART开头的标注都是 ART内部/ART thread内部的数据成员
  3. 在低地址的前两个page,都是 ---p权限,就是用来做 stack overflow检测的,分别是 pthread和ART创建的 guard page
  4. 接下来黄色部分描述的 8KB 是为了抛出 StackOverflow Exception 预留的栈空间,因为如果不预留的话,在overflow的时候,就没有栈空间用来执行抛出 StackOverflow 异常了
  5. 再下面的绿色部分,是线程真正能够使用的栈空间
  6. 最下面的部分,用来存储 pthread_internal_t 来描述当前 pthread,并有个 alignment保证下面的数据 16 byte对齐
看下一个线程 stack 在maps的情况:
7f8000d000-7f8000e000 ---p 00000000 00:00 0                              [anon:thread stack guard page]7f8000e000-7f8000f000 ---p 00000000 00:00 07f8000f000-7f8010a000 rw-p 00000000 00:00 0                              [stack:7496]
现在就比较明了了,在栈的末尾,先有 8KB的 protect page,都是 ---p权限,所以任何的尝试从 protected page 读取或者写入数据的指令,都会触发 segement fault,从而产生一个 SIGSEGV 信号。

一般情况下,线程运行的过程中,SP都是在图中的浅绿色部分栈空间中,认为栈空间还充足。但只要 SP超出浅绿色部分栈空间,剩余的栈空间就不到 8+4+4=16KB了。此时认为当前线程发生了 stack overflow问题;那么此时 SP- 8192(reserved) 计算得出的地址,就会在 protected page范围内,此时再执行 ldr.w r12, [r12, #0] 就会产生一个SIGSEGV信号,且 fault_addr = SP-8192。而在没有发生 overflow的情况下,SP还在浅绿色部分栈空间,SP-8192的地址肯定是可以被访问的,就不会发生 segement fault。

这样,我们在StackOverflowHander的Action中,只需要检查当前这个 SIGSEGV错误中的 fault_addr是否是等于 SP - 8192,如果相等,这说明当前线程发生了 stack overflow错误,需要抛出StackOverflow异常。

需要指明的是,这样的检测代码都在一个java 函数对应的 generated code的最开始位置,也即在跳转到一个Java 函数后,需要先检查是否发生了 overflow,因为接下来的代码中很快就会减小 SP来使用栈空间。比如:

  34: void sun.util.logging.PlatformLogger.warning(java.lang.String, java.lang.Object[]) (dex_method_idx=26617)    DEX CODE:      0x0000: 5420 4b27                 | iget-object v0, v2, Lsun/util/logging/PlatformLogger$LoggerProxy; sun.util.logging.PlatformLogger.loggerProxy // field@10059      0x0002: 6201 3c27                 | sget-object  v1, Lsun/util/logging/PlatformLogger$Level; sun.util.logging.PlatformLogger$Level.WARNING // field@10044      0x0004: 6e40 d267 1043            | invoke-virtual {v0, v1, v3, v4}, void sun.util.logging.PlatformLogger$LoggerProxy.doLog(sun.util.logging.PlatformLogger$Level, java.lang.String, java.lang.Object[]) // method@26578    CODE: (code_offset=0x0094ffb4 size_offset=0x0094ffb0 size=144)...      0x0094ffb4: d1400bf0  sub x16, sp, #0x2000 (8192)      0x0094ffb8: b940021f  ldr wzr, [x16]        StackMap [native_pc=0x94ffbc] (dex_pc=0x0, native_pc_offset=0x8, dex_register_map_offset=0xffffffff, inline_info_offset=0xffffffff, register_mask=0x0, stack_mask=0b0000000000000000)      0x0094ffbc: f8190fe0  str x0, [sp, #-112]!      0x0094ffc0: a90457f4  stp x20, x21, [sp, #64]      0x0094ffc4: a9055ff6  stp x22, x23, [sp, #80]      0x0094ffc8: a9067bf8  stp x24, lr, [sp, #96]      ....
这是真实运行情况下的oat文件,从中看到在开始的 generated code开始,立即就进行的 stack overflow检测。这里使用的是 x16和 wzr,与上面有所不同,没有关系,实现原理一样,有一点区别是,使用 wzr作为目的寄存器时会丢弃结果,而由于这里的 ldr指令的目的仅仅是为了检测 x16所在地址空间能否访问,而不关注其内容,所以这里才使用 wzr,能够在正常访问的情况下丢弃掉这个结果。
理解了这些,StackOverflowHandler::Action()函数的实现就比较容易理解了:
bool StackOverflowHandler::Action(int sig ATTRIBUTE_UNUSED, siginfo_t* info ATTRIBUTE_UNUSED,                                  void* context) {  struct ucontext* uc = reinterpret_cast<struct ucontext*>(context);  struct sigcontext *sc = reinterpret_cast<struct sigcontext*>(&uc->uc_mcontext);  uintptr_t sp = sc->arm_sp;  uintptr_t fault_addr = sc->fault_address;  uintptr_t overflow_addr = sp - GetStackOverflowReservedBytes(kArm);  // Check that the fault address is the value expected for a stack overflow.  if (fault_addr != overflow_addr) {    VLOG(signals) << "Not a stack overflow";    return false;  }  VLOG(signals) << "Stack overflow found";  sc->arm_pc = reinterpret_cast<uintptr_t>(art_quick_throw_stack_overflow);  // The kernel will now return to the address in sc->arm_pc.  return true;}
在这里发现 fault addr 匹配 SP-Reserved 后,就会跳转到 art_quick_throw_stack_overflow 去抛出异常:
    /*     * Called by managed code to create and deliver a StackOverflowError.     */NO_ARG_RUNTIME_EXCEPTION art_quick_throw_stack_overflow, artThrowStackOverflowFromCode
看下这个宏:
.macro NO_ARG_RUNTIME_EXCEPTION c_name, cxx_name    .extern \cxx_nameENTRY \c_name    SETUP_SAVE_ALL_CALLEE_SAVES_FRAME r0       @ save all registers as basis for long jump context    mov r0, r9                      @ pass Thread::Current    bl  \cxx_name                   @ \cxx_name(Thread*)END \c_name.endm
展开后应该是:
    .extern artThrowStackOverflowFromCodeENTRY art_quick_throw_stack_overflow    SETUP_SAVE_ALL_CALLEE_SAVES_FRAME r0       @ save all registers as basis for long jump context    mov r0, r9                      @ pass Thread::Current    bl  artThrowStackOverflowFromCode                   @ \cxx_name(Thread*)END art_quick_throw_stack_overflow
所以最终是跳转到 artThrowStackOverflowFromCode函数,进行异常的抛出:
extern "C" NO_RETURN void artThrowStackOverflowFromCode(Thread* self)    REQUIRES_SHARED(Locks::mutator_lock_) {  ScopedQuickEntrypointChecks sqec(self);  ThrowStackOverflowError(self);  self->QuickDeliverException();}
ThrowStackOverflowError()函数,会创建一个 StackOverflowError类对象,并设置好 error message,stack trace等信息,然后使用 self->SetException(e),把这个Throwable对象设置到当前线程的 tlsPtr_.exception 成员中,以便后续通过self->GetException()进行获取。
void ThrowStackOverflowError(Thread* self) {  self->SetStackEndForStackOverflow();  // Allow space on the stack for constructor to execute.  JNIEnvExt* env = self->GetJniEnv();  std::string msg("stack size ");  msg += PrettySize(self->GetStackSize());  std::string error_msg;  // Allocate an uninitialized object.  ScopedLocalRef<jobject> exc(env,                              env->AllocObject(WellKnownClasses::java_lang_StackOverflowError));  if (exc.get() != nullptr) {    ScopedLocalRef<jstring> s(env, env->NewStringUTF(msg.c_str()));    if (s.get() != nullptr) {      env->SetObjectField(exc.get(), WellKnownClasses::java_lang_Throwable_detailMessage, s.get());      // cause.      env->SetObjectField(exc.get(), WellKnownClasses::java_lang_Throwable_cause, exc.get());      // suppressedExceptions.      ScopedLocalRef<jobject> emptylist(env, env->GetStaticObjectField(          WellKnownClasses::java_util_Collections,          WellKnownClasses::java_util_Collections_EMPTY_LIST));      CHECK(emptylist.get() != nullptr);      env->SetObjectField(exc.get(),                          WellKnownClasses::java_lang_Throwable_suppressedExceptions,                          emptylist.get());      // stackState is set as result of fillInStackTrace. fillInStackTrace calls      // nativeFillInStackTrace.      ScopedLocalRef<jobject> stack_state_val(env, nullptr);      {        ScopedObjectAccessUnchecked soa(env);        stack_state_val.reset(soa.Self()->CreateInternalStackTrace<false>(soa));      }      if (stack_state_val.get() != nullptr) {        env->SetObjectField(exc.get(),                            WellKnownClasses::java_lang_Throwable_stackState,                            stack_state_val.get());        // stackTrace.        ScopedLocalRef<jobject> stack_trace_elem(env, env->GetStaticObjectField(            WellKnownClasses::libcore_util_EmptyArray,            WellKnownClasses::libcore_util_EmptyArray_STACK_TRACE_ELEMENT));        env->SetObjectField(exc.get(),                            WellKnownClasses::java_lang_Throwable_stackTrace,                            stack_trace_elem.get());      } else {        error_msg = "Could not create stack trace.";      }      // Throw the exception.      self->SetException(self->DecodeJObject(exc.get())->AsThrowable());    } else {      // Could not allocate a string object.      error_msg = "Couldn't throw new StackOverflowError because JNI NewStringUTF failed.";    }  } else {    error_msg = "Could not allocate StackOverflowError object.";  }  if (!error_msg.empty()) {    LOG(WARNING) << error_msg;    CHECK(self->IsExceptionPending());  }  bool explicit_overflow_check = Runtime::Current()->ExplicitStackOverflowChecks();  self->ResetDefaultStackEnd();  // Return to default stack size.  // And restore protection if implicit checks are on.  if (!explicit_overflow_check) {    self->ProtectStack();  }}

需要说明的是 SetStackEndForStackOverflow()函数设置   tlsPtr_.stack_end = tlsPtr_.stack_begin;表示正在处理 stack overflow,并且把 ART设置的那 4KB的protected region设置为可读写(这么做的原因是增加4KB的可使用栈空间,以满足 stack overflow抛出过程的使用)。下面一部分代码就是 error msg,stacktrace填充的逻辑。业务逻辑完成后,再次把 tlsPtr_.stack_end还原到 Reserved page之前的位置,并把那 4KB region重新protect 起来。

而在ThrowStackOverflowError函数之后的 QuickDeliverException函数的功能就是尝试从当前 thread 的 stack 上找到当前 Exception对应的 catch block,交给其去处理。

分析过程中的几个疑问

1.一个线程发生 StackOverflow,触发 segement fault时,这个线程的状态是怎样的?

答:解释不太好,先说下暂时的理解,待后续研究CPU异常处理机制。访问不可读的内存时,应该会产生page fault(此时应该在内核态了),在后续page fault处理流程中,发现该page fault符合 SIGSEGV信号的条件,然后给发生异常的线程发送 SIGSEGV信号。所以,触发 segment fault时,处于内核态。

2.我们知道产生的 SIGSEGV 会由ART处理,那么是哪个线程处理的?

答:从第一点的答案可以知道,内核会把 SIGSEGV信号发送给发生异常的那个线程处理,处理时机应该是CPU异常处理机制执行完成,从内核态切换到用户态的时候,检查到有pending signal,然后调用信号处理函数去处理这个信号。相当于发生异常的线程会调用 SigChain::Hander函数来处理 SIGSEGV信号。

3.检测 StackOverflow的位置除了上面了解的一种,还有哪些位置会检测 StackOverflow?

   上面提到的这种是generated code中,在函数的入口位置(此时这个函数还没有开辟当前frame的栈空间),进行检测stack overflow。

   那么在 Interpreter 模式,或者从quick模式切换到 Interpreter 模式时,显然没有 generated code了,我们自己设想的话,这个检测应该在切换的过程中 (还没有开始给准备执行的java 函数分配栈空间)完成。检查了一下代码,发现有这么些地方会检查 stack overflow问题:

  • reflection.cc:InvokeWithVarArgs() / InvokeWithJValues() / InvokeVirtualOrInterfaceWithJValues() / InvokeVirtualOrInterfaceWithVarArgs() / InvokeMethod(),在这几个函数的入口位置都会进行 stack overflow检测
  • interpreter.cc:EnterInterpreterFromInvoke() / EnterInterpreterFromEntryPoint() / ArtInterpreterToInterpreterBridge() 在这几个函数入口 也会检查
  • art_method.cc:void ArtMethod::Invoke() 这个函数入口也会进行检查

总的来讲,原理就是在执行将要跳转到的函数的栈空间开辟之前,完成 stack overflow异常的检测。

Java stack overflow 异常说到这里。

原创粉丝点击