3

《Chrome V8原理讲解》第十三篇 String类方法的源码分析

 2 years ago
source link: https://zhuanlan.zhihu.com/p/431162002
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

《Chrome V8原理讲解》第十三篇 String类方法的源码分析

chrome v8连载,3~4天一篇,持续更新中...

Javascript对象是属性和一些成员值的集合。通过“.”符号来引用属性值。字符串(string)不是对象,为什么它会有属性呢?因为引用字符串的属性时,V8会将字符串转换成对象,此时对象就有了操作字符串的方法,这一转换过程对Javascript开发人员不可见,通过分析V8源码可以清楚细看这一过程,可以更好地理解Javascript中String类型的行为和特点。本文详细讲解V8中字符串相关的源码,对字符串的定义方法(章节2),字符串对象转换及重要数据结构(章节3)进行说明。最后进行技术总结,并介绍本文使用的V8调试手段(章节4)。

2 字符串String定义

在《Javascript:The Definitive Guide》书中提到:“字符串不是对象,它为什么有属性?”见下面代码。

var s = "hello world!";
var word = s.substring(s.indexOf(" ")+1,s.length);
console.log(word)

书中给出的答案是:“只要引用了字符串s的属性,Javascript会将字符串转换成对象—new String(s)”。我们知道,转换过程是由V8完成,为了看懂这一转换过程,我将其拆解成以下两个问题进行回答:
(1) 要先确定这个字符串s定义时的类型是简单的字符串,还是对象?如果定义时已经是对象类型,“.”操作时还需要转换吗?
(2) 如果需要转换,它一定发生在代码运行期。需要跟踪字节码的运行,才能看到转换过程。
如下是这段JS源码对应的字节码,我们先来确定字符串s定义后的类型是什么,即回答问题1。

1.  [generated bytecode for function:  (0x03d78d381bd1 <SharedFunctionInfo>)]
2.  Parameter count 1
3.  Register count 6
4.  Frame size 48
5.           000003D78D381D06 @    0 : 12 00             LdaConstant [0]
6.           000003D78D381D08 @    2 : 26 fa             Star r1
7.           000003D78D381D0A @    4 : 0b                LdaZero
8.           000003D78D381D0B @    5 : 26 f9             Star r2
9.           000003D78D381D0D @    7 : 27 fe f8          Mov <closure>, r3
10.           000003D78D381D10 @   10 : 61 2d 01 fa 03    CallRuntime [DeclareGlobals], r1-r3
11.      0 E> 000003D78D381D15 @   15 : a7                StackCheck
12.      8 S> 000003D78D381D16 @   16 : 12 01             LdaConstant [1]
13.      8 E> 000003D78D381D18 @   18 : 15 02 04          StaGlobal [2], [4]
14.     36 S> 000003D78D381D1B @   21 : 13 02 00          LdaGlobal [2], [0]
15.           000003D78D381D1E @   24 : 26 f9             Star r2
16.     38 E> 000003D78D381D20 @   26 : 29 f9 03          LdaNamedPropertyNoFeedback r2, [3]
17.           000003D78D381D23 @   29 : 26 fa             Star r1
18.     48 E> 000003D78D381D25 @   31 : 13 02 00          LdaGlobal [2], [0]
19.           000003D78D381D28 @   34 : 26 f7             Star r4
20.     50 E> 000003D78D381D2A @   36 : 29 f7 04          LdaNamedPropertyNoFeedback r4, [4]
21.           000003D78D381D2D @   39 : 26 f8             Star r3
22.           000003D78D381D2F @   41 : 12 05             LdaConstant [5]
23.           000003D78D381D31 @   43 : 26 f6             Star r5
24.     50 E> 000003D78D381D33 @   45 : 5f f8 f7 02       CallNoFeedback r3, r4-r5
25.     62 E> 000003D78D381D37 @   49 : 40 01 06          AddSmi [1], [6]
26.           000003D78D381D3A @   52 : 26 f8             Star r3
27.     65 E> 000003D78D381D3C @   54 : 13 02 00          LdaGlobal [2], [0]
28.           000003D78D381D3F @   57 : 26 f7             Star r4
29.     67 E> 000003D78D381D41 @   59 : 29 f7 06          LdaNamedPropertyNoFeedback r4, [6]
30.           000003D78D381D44 @   62 : 26 f7             Star r4
31.  //省略很多.................
32.  Constant pool (size = 10)
33.  000003D78D381C71: [FixedArray] in OldSpace
34.   - map: 0x03ac45880169 <Map>
35.   - length: 10
36.             0: 0x03d78d381c11 <FixedArray[8]>
37.             1: 0x03d78d381b69 <String[#12]: hello world!>
38.             2: 0x03d78d381b51 <String[#1]: s>
39.             3: 0x02ed266ea9e9 <String[#9]: substring>
40.             4: 0x02ed266e8121 <String[#7]: indexOf>
41.             5: 0x006d3e183c21 <String[#1]:  >
42.             6: 0x03ac45884819 <String[#6]: length>
43.             7: 0x03ac45885301 <String[#4]: word>
44.             8: 0x02ed266f2441 <String[#7]: console>
45.             9: 0x02ed266f1a81 <String[#3]: log>
46.  Handler Table (size = 0)

先说明一个概念Constant pool,常量池(代码32行至结尾),在Javascript编译期生成,用于存储常量数据的一个字符串类型数组,代码32行开始是长常池,代码37行存储Javascript源码中的hello world!,代码38行存储字符串变量名s。字节码执行过程中,常量池为其提供数据,字节码通过下标对常量池进行访问。
解答问题(1)字符串s定义时的类型是什么?
var s = "hello world!";,由代码12,13行完成全局字符串s的定义。代码12行LdaConstant [1]把常量池中下标为1的元素hello world!(代码37行)加载到累加器;代码13行StaGlobal [2], [4]以常量池[2](代码38行)中的值(s)作为key,把累加器值存为全局变量,也就是s = hello world!
debug跟踪这两条字节码的执行过程,可以看s定义后的类型,分析13行的字节码,得知它最终调用如下代码:

RUNTIME_FUNCTION(Runtime_StoreGlobalICNoFeedback_Miss) {
  HandleScope scope(isolate);
  DCHECK_EQ(2, args.length());
  // Runtime functions don't follow the IC's calling convention.
  Handle<Object> value = args.at(0);
  Handle<Name> key = args.at<Name>(1);

  // TODO(mythria): Replace StoreGlobalStrict/Sloppy with StoreNamed.
  StoreGlobalIC ic(isolate, Handle<FeedbackVector>(), FeedbackSlot(),
                   FeedbackSlotKind::kStoreGlobalStrict);
  RETURN_RESULT_OR_FAILURE(isolate, ic.Store(key, value));
}
//.................分隔线...................................
#define RETURN_RESULT_OR_FAILURE(isolate, call)      \
  do {                                               \
    Handle<Object> __result__;                       \
    Isolate* __isolate__ = (isolate);                \
    if (!(call).ToHandle(&__result__)) {             \
      DCHECK(__isolate__->has_pending_exception());  \
      return ReadOnlyRoots(__isolate__).exception(); \
    }                                                \
    DCHECK(!__isolate__->has_pending_exception());   \
    return *__result__;                              \
  } while (false)

上面这段代码最后的RETURN_RESULT_OR_FAILURE是宏模板,继续跟踪,进入WriteDataValue(),代码如下:

void LookupIterator::WriteDataValue(Handle<Object> value,
                                    bool initializing_store) {
  DCHECK_EQ(DATA, state_);
  Handle<JSReceiver> holder = GetHolder<JSReceiver>();
  if (IsElement()) {
    Handle<JSObject> object = Handle<JSObject>::cast(holder);
    ElementsAccessor* accessor = object->GetElementsAccessor(isolate_);
    accessor->Set(object, number_, *value);
  } else if (holder->HasFastProperties(isolate_)) {
    if (property_details_.location() == kField) {
  //省略很多..............................

WriteDataValue()方法先判断是不是全局对象,测试代码中字符串s定义在全局空间内,所以是全局变量,满足图1中标记位置的判断条件。

注意看图1中的绿色标记,value的值是hello world!,它最终存以字典{key:value}形式存在全局表中,至此完成了s的定义。此时,它在V8中的类型是ONE_BYTE_INTERNALIZED_STRING,单字节字符串,这与书中的说法一致,s是字符串。

3 字符串的substring()方法

下面回答前述的问题2,如何转换。
根据前面的源代码var word = s.substring(s.indexOf(" ")+1,s.length);,现在要执行s.substring(),在V8执行逻辑中,先要获取字符串s的substring()方法,也就是’.’操作。前述字节码中第14、15行把刚定义的字节串s存到了r2寄存器中,代码16行:
LdaNamedPropertyNoFeedback r2, [3]
这条字节码的作用是在r2寄存器(字符串变量s的地址)内容中获取常量池[3](字符串substring)中的方法,就是获取s的substring()方法,字节码LdaNamedPropertyNoFeedback源码如下:

1.  IGNITION_HANDLER(LdaNamedPropertyNoFeedback, InterpreterAssembler) {
2.    TNode<Object> object = LoadRegisterAtOperandIndex(0);
3.    TNode<Name> name = CAST(LoadConstantPoolEntryAtOperandIndex(1));
4.    TNode<Context> context = GetContext();
5.    TNode<Object> result =
6.        CallBuiltin(Builtins::kGetProperty, context, object, name);
7.    SetAccumulator(result);
8.    CallRuntime(Runtime::kDebugPrint, context, object, name,result);//这里是我添加的调试命令
9.    Dispatch();
10.  }
11.  //...........................分隔线..........................................
12.  TF_BUILTIN(GetProperty, CodeStubAssembler) {
13.    Node* object = Parameter(Descriptor::kObject);
14.    Node* key = Parameter(Descriptor::kKey);
15.    Node* context = Parameter(Descriptor::kContext);
16.    // TODO(duongn): consider tailcalling to GetPropertyWithReceiver(object,
17.    // object, key, OnNonExistent::kReturnUndefined).
18.    Label if_notfound(this), if_proxy(this, Label::kDeferred),
19.        if_slow(this, Label::kDeferred);
20.    CodeStubAssembler::LookupInHolder lookup_property_in_holder =
21.        [=](Node* receiver, Node* holder, Node* holder_map,
22.            Node* holder_instance_type, Node* unique_name, Label* next_holder,
23.            Label* if_bailout) {
24.          VARIABLE(var_value, MachineRepresentation::kTagged);
25.          Label if_found(this);
26.          TryGetOwnProperty(context, receiver, holder, holder_map,
27.                            holder_instance_type, unique_name, &if_found,
28.                            &var_value, next_holder, if_bailout);
29.          BIND(&if_found);
30.          Return(var_value.value());
31.        };
32.    CodeStubAssembler::LookupInHolder lookup_element_in_holder =
33.        [=](Node* receiver, Node* holder, Node* holder_map,
34.            Node* holder_instance_type, Node* index, Label* next_holder,
35.            Label* if_bailout) {
36.          // Not supported yet.
37.          Use(next_holder);
38.          Goto(if_bailout);
39.        };
40.    TryPrototypeChainLookup(object, object, key, lookup_property_in_holder,
41.                            lookup_element_in_holder, &if_notfound, &if_slow,
42.                            &if_proxy);
43.    BIND(&if_notfound);
44.    Return(UndefinedConstant());
45.    BIND(&if_slow);
46.    TailCallRuntime(Runtime::kGetProperty, context, object, key);
47.    BIND(&if_proxy);
48.    {
49.      // Convert the {key} to a Name first.
50.      TNode<Object> name = CallBuiltin(Builtins::kToName, context, key);
51.      // The {object} is a JSProxy instance, look up the {name} on it, passing
52.      // {object} both as receiver and holder. If {name} is absent we can safely
53.      // return undefined from here.
54.      TailCallBuiltin(Builtins::kProxyGetProperty, context, object, name, object,
55.                      SmiConstant(OnNonExistent::kReturnUndefined));
56.    }
57.  }

调试过程中只有汇编码,无法看到C++源码,因为他们属于Builtin,是预先编译好并存储在snapshot_blob.bin文件中,V8启动时反序列化加载的二进制源码,关于Builtin详见之前的文章。
代码8行,是我加入的调用断点,触发时会进入C++源码环境,方便给大家展示程序状态。代码5行调用TF_BUILTIN(GetProperty, CodeStubAssembler)返回结果result,result是substring()方法的地址指针。

图2是VS2019的调试窗口,前面提到无法做C++源码调试,在这看到的程序状态信息,因为代码8行是我添加的CallRuntime(Runtime::kDebugPrint, context, object, name,result)指令,才得以进入C++环境查看程序状态。这里只能看到程序状态,不要误以为这种方式可以摆脱汇编,一直在C++源码中调试执行,看图3调用堆栈。

图3中可以看到,调用堆栈只有两层,因为DebugPrintBuiltin::LdaNamedPropertyNoFeedback调用,退出后还要回到汇编代码。
图2中的args[0]是’hello world!’,它是之前声明的全局变量s,注意看它的类型依旧就是ONE_BYTE_INTERNALIZED_STRING;再看args[1]substring,是从常量池中读取的,它的类型同上;最后看args[2],它的类型是JS_FUNCTION,这就是获取的substring()方法的地址指针,注意与args[1]的区别。
到此,转换过程完毕,我们并没有看到字符串s的类型在V8中发生过变化,但也没有阻碍获取字符串substring()方法。

4 技术总结

(1) 相对于bool等几个基础类型,String是复合类型,本文只讲了一种字符串ONE_BYTE类型,下面给出其它字符串类型。

switch (map.instance_type()) {
    case CONS_STRING_TYPE:
    case CONS_ONE_BYTE_STRING_TYPE:
    case THIN_STRING_TYPE:
    case THIN_ONE_BYTE_STRING_TYPE:
    case SLICED_STRING_TYPE:
    case SLICED_ONE_BYTE_STRING_TYPE:
    case EXTERNAL_STRING_TYPE:
    case EXTERNAL_ONE_BYTE_STRING_TYPE:
    case UNCACHED_EXTERNAL_STRING_TYPE:
    case UNCACHED_EXTERNAL_ONE_BYTE_STRING_TYPE:
    case STRING_TYPE:
    case ONE_BYTE_STRING_TYPE:
    //省略............................

在V8内部,对String类型做了进一步的详细区分,定义很多不同的字符串类型,上述代码每一个XXX_STRING_TYPE代表一种字符串类型。
(2) 全局字符串s的类型没有因为’.’操作发生变化。这绝对没违背书中所描述的技术原理,只是V8的具体实现方式不同而已。
(3) 从V8源码的角度来讲来,String类的父类是Name,Name的父类是HeapObject,最后是Ojbect类,这么说来,String类本身就是一个堆对象方法。但这和书中说的不是一个概念,书中强调:对字符串进行‘.’操作时,它就不再是字符串,而是一个对象了,注意区别。
(4) 下面是我用的DebugPrint()方法,这是V8内置方法,代码如下。

1.  RUNTIME_FUNCTION(Runtime_DebugPrint) {
2.    SealHandleScope shs(isolate);
3.    //DCHECK_EQ(1, args.length());这行我注释掉了
4.    MaybeObject maybe_object(*args.address_of_arg_at(0));
5.    StdoutStream os;
6.    if (maybe_object->IsCleared()) {
7.      os << "[weak cleared]";
8.    } else {
9.      Object object = maybe_object.GetHeapObjectOrSmi();
10.      bool weak = maybe_object.IsWeak();
11.  #ifdef DEBUG
12.      if (object.IsString() && !isolate->context().is_null()) {
13.        DCHECK(!weak);
14.        // If we have a string, assume it's a code "marker"
15.        // and print some interesting cpu debugging info.
16.        object.Print(os);
17.        JavaScriptFrameIterator it(isolate);
18.        JavaScriptFrame* frame = it.frame();
19.        os << "fp = " << reinterpret_cast<void*>(frame->fp())
20.           << ", sp = " << reinterpret_cast<void*>(frame->sp())
21.           << ", caller_sp = " << reinterpret_cast<void*>(frame->caller_sp())
22.           << ": ";
23.      } else {
24.        os << "DebugPrint: ";
25.        if (weak) {
26.          os << "[weak] ";
27.        }
28.        object.Print(os);
29.      }
30.      if (object.IsHeapObject()) {
31.        HeapObject::cast(object).map().Print(os);
32.      }
33.  #else
34.      if (weak) {
35.        os << "[weak] ";
36.      }
37.      // ShortPrint is available in release mode. Print is not.
38.      os << Brief(object);
39.  #endif
40.    }
41.    os << std::endl;
42.    return args[0];  // return TOS
43.  }

该方法类似“断点”功能,在调试V8源码时,可以在Builtin中使用,并把想要观察的变量作为参数传入,就可以回到C++环境查看程序状态。
这个方法原本是为调试Javascript源码提供的,在JS中的使用方式是%DebugPrint()。我用来充当调试断点,给大家展示程序中断状态。在Builtin中的调用方法是:CallRuntime(Runtime::kDebugPrint, context, your args0,your args1....);
好了,今天到这里,下次见。

恳请读者批评指正、提出宝贵意见
微信:qq9123013 备注:v8交流 邮箱:[email protected]

本文由灰豆原创发布

出处:https://www.anquanke.com/post/id/256677


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK