欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-15 14:45:42
...

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

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

先看下*Handler实现的注释:

// 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的布局:

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

图的说明:

  1. ART中的thread也是通过 pthread创建的,所以 pthread 开头的标注都是pthread内部的数据成员
  2. 以ART开头的标注都是 ART内部/ART thread内部的数据成员
  3. 在低地址的前两个page,都是 ---p权限,就是用来做 stack overflow检测的,分别是 pthread和ART创建的 guard page
  4. 接下来黄色部分描述的 8KB 是为了抛出 * Exception 预留的栈空间,因为如果不预留的话,在overflow的时候,就没有栈空间用来执行抛出 * 异常了
  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。

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

需要指明的是,这样的检测代码都在一个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 // aaa@qq.com
      0x0002: 6201 3c27                 | sget-object  v1, Lsun/util/logging/PlatformLogger$Level; sun.util.logging.PlatformLogger$Level.WARNING // aaa@qq.com
      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[]) // aaa@qq.com

    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,能够在正常访问的情况下丢弃掉这个结果。
理解了这些,*Handler::Action()函数的实现就比较容易理解了:
bool *Handler::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 - Get*ReservedBytes(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 *Error.
     */
NO_ARG_RUNTIME_EXCEPTION art_quick_throw_stack_overflow, artThrow*FromCode
看下这个宏:
.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 artThrow*FromCode
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  artThrow*FromCode                   @ \cxx_name(Thread*)
END art_quick_throw_stack_overflow
所以最终是跳转到 artThrow*FromCode函数,进行异常的抛出:
extern "C" NO_RETURN void artThrow*FromCode(Thread* self)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  ScopedQuickEntrypointChecks sqec(self);
  Throw*Error(self);
  self->QuickDeliverException();
}
Throw*Error()函数,会创建一个 *Error类对象,并设置好 error message,stack trace等信息,然后使用 self->SetException(e),把这个Throwable对象设置到当前线程的 tlsPtr_.exception 成员中,以便后续通过self->GetException()进行获取。
void Throw*Error(Thread* self) {

  self->SetStackEndFor*();  // 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_*Error));
  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 *Error because JNI NewStringUTF failed.";
    }
  } else {
    error_msg = "Could not allocate *Error object.";
  }

  if (!error_msg.empty()) {
    LOG(WARNING) << error_msg;
    CHECK(self->IsExceptionPending());
  }

  bool explicit_overflow_check = Runtime::Current()->Explicit*Checks();
  self->ResetDefaultStackEnd();  // Return to default stack size.

  // And restore protection if implicit checks are on.
  if (!explicit_overflow_check) {
    self->ProtectStack();
  }
}

需要说明的是 SetStackEndFor*()函数设置   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 起来。

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

分析过程中的几个疑问

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

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

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

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

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

   上面提到的这种是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()->Explicit*Checks()) {
    // 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 += Get*ReservedBytes(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::k*ImplicitCheckSize +
        Get*ReservedBytes(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;
}