线程栈

Linux机器上线程的栈(Stack)大小默认为8M。维护着这个线程里函数的调用关系,每个函数对应一帧(Frame)。

stack-frame.png

x86_64栈帧

具体看下这段C程序在x86_64体系的CPU上栈帧是怎么维护的。

int sum(int i, int j) {
    return i + j;
}
int main(int argc, char* args[]) {
    int x = sum(1, 2);
    return 0;
}

以上程序编译后,通过objdump -d导出汇编。main函数部分如下

000000000040050d <_Z3sumii>:
  40050d:   55                      push   %rbp //把上一帧基址rbp压栈(注意这一步会改变rsp)
  40050e:   48 89 e5                mov    %rsp,%rbp //把rbp指向上一帧的rsp(从这里可以看出保存上一个帧rbp的位置不算在这一帧里的)
  400511:   89 7d fc                mov    %edi,-0x4(%rbp) //参数1
  400514:   89 75 f8                mov    %esi,-0x8(%rbp) //参数2
  400517:   8b 45 f8                mov    -0x8(%rbp),%eax //add参数寄存器
  40051a:   8b 55 fc                mov    -0x4(%rbp),%edx //add参数寄存器
  40051d:   01 d0                   add    %edx,%eax //add结果在eax
  40051f:   5d                      pop    %rbp //把rsp指向rbp(也就是销毁这一帧)
  400520:   c3                      retq //把栈顶值压入rip(也就是执行return address)

0000000000400521 <main>:
  400521:   55                      push   %rbp
  400522:   48 89 e5                mov    %rsp,%rbp
  400525:   48 83 ec 20             sub    $0x20,%rsp
  400529:   89 7d ec                mov    %edi,-0x14(%rbp)
  40052c:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
  400530:   be 02 00 00 00          mov    $0x2,%esi //参数2
  400535:   bf 01 00 00 00          mov    $0x1,%edi //参数1
  40053a:   e8 ce ff ff ff          callq  40050d <_Z3sumii> //push %rip(return address) + jump sum
  40053f:   89 45 fc                mov    %eax,-0x4(%rbp)
  400542:   b8 00 00 00 00          mov    $0x0,%eax
  400547:   c9                      leaveq 
  400548:   c3                      retq   
  400549:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

汇编指令callq、retq、push、pop和rbp(栈基地址寄存器)、rsp(栈顶地址寄存器)、rip(指令寄存器)一起配合构造起了程序的运行栈。
gdb-stack.png

x86-64指令集见https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/24592.pdf

插个题外话。
retq指令是完全信任并且去执行上一帧里保存的return地址的。这在正常情况下是没有问题的。
但是,栈是从高地址到低地址分配的,而帧上的局部变量是从低地址开始写的。如果局部变量因为内存越界,它其实覆盖的是高地址区域也就是上一帧的内容。在某种精心的设计下,可能就刚好把return地址改到了一个恶意的地址上。所以内存越界不只是简单的程序bug,更是严重的安全漏洞,是给黑客的特洛伊木马开了一道门。

Java栈帧

上面x86_64指令集维护函数栈的过程,JVM中通过C++代码也做了类似的一个事情。

关于Java栈构造的代码在
jdk\src\hotspot\share\runtime\javaCalls.cpp

构造Java栈的最重要工作是JavaCallWrapper类完成的

// A JavaCallWrapper is constructed before each JavaCall and destructed after the call.
// Its purpose is to allocate/deallocate a new handle block and to save/restore the last
// Java fp/sp. A pointer to the JavaCallWrapper is stored on the stack.

class JavaCallWrapper: StackObj {
  friend class VMStructs;
 private:
  JavaThread*      _thread;                 // the thread to which this call belongs
  JNIHandleBlock*  _handles;                // the saved handle block
  Method*          _callee_method;          // to be able to collect arguments if entry frame is top frame
  oop              _receiver;               // the receiver of the call (if a non-static call)

  JavaFrameAnchor  _anchor;                 // last thread anchor state that we must restore

  JavaValue*       _result;                 // result value
}

Java调用都是从JavaCalls::call_helper开始的。
void JavaCalls::call(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS)
这个函数的工作

  • 检查必要的上下文
  • 检查method是否已编译,没有则编译
  • 构造Java栈,JavaCallWrapper::JavaCallWrapper
    Java栈的地址和指令保存在JavaFrameAnchor对象上,JavaThread上有记录。
  • 调用底层的Stub接口StubRoutines::call_stub执行method.entry_point
  • 销毁Java栈,JavaCallWrapper::~JavaCallWrapper

构造Java栈的代码

JavaCallWrapper::JavaCallWrapper(const methodHandle& callee_method, Handle receiver, JavaValue* result, TRAPS) {
  JavaThread* thread = (JavaThread *)THREAD;
  bool clear_pending_exception = true;

  guarantee(thread->is_Java_thread(), "crucial check - the VM thread cannot and must not escape to Java code");
  assert(!thread->owns_locks(), "must release all locks when leaving VM");
  guarantee(thread->can_call_java(), "cannot make java calls from the native compiler");
  _result   = result;

  // Allocate handle block for Java code. This must be done before we change thread_state to _thread_in_Java_or_stub,
  // since it can potentially block.
  JNIHandleBlock* new_handles = JNIHandleBlock::allocate_block(thread);

  // After this, we are official in JavaCode. This needs to be done before we change any of the thread local
  // info, since we cannot find oops before the new information is set up completely.
  ThreadStateTransition::transition(thread, _thread_in_vm, _thread_in_Java);

  // Make sure that we handle asynchronous stops and suspends _before_ we clear all thread state
  // in JavaCallWrapper::JavaCallWrapper(). This way, we can decide if we need to do any pd actions
  // to prepare for stop/suspend (flush register windows on sparcs, cache sp, or other state).
  if (thread->has_special_runtime_exit_condition()) {
    thread->handle_special_runtime_exit_condition();
    if (HAS_PENDING_EXCEPTION) {
      clear_pending_exception = false;
    }
  }


  // Make sure to set the oop's after the thread transition - since we can block there. No one is GC'ing
  // the JavaCallWrapper before the entry frame is on the stack.
  _callee_method = callee_method();
  _receiver = receiver();

#ifdef CHECK_UNHANDLED_OOPS
  THREAD->allow_unhandled_oop(&_receiver);
#endif // CHECK_UNHANDLED_OOPS

  _thread       = (JavaThread *)thread;
  _handles      = _thread->active_handles();    // save previous handle block & Java frame linkage

  // For the profiler, the last_Java_frame information in thread must always be in
  // legal state. We have no last Java frame if last_Java_sp == NULL so
  // the valid transition is to clear _last_Java_sp and then reset the rest of
  // the (platform specific) state.

  _anchor.copy(_thread->frame_anchor());
  _thread->frame_anchor()->clear();

  debug_only(_thread->inc_java_call_counter());
  _thread->set_active_handles(new_handles);     // install new handle block and reset Java frame linkage

  assert (_thread->thread_state() != _thread_in_native, "cannot set native pc to NULL");

  // clear any pending exception in thread (native calls start with no exception pending)
  if(clear_pending_exception) {
    _thread->clear_pending_exception();
  }

  if (_anchor.last_Java_sp() == NULL) {
    _thread->record_base_of_stack_pointer();
  }
}

销毁Java栈的代码

JavaCallWrapper::~JavaCallWrapper() {
  assert(_thread == JavaThread::current(), "must still be the same thread");

  // restore previous handle block & Java frame linkage
  JNIHandleBlock *_old_handles = _thread->active_handles();
  _thread->set_active_handles(_handles);

  _thread->frame_anchor()->zap();

  debug_only(_thread->dec_java_call_counter());

  if (_anchor.last_Java_sp() == NULL) {
    _thread->set_base_of_stack_pointer(NULL);
  }


  // Old thread-local info. has been restored. We are not back in the VM.
  ThreadStateTransition::transition_from_java(_thread, _thread_in_vm);

  // State has been restored now make the anchor frame visible for the profiler.
  // Do this after the transition because this allows us to put an assert
  // the Java->vm transition which checks to see that stack is not walkable
  // on sparc/ia64 which will catch violations of the reseting of last_Java_frame
  // invariants (i.e. _flags always cleared on return to Java)

  _thread->frame_anchor()->copy(&_anchor);

  // Release handles after we are marked as being inside the VM again, since this
  // operation might block
  JNIHandleBlock::release_block(_old_handles, _thread);
}

最终的Java栈结构如下
java-stack.png

ZGC在初始标记阶段,扫描GC Roots对象就是从每个JavaThread线程的_anchor对象开始遍历的。



微信扫描下方的二维码阅读本文

上一篇: JDK源码阅读-klass和oop

下一篇: JDK源码阅读-Safepoint

Categories: 编程语言

0 Comments

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注