ART异常处理机制(2) - StackOverflowError 实现

在本篇介绍 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 0
7f8000f000-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_name
ENTRY \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 artThrowStackOverflowFromCode
ENTRY 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 异常说到这里。


补充: StackSize:

stack_size  >= 1MB + 8k + 8k

static size_t FixStackSize(size_t stack_size) {
  // A stack size of zero means "use the default".
  if (stack_size == 0) {
    stack_size = Runtime::Current()->GetDefaultStackSize();
  }

  // Dalvik used the bionic pthread default stack size for native threads,
  // so include that here to support apps that expect large native stacks.
  stack_size += 1 * MB;

  // It's not possible to request a stack smaller than the system-defined PTHREAD_STACK_MIN.
  if (stack_size < PTHREAD_STACK_MIN) {
    stack_size = PTHREAD_STACK_MIN;
  }

  if (Runtime::Current()->ExplicitStackOverflowChecks()) {
    // It's likely that callers are trying to ensure they have at least a certain amount of
    // stack space, so we should add our reserved space on top of what they requested, rather
    // than implicitly take it away from them.
    stack_size += GetStackOverflowReservedBytes(kRuntimeISA);
  } else {
    // If we are going to use implicit stack checks, allocate space for the protected
    // region at the bottom of the stack.
    stack_size += Thread::kStackOverflowImplicitCheckSize +
        GetStackOverflowReservedBytes(kRuntimeISA);
  }

  // Some systems require the stack size to be a multiple of the system page size, so round up.
  stack_size = RoundUp(stack_size, kPageSize);

  return stack_size;
}


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值