《Chrome V8原理讲解》第十八篇 利用汇编看V8,洞察看不见的行为

robots

 

前言

我认为,汇编分析无疑是调试v8的终级武器,当用C++调试无法获取更详细的信息时,汇编分析是最好的帮手,但如果不研究v8字节码或挖漏洞等,此文也许没必要看。本文与第十篇文章讲的是同一件事,但本文讲的方法能让你看到更多更详细的V8内部信息,算是第十篇文章的高配版。

 

1 摘要

图1是字节码(Bytecode)的执行示意图,说明了JavaScript字节码的解释执行(interpreter)过程,描绘Javascript语言这图够足用,但对学习V8源码来说,图1不够详细,它隐藏了解释器(Ignition)的工作流程,没有说明字节码的加载(load)和调度(dispatch)细节,等等。本文在图1基础上进一步深入,拆解字节码的解释细节,包括:Builtin加载与执行、字节码加载与调用等过程,详细讲解字节码解释执行过程,力求在演草纸上为读者描绘解释器的运行细节和字节码的解释过程。本文讲解思路是:利用汇编调试(debug),跟随JSFunction实例的执行,它执行到哪,就讲解到哪。本文内容组织方式:记录关键变量地址,做调试前的准备工作(章节2);调试Bytecode解释执行过程(章节3)。
注意: 字节码处理程序(BytecodeHandler)是CodeStubAssembler类型的Builtin功能,debug分析只能看到汇编码,无C++代码。

 

2 准备工作

测试用例是console.log(JsPrint(6));,它的Bytecode Array代码如下:

162BD81556 @  0 : 12 00             LdaConstant [0]
162BD81558 @  2 : 26 fa             Star r1
162BD8155A @  4 : 0b                LdaZero
162BD8155B @  5 : 26 f9             Star r2
162BD8155D @  7 : 27 fe f8          Mov <closure>, r3
162BD81560 @ 10 : 61 2d 01 fa 03    CallRuntime [DeclareGlobals], r1-r3
162BD81565 @ 15 : a7                StackCheck
162BD81566 @ 16 : 13 01 02          LdaGlobal [1], [2]
162BD81569 @ 19 : 26 f9             Star r2
162BD8156B @ 21 : 29 f9 02          LdaNamedPropertyNoFeedback r2, [2]
162BD8156E @ 24 : 26 fa             Star r1
162BD81570 @ 26 : 0d                LdaUndefined
162BD81571 @ 27 : 26 f7             Star r4
162BD81573 @ 29 : 13 03 00          LdaGlobal [3], [0]
162BD81576 @ 32 : 26 f8             Star r3
162BD81578 @ 34 : 0c 06             LdaSmi [6]
162BD8157A @ 36 : 26 f6             Star r5
162BD8157C @ 38 : 5f f8 f7 02       CallNoFeedback r3, r4-r5
162BD81580 @ 42 : 26 f8             Star r3
162BD81582 @ 44 : 5f fa f9 02       CallNoFeedback r1, r2-r3
162BD81586 @ 48 : 26 fb             Star r0
162BD81588 @ 50 : ab                Return

汇编调试存在工作量大、晦涩难懂等诸多缺点,但它最大的优点是:能看到加载、解释、调度的细节,这是其它语言不具备的优势。开始汇编调试之前,要先看懂V8堆栈构建,字节码和寄存器编码规则等知识,它是本文必用的知识,参见第七篇。Invoke()是最后一个可以看到C++源码的函数,源码如下:

1.  V8_WARN_UNUSED_RESULT MaybeHandle<Object> Invoke(Isolate* isolate,
2.                                                  const InvokeParams& params) {
3.   if (params.target->IsJSFunction()) {
4.  //............省略很多...............
5.   }
6.    Object value;
7.     Handle<Code> code =
8.         JSEntry(isolate, params.execution_target, params.is_construct);
9.     {
10.       SaveContext save(isolate);
11.       SealHandleScope shs(isolate);
12.       if (FLAG_clear_exceptions_on_js_entry) isolate->clear_pending_exception();
13.       if (params.execution_target == Execution::Target::kCallable) {
14.         using JSEntryFunction = GeneratedCode<Address(
15.             Address root_register_value, Address new_target, Address target,
16.             Address receiver, intptr_t argc, Address** argv)>;
17.         // clang-format on
18.         JSEntryFunction stub_entry =
19.             JSEntryFunction::FromAddress(isolate, code->InstructionStart());
20.         Address orig_func = params.new_target->ptr();
21.         Address func = params.target->ptr();
22.         Address recv = params.receiver->ptr();
23.         Address** argv = reinterpret_cast<Address**>(params.argv);
24.         RuntimeCallTimerScope timer(isolate, RuntimeCallCounterId::kJS_Execution);
25.         value = Object(stub_entry.Call(isolate->isolate_data()->isolate_root(),
26.                                        orig_func, func, recv, params.argc, argv));
27.       } else {
28.  //............省略很多...............
29.       }
30.     }
31.     //............省略很多...............
32.     return Handle<Object>(value, isolate);
33.   }

上述代码Invoke()为解释执行JSFunction实例做初始化工作,我们需要在此处记录JSFunction实例重要成员变量的地址:code、stub_entry、func,等等。以前的文章对这些变量知识做过讲解,本文不再赘述,直接给出结果:
(1)code,代码7行,它是Builtin:JSEntry的基址。
地址:1FA 0E06 ED30
(2)stub_entry,代码18行,从code中计算Builtin:JSEntry中第一条指令地址,它是Ignition工作的入口地址。JSEntry的头部填充了其它信息,要利用堆栈偏移量取出正确数值,代码如下:

Address Code::OffHeapInstructionStart() const {
  DCHECK(is_off_heap_trampoline());
  if (Isolate::CurrentEmbeddedBlob() == nullptr) return raw_instruction_start();
  EmbeddedData d = EmbeddedData::FromBlob();
  return d.InstructionStartOfBuiltin(builtin_index());
}

第一条指令地址:1FA 1326 1840
(3)func,代码21行,是JSFunction实例地址,它的Javascript源码是console.log(JsPrint(6));
地址:16 2BD8 15A9
(4)func中使用Builtin::InterpreterEntryTrampoline,地址:52 61C0 8A41
(5)dispatch_table,调度表基址:1FA 0E08 CFB0
以上信息用来在debug过程中对代码进行定位,图2给出当前位置函数调用堆栈,之后进入汇编debug。

 

3 调试字节码

开始执行汇编,图3给出了Invoke()方法25行stub_entry.Call()的汇编代码和寄存器信息。

RCX的值是stub_entry,是Builtin:JSEntry的第一条指令地址,code是Builtin:JSEntry的基址,RCX(stub_entry)是code中第一条指令地址。code加上偏移量得到的结果正是stub_entry,前面提到stub_entry是Ignition入口,接着执行见图4。

RBX的值为1FA0E068A00,它是Invoke()方法25行stub_entry.Call()的第一个参数isolate->isolate_data()->isolate_root()。图4中能看到把RCX(stub_entry)存入rsp+58h,继续执行。

图5中R8是func地址,call rsi这条指令时,RSI是stub_entry,执行Builtin:JSEntry,Ignition开始执行。 源码如下:

1.  void Builtins::Generate_JSEntry(MacroAssembler* masm) {
2.    Generate_JSEntryVariant(masm, StackFrame::ENTRY,
3.                            Builtins::kJSEntryTrampoline);
4.  }
5.  //======================分隔线========================
6.  void Generate_JSEntryVariant(MacroAssembler* masm, StackFrame::Type type,
7.                               Builtins::Name entry_trampoline) {
8.    Label invoke, handler_entry, exit;
9.    Label not_outermost_js, not_outermost_js_2;
10.    {  
11.      NoRootArrayScope uninitialized_root_register(masm);
12.      __ pushq(rbp);
13.      __ movq(rbp, rsp);
14.      __ Push(Immediate(StackFrame::TypeToMarker(type)));
15.      __ AllocateStackSpace(kSystemPointerSize);
16.      __ pushq(r12);
17.      __ pushq(r13);
18.      __ pushq(r14);
19.      __ pushq(r15);
20.  #ifdef _WIN64
21.      __ pushq(rdi);  // Only callee save in Win64 ABI, argument in AMD64 ABI.
22.      __ pushq(rsi);  // Only callee save in Win64 ABI, argument in AMD64 ABI.
23.  #endif
24.      __ pushq(rbx);
25.  #ifdef _WIN64 //这个#ifdef为TRUE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
26.      // On Win64 XMM6-XMM15 are callee-save.
27.      __ AllocateStackSpace(EntryFrameConstants::kXMMRegistersBlockSize);
28.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 0), xmm6);
29.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 1), xmm7);
30.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 2), xmm8);
31.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 3), xmm9);
32.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 4), xmm10);
33.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 5), xmm11);
34.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 6), xmm12);
35.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 7), xmm13);
36.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 8), xmm14);
37.      __ movdqu(Operand(rsp, EntryFrameConstants::kXMMRegisterSize * 9), xmm15);
38.      STATIC_ASSERT(EntryFrameConstants::kCalleeSaveXMMRegisters == 10);
39.      STATIC_ASSERT(EntryFrameConstants::kXMMRegistersBlockSize ==
40.                    EntryFrameConstants::kXMMRegisterSize *
41.                        EntryFrameConstants::kCalleeSaveXMMRegisters);
42.  #endif
43.  //............省略很多..............................
44.  }

上述代码12~37行,与图6中标记的汇编码完全对应,Builtin:JSEntry的功能是按标准C语言调用约定组织信息,为Builtin::InterpreterEntryTrampoline做准备。

Builtin:JSEntry和Builtin::InterpreterEntryTrampoline源码请读者自行分析,不再赘述。Builtin::InterpreterEntryTrampoline源码中调用dispatch_table,进入测试用例的第一条字节码LdaConstant,图7给出加载dispatch_table,并计算和调用LdaConstant的过程。

R15寄存器是dispatch_table基址,R15是V8维护的指令调度专用物理寄器,图7中(1)加载dispatch_table基址;(2)计算LdaConstant地址;(3)调用LdaConstant。
总结:图7中,三个功能的组合,实现了指令调度,也就是每个字节码处理程序都有的尾部调用(TailCall)方法Dispatch()
继续执行,进入LdaConstant的处理程序,代码如下:

1.  IGNITION_HANDLER(LdaConstant, InterpreterAssembler) {
2.    TNode<Object> constant = LoadConstantPoolEntryAtOperandIndex(0);
3.    SetAccumulator(constant);
4.    Dispatch();
5.  }

汇编码篇幅太长,字节码处理程序内部的汇编过程不做讲解,代码第4行Dispatch();调度下一条字节码Star,图8是调度的汇编码。

图8中,R15依旧是dispatch_table基址,这正是V8官方文档中提到的:“用物理寄存器维护dispatch table”。 接下进入Star开始执行,剩余代码请读者自行分析。

总结,本文从汇编角度对console.log(JsPrint(6));做了细致讲解,包括:Builtin::JSEntry、InterpreterEntryTrampoline的执行,字节码的加载(load)、解释(interpreter)以及调度(disaptch)四方面内容,描绘了Ignition从启动到解释字节码的全流程。
好了,今天到这里,下次见。

恳请读者批评指正、提出宝贵意见
微信:qq9123013 备注:v8交流 邮箱:v8blink@outlook.com

(完)