Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

001-v8初始化分析 #82

Open
xinali opened this issue Sep 2, 2022 · 0 comments
Open

001-v8初始化分析 #82

xinali opened this issue Sep 2, 2022 · 0 comments

Comments

@xinali
Copy link
Owner

xinali commented Sep 2, 2022

v8初始化分析

环境搭建

这部分比较简单拉取最新的v8代码,并编译测试

拉取代码,过程也比较简单

# get depot_tools
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

# 注意PATH中不能含有~,否则下一步操作会报错
export PATH=/path/to/depot_tools:$PATH

# get v8 code
fetch v8

# sync libs
gclient sync

# compile && build
tools/dev/gm.py x64.release

# 如果想编译测试部分
tools/dev/gm.py x64.release.check

上述是官方编译方法,在没有问题的情况下,如果想要自定义一些编译参数,打开out/x64.release/args.gn,写入一下内容,这里这么做,主要方便调试,会自动编译v8 samples下的文件

is_debug = true
symbol_level = 2
v8_enable_backtrace = true
v8_optimized_debug = false

然后重新编译

cd src/out/x64.release
ninja

具体可以使用哪些编译参数,可以使用gn获取

➜  v8 git:(035982c6dd) gn args --list out/x64.release | more
action_pool_depth
    Current value (from the default) = -1
      From //build/toolchain/BUILD.gn:11

    Pool for non goma tasks.

added_rust_stdlib_libs
    Current value (from the default) = []
      From //build/config/rust.gni:44

    Any extra std rlibs in your Rust toolchain, relative to the standard
    Rust toolchain. Typically used with 'use_unverified_rust_toolchain' = true

also_build_ash_chrome
    Current value (from the default) = false
      From //build/config/chromeos/ui_mode.gni:26

    Setting this to true when building linux Lacros-chrome will cause it to
    *also* build linux ash-chrome in a subdirectory using an alternate
    toolchain.
    Don't set this unless you're sure you want it, because it'll double
    your build time.

also_build_lacros_chrome
    Current value (from the default) = false
      From //build/config/chromeos/ui_mode.gni:30

    Setting this to true when building linux ash-chrome will cause it to
    *also* build linux Lacros-chrome in a subdirectory using an alternate toolchain.

also_build_lacros_chrome_for_architecture
    Current value (from the default) = ""
      From //build/config/chromeos/ui_mode.gni:35
      ...

编译完成,就可以看到如下文件

➜  x64.release git:(035982c6dd) ✗ ll
total 1664992
-rw-r--r--    1 xina1i  staff    86B Jul 19 10:11 args.gn
-rwxr-xr-x    1 xina1i  staff   303K Jul 19 10:15 bigint_shell
-rw-r--r--    1 xina1i  staff    29K Jul 19 10:11 build.ninja
-rw-r--r--    1 xina1i  staff   3.6K Jul 19 10:11 build.ninja.d
-rwxr-xr-x    1 xina1i  staff   339K Jul 19 10:15 bytecode_builtins_list_generator
-rwxr-xr-x    1 xina1i  staff    32M Jul 19 11:19 cctest
...
-rwxr-xr-x    1 xina1i  staff    79M Jul 19 11:18 unittests
-rw-r--r--    1 xina1i  staff   975B Jul 19 10:14 v8_build_config.json
-rwxr-xr-x    1 xina1i  staff   2.2M Jul 19 10:15 v8_heap_base_unittests
-rwxr-xr-x    1 xina1i  staff    56K Jul 19 11:51 v8_hello_world
-rwxr-xr-x    1 xina1i  staff   195K Jul 19 11:18 v8_sample_process
-rwxr-xr-x    1 xina1i  staff    61K Jul 19 11:18 v8_shell
-rwxr-xr-x    1 xina1i  staff   1.0M Jul 19 11:18 v8_simple_inspector_fuzzer
-rwxr-xr-x    1 xina1i  staff    46K Jul 19 11:18 v8_simple_json_fuzzer
-rwxr-xr-x    1 xina1i  staff   394K Jul 19 11:18 v8_simple_multi_return_fuzzer
-rwxr-xr-x    1 xina1i  staff    99K Jul 19 11:18 v8_simple_parser_fuzzer
-rwxr-xr-x    1 xina1i  staff   192K Jul 19 11:18 v8_simple_regexp_builtins_fuzzer
-rwxr-xr-x    1 xina1i  staff    99K Jul 19 11:18 v8_simple_regexp_fuzzer
-rwxr-xr-x    1 xina1i  staff   2.4M Jul 19 11:18 v8_simple_wasm_async_fuzzer
-rwxr-xr-x    1 xina1i  staff   2.5M Jul 19 11:18 v8_simple_wasm_code_fuzzer
-rwxr-xr-x    1 xina1i  staff   3.1M Jul 19 11:18 v8_simple_wasm_compile_fuzzer
-rwxr-xr-x    1 xina1i  staff   2.4M Jul 19 11:18 v8_simple_wasm_fuzzer
-rwxr-xr-x    1 xina1i  staff   2.5M Jul 19 11:18 v8_simple_wasm_streaming_fuzzer
-rwxr-xr-x    1 xina1i  staff   494K Jul 19 11:18 wami
-rwxr-xr-x    1 xina1i  staff   127M Jul 19 11:19 wasm_api_tests
-rwxr-xr-x    1 xina1i  staff   160K Jul 19 10:15 zlib_bench

如果对samples或者别的文件进行了更改,直接运行ninja也只会重新编译链接改动的文件,不会重新全量编译整个v8,速度非常快,便于测试。

简单调试及elements分析

根据参考中的文章,我们先从v8 samples里面的shell.cc的代码看起,来看一下elements的处理过程,然后边用lldb调试生成的v8_shell

int main(int argc, char* argv[]) {
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  v8::V8::InitializeExternalStartupData(argv[0]);
  std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::SetFlagsFromCommandLine(&argc, argv, true);
  v8::V8::Initialize();
  ...
}

跟踪Initialize,这里会有多个Initialize,直接通过源码不一定能找到,直接lldb下断点即可

在使用lldb调试器前,先载入chromium调试器的初始化脚本tools/lldb_commands.py,其增加了部分便于调试的指令

关于调试器选择:

  1. windows建议使用windbg/windbg preview
  2. linux建议使用gdb
  3. mac建议使用lldb

针对各个调试器初始化,建议载入chromium预先准备的初始化脚本

image-20220720192925341

回头来看源码,InitializeOncePerProcess实现

void ElementsAccessor::InitializeOncePerProcess() {
  static ElementsAccessor* accessor_array[] = {
#define ACCESSOR_ARRAY(Class, Kind, Store) new Class(),
      ELEMENTS_LIST(ACCESSOR_ARRAY)
#undef ACCESSOR_ARRAY
  };

继续

// First argument in list is the accessor class, the second argument is the
// accessor ElementsKind, and the third is the backing store class.  Use the
// fast element handler for smi-only arrays.  The implementation is currently
// identical.  Note that the order must match that of the ElementsKind enum for
// the |accessor_array[]| below to work.
#define ELEMENTS_LIST(V)                                                      \
  V(FastPackedSmiElementsAccessor, PACKED_SMI_ELEMENTS, FixedArray)           \
  V(FastHoleySmiElementsAccessor, HOLEY_SMI_ELEMENTS, FixedArray)             \
  V(FastPackedObjectElementsAccessor, PACKED_ELEMENTS, FixedArray)            \
  V(FastHoleyObjectElementsAccessor, HOLEY_ELEMENTS, FixedArray)              \
  V(FastPackedDoubleElementsAccessor, PACKED_DOUBLE_ELEMENTS,                 \
  ...

注意到注释里的smi-only arrays,搜一下v8相关的smi arrays,找到了Elements kinds in V8,把文章简单过一下

JavaScript会优化纯数字索引的对象属性,这类属性会使用Array构造,并命名该类属性为elements,其中elements有几种kind

  • Small integers, also known as Smi.

  • Doubles, for floating-point numbers and integers that cannot be represented as a Smi.

  • Regular elements, for values that cannot be represented as Smi or doubles.

对于elementkind转换

  • V8 assigns an elements kind to each array.

  • The elements kind of an array is not set in stone — it can change at runtime. In the earlier example, we transitioned from PACKED_SMI_ELEMENTS to PACKED_ELEMENTS.

  • Elements kind transitions can only go from specific kinds to more general kinds.

除了packed arrays还存在holey arrays,简单举例

const array = [1, 2, 3, 4.56, 'x'];
// elements kind: PACKED_ELEMENTS
array.length; // 5
array[9] = 1; // array[5] until array[8] are now holes
// elements kind: HOLEY_ELEMENTS

针对packedholey

  • The most common elements kinds come in PACKED and HOLEY flavors.

  • Operations on packed arrays are more efficient than operations on holey arrays.

  • Elements kinds can transition from PACKED to HOLEY flavors.

对于两者之间的关系

在文章中,除了说了elements,还顺带提到了JavaScriptproperties

Objects have properties that map to values, whereas arrays have indices that map to elements.

提到了Objectsproperties,这两个构成了简单的Javascript Objects,所以再来看看这个内容

basic JavaScript object如下:

这种是比较简单的JavaScript Objects,但是properties不可能一直这么简单,所以需要更加复杂的结构来记录更加复杂的properties

  • Array-indexed properties are stored in a separate elements store.

  • Named properties are stored in the properties store.

  • Elements and properties can either be arrays or dictionaries.

  • Each JavaScript object has a HiddenClass associated that keeps information about the object shape.

JavaScript Object中,其具体结构

对于hidden class的变化

  • The basic assumption about HiddenClasses is that objects with the same structure — e.g. the same named properties in the same order — share the same HiddenClass. To achieve that we use a different HiddenClass when a property gets added to an object.

根据上面的描述,对于hardden class比较形象的解释

image-20220720105412279

可以看一下这个V8 hidden class and inline cache ppt,说的比较简单,但是解释hidden class比较形象

  • Objects with the same structure (same properties in the same order) have the same HiddenClass

  • By default every new named property added causes a new HiddenClass to be created.

  • Adding array-indexed properties does not create new HiddenClasses.

对于JavaScript Object,除了上面说到的properties/elements,还有一种in-object properties,这种类型直接存储在对象中,所以在查找过程中,速度会特别快,但是因为对象内存空间限制,这种in-object properties是预先初始化就固定的

阶段结论

  • There are three different named property types: in-object, fast and slow/dictionary.
    1. In-object properties are stored directly on the object itself and provide the fastest access.
    2. Fast properties live in the properties store, all the meta information is stored in the descriptor array on the HiddenClass.
    3. Slow properties live in a self-contained properties dictionary, meta information is no longer shared through the HiddenClass.
  • Slow properties allow for efficient property removal and addition but are slower to access than the other two types.

到这里就差不多简单说明了JavaScript Object有关的elements/properties/in-object properties,所以再回过头来看一下其中一个elements kind的定义,就像文章说的,它有很多种kinds,来看其中一个FastPackedSmiElementsAccessor

class FastPackedSmiElementsAccessor
    : public FastSmiOrObjectElementsAccessor<
          FastPackedSmiElementsAccessor,
          ElementsKindTraits<PACKED_SMI_ELEMENTS>> {};

这个类的定义很有意思,子类类型作为模板参数传给基类,这里用到了一种叫做CRPT的方法,父类模板会根据子类传入的参数类型,调用对应子类的方法,与c++的虚函数有点类似,但是会比虚函数的开销更小。

继续跟

class FastSmiOrObjectElementsAccessor
    : public FastElementsAccessor<Subclass, KindTraits> {
 public:
  static inline void SetImpl(Handle<JSObject> holder, InternalIndex entry,
                             Object value) {
    SetImpl(holder->elements(), entry, value);
  }

  static inline void SetImpl(FixedArrayBase backing_store, InternalIndex entry,
                             Object value) {
    FixedArray::cast(backing_store).set(entry.as_int(), value);
  }

  static inline void SetImpl(FixedArrayBase backing_store, InternalIndex entry,
                             Object value, WriteBarrierMode mode) {
    FixedArray::cast(backing_store).set(entry.as_int(), value, mode);
  }
  ...

继续

template <typename Subclass, typename KindTraits>
class FastElementsAccessor : public ElementsAccessorBase<Subclass, KindTraits> {
 public:
  using BackingStore = typename KindTraits::BackingStore;

  static Handle<NumberDictionary> NormalizeImpl(Handle<JSObject> object,
                                                Handle<FixedArrayBase> store) {
    Isolate* isolate = object->GetIsolate();
    ElementsKind kind = Subclass::kind();

    // Ensure that notifications fire if the array or object prototypes are
    // normalizing.
    if (IsSmiOrObjectElementsKind(kind) ||
        kind == FAST_STRING_WRAPPER_ELEMENTS) {
      isolate->UpdateNoElementsProtectorOnNormalizeElements(object);
    }

    int capacity = object->GetFastElementsUsage();
    Handle<NumberDictionary> dictionary =
        NumberDictionary::New(isolate, capacity);

    PropertyDetails details = PropertyDetails::Empty();
    int j = 0;
    int max_number_key = -1;
    for (int i = 0; j < capacity; i++) {
      if (IsHoleyElementsKindForRead(kind)) {
        if (BackingStore::cast(*store).is_the_hole(isolate, i)) continue;
      }
    ...

继续

template <typename Subclass, typename ElementsTraitsParam>
class ElementsAccessorBase : public InternalElementsAccessor {
 public:
  ElementsAccessorBase() = default;
  ElementsAccessorBase(const ElementsAccessorBase&) = delete;
  ElementsAccessorBase& operator=(const ElementsAccessorBase&) = delete;

  using ElementsTraits = ElementsTraitsParam;
  using BackingStore = typename ElementsTraitsParam::BackingStore;

  static ElementsKind kind() { return ElementsTraits::Kind; }
  ...
  Maybe<uint32_t> Push(Handle<JSArray> receiver, BuiltinArguments* args,
                       uint32_t push_size) final {
    return Subclass::PushImpl(receiver, args, push_size);
  }

  static Maybe<uint32_t> PushImpl(Handle<JSArray> receiver,
                                  BuiltinArguments* args, uint32_t push_sized) {
    UNREACHABLE();
  }

  Maybe<uint32_t> Unshift(Handle<JSArray> receiver, BuiltinArguments* args,
                          uint32_t unshift_size) final {
    return Subclass::UnshiftImpl(receiver, args, unshift_size);
  }

  static Maybe<uint32_t> UnshiftImpl(Handle<JSArray> receiver,
                                     BuiltinArguments* args,
                                     uint32_t unshift_size) {
    UNREACHABLE();
  }

  MaybeHandle<Object> Pop(Handle<JSArray> receiver) final {
    return Subclass::PopImpl(receiver);
  }

  static MaybeHandle<Object> PopImpl(Handle<JSArray> receiver) {
    UNREACHABLE();
  }

  MaybeHandle<Object> Shift(Handle<JSArray> receiver) final {
    return Subclass::ShiftImpl(receiver);
  }
  ...

到这个函数中,终于找到了elements的各种操作实现,比如pop/push/slice等等

继续,这里使用到了nested class,主要用于访问控制

// The InternalElementsAccessor is a helper class to expose otherwise protected
// methods to its subclasses. Namely, we don't want to publicly expose methods
// that take an entry (instead of an index) as an argument.
class InternalElementsAccessor : public ElementsAccessor {
 public:
  InternalIndex GetEntryForIndex(Isolate* isolate, JSObject holder,
                                 FixedArrayBase backing_store,
                                 size_t index) override = 0;

  PropertyDetails GetDetails(JSObject holder, InternalIndex entry) override = 0;
};

继续

// Abstract base class for handles that can operate on objects with differing
// ElementsKinds.
class ElementsAccessor {
 public:
  ElementsAccessor() = default;
  virtual ~ElementsAccessor() = default;
  ElementsAccessor(const ElementsAccessor&) = delete;
  ElementsAccessor& operator=(const ElementsAccessor&) = delete;

  // Returns a shared ElementsAccessor for the specified ElementsKind.
  static ElementsAccessor* ForKind(ElementsKind elements_kind) {
    DCHECK_LT(static_cast<int>(elements_kind), kElementsKindCount);
    return elements_accessors_[elements_kind];
  }

  // Checks the elements of an object for consistency, asserting when a problem
  // is found.
  virtual void Validate(JSObject obj) = 0;
  ...
  V8_WARN_UNUSED_RESULT virtual Maybe<uint32_t> Unshift(
      Handle<JSArray> receiver, BuiltinArguments* args,
      uint32_t unshift_size) = 0;

  V8_WARN_UNUSED_RESULT virtual MaybeHandle<Object> Pop(
      Handle<JSArray> receiver) = 0;

  V8_WARN_UNUSED_RESULT virtual MaybeHandle<Object> Shift(
      Handle<JSArray> receiver) = 0;

这个是elements的最顶层的抽象基类,定义了elements的一些基本操作

elements的整个定义及实现过程

ElementsAccessor
  |-> InternalElementsAccessor
        |-> ElementsAccessorBase
              |-> FastElementsAccessor
                    |-> FastSmiOrObjectElementsAccessor
                          |-> FastPackedSmiElementsAccessor
  

到这里,我们搭建了v8的测试环境,并且简单跟了一下elements的实现流程,下一部分具体分析v8的执行过程

v8执行过程

我们来看一下最简单的hello-world.cc来了解一下v8的整体运行流程

// 头文件

int main(int argc, char* argv[]) {
  // Initialize V8.
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  v8::V8::InitializeExternalStartupData(argv[0]);
  std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();

  // Create a new Isolate and make it the current one.
  v8::Isolate::CreateParams create_params;
  create_params.array_buffer_allocator =
      v8::ArrayBuffer::Allocator::NewDefaultAllocator();
  v8::Isolate* isolate = v8::Isolate::New(create_params);
  {
    v8::Isolate::Scope isolate_scope(isolate);

    // Create a stack-allocated handle scope.
    v8::HandleScope handle_scope(isolate);

    // Create a new context.
    v8::Local<v8::Context> context = v8::Context::New(isolate);

    // Enter the context for compiling and running the hello world script.
    v8::Context::Scope context_scope(context);

    {
      // Create a string containing the JavaScript source code.
      v8::Local<v8::String> source =
          v8::String::NewFromUtf8Literal(isolate, "'Hello' + ', World!'");

      // Compile the source code.
      v8::Local<v8::Script> script =
          v8::Script::Compile(context, source).ToLocalChecked();

      // Run the script to get the result.
      v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();

      // Convert the result to an UTF8 string and print it.
      v8::String::Utf8Value utf8(isolate, result);
      printf("%s\n", *utf8);
    }

    {
      // Use the JavaScript API to generate a WebAssembly module.
      //
      // |bytes| contains the binary format for the following module:
      //
      //     (func (export "add") (param i32 i32) (result i32)
      //       get_local 0
      //       get_local 1
      //       i32.add)
      //
      const char csource[] = R"(
        let bytes = new Uint8Array([
          0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01,
          0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07,
          0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, 0x01,
          0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b
        ]);
        let module = new WebAssembly.Module(bytes);
        let instance = new WebAssembly.Instance(module);
        instance.exports.add(3, 4);
      )";

      // Create a string containing the JavaScript source code.
      v8::Local<v8::String> source =
          v8::String::NewFromUtf8Literal(isolate, csource);

      // Compile the source code.
      v8::Local<v8::Script> script =
          v8::Script::Compile(context, source).ToLocalChecked();

      // Run the script to get the result.
      v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();

      // Convert the result to a uint32 and print it.
      uint32_t number = result->Uint32Value(context).ToChecked();
      printf("3 + 4 = %u\n", number);
    }
  }

  // Dispose the isolate and tear down V8.
  isolate->Dispose();
  v8::V8::Dispose();
  v8::V8::DisposePlatform();
  delete create_params.array_buffer_allocator;
  return 0;
}

v8初始化

  // 添加v8对ICU国际化的支持(International Components for Unicode)
  // 这个可以通过在编译时,使用v8_enable_i18n_support参数选择是否支持ICU
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  
  // 载入snapshot相关内容
  v8::V8::InitializeExternalStartupData(argv[0]);
  
  // 初始化平台相关信息,主要包括cpu数目、是否启动stack dump、日志跟踪之类的
  std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  
  v8::V8::Initialize();

每个初始化调用都简单跟踪一下

snapshot初始化

v8 使用了一个快捷方式来加快速度,将先前准备好的快照直接反序列化到堆中以获得初始化的上下文。在普通台式计算机上,这可以将创建上下文的时间从 40 毫秒缩短到不到 2 毫秒。在普通手机上,这可能意味着 270 毫秒和 10 毫秒之间的差异。嵌入 v8Chrome 以外的应用程序可能需要的不仅仅是普通的 JavaScript。许多在启动时加载的额外的库脚本,在“实际”应用程序运行之前。例如,一个基于 v8 的简单 TypeScript VM 必须在启动时加载 TypeScript 编译器,以便将 TypeScript 源代码即时转换为 JavaScript,所以snapshot在对提高v8运行速度

v8::V8::InitializeExternalStartupData
  -> LoadFromFile
      -> Load

核心功能点在Load函数中

void Load(const char* blob_file, v8::StartupData* startup_data,
          void (*setter_fn)(v8::StartupData*)) {
  ClearStartupData(startup_data);

  CHECK(blob_file);

  FILE* file = base::Fopen(blob_file, "rb");
  if (!file) {
    PrintF(stderr, "Failed to open startup resource '%s'.\n", blob_file);
    return;
  }

  fseek(file, 0, SEEK_END);
  startup_data->raw_size = static_cast<int>(ftell(file));
  rewind(file);

  startup_data->data = new char[startup_data->raw_size];
  int read_size = static_cast<int>(fread(const_cast<char*>(startup_data->data),
                                         1, startup_data->raw_size, file));
  base::Fclose(file);

  if (startup_data->raw_size == read_size) {
    (*setter_fn)(startup_data);
  } else {
    PrintF(stderr, "Corrupted startup resource '%s'.\n", blob_file);
  }
}

功能是将snapshot的文件读入结构v8::StartupData

platform相关初始化

hello-world.cc由两行代码完成

// 创建默认platform
std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
// 初始化部分platform成员变量
v8::V8::InitializePlatform(platform.get());

首先来看一下v8::platform::NewDefaultPlatform

std::unique_ptr<v8::Platform> NewDefaultPlatform(
    int thread_pool_size, IdleTaskSupport idle_task_support,
    InProcessStackDumping in_process_stack_dumping,
    std::unique_ptr<v8::TracingController> tracing_controller) {
  if (in_process_stack_dumping == InProcessStackDumping::kEnabled) {
    v8::base::debug::EnableInProcessStackDumping();
  }
  // 设置线程池
  thread_pool_size = GetActualThreadPoolSize(thread_pool_size);
  // 创建platform
  auto platform = std::make_unique<DefaultPlatform>(
      thread_pool_size, idle_task_support, std::move(tracing_controller));
  return platform;
}

跟进DefaultPlatform

class V8_PLATFORM_EXPORT DefaultPlatform : public NON_EXPORTED_BASE(Platform) {
 public:
  // NewDefaultPlatform调用的显示构造函数
  explicit DefaultPlatform(
      int thread_pool_size = 0,
      IdleTaskSupport idle_task_support = IdleTaskSupport::kDisabled,
      std::unique_ptr<v8::TracingController> tracing_controller = {});

  ~DefaultPlatform() override;

  DefaultPlatform(const DefaultPlatform&) = delete;
  DefaultPlatform& operator=(const DefaultPlatform&) = delete;

  void EnsureBackgroundTaskRunnerInitialized();

  bool PumpMessageLoop(
      v8::Isolate* isolate,
      MessageLoopBehavior behavior = MessageLoopBehavior::kDoNotWait);

  void RunIdleTasks(v8::Isolate* isolate, double idle_time_in_seconds);

  void SetTracingController(
      std::unique_ptr<v8::TracingController> tracing_controller);

  using TimeFunction = double (*)();

  void SetTimeFunctionForTesting(TimeFunction time_function);

  // v8::Platform implementation.
  int NumberOfWorkerThreads() override;
  std::shared_ptr<TaskRunner> GetForegroundTaskRunner(
      v8::Isolate* isolate) override;
  void CallOnWorkerThread(std::unique_ptr<Task> task) override;
  void CallDelayedOnWorkerThread(std::unique_ptr<Task> task,
                                 double delay_in_seconds) override;
  bool IdleTasksEnabled(Isolate* isolate) override;
  std::unique_ptr<JobHandle> PostJob(
      TaskPriority priority, std::unique_ptr<JobTask> job_state) override;
  std::unique_ptr<JobHandle> CreateJob(
      TaskPriority priority, std::unique_ptr<JobTask> job_state) override;
  double MonotonicallyIncreasingTime() override;
  double CurrentClockTimeMillis() override;
  v8::TracingController* GetTracingController() override;
  StackTracePrinter GetStackTracePrinter() override;
  v8::PageAllocator* GetPageAllocator() override;

  void NotifyIsolateShutdown(Isolate* isolate);

 private:
  base::Mutex lock_;
  // 线程池数量
  const int thread_pool_size_;
  // task相关
  IdleTaskSupport idle_task_support_;
  std::shared_ptr<DefaultWorkerThreadsTaskRunner> worker_threads_task_runner_;
  std::map<v8::Isolate*, std::shared_ptr<DefaultForegroundTaskRunner>>
      foreground_task_runner_map_;

  std::unique_ptr<TracingController> tracing_controller_;
  // 内存分配器
  std::unique_ptr<PageAllocator> page_allocator_;

  TimeFunction time_function_for_testing_ = nullptr;
};

基本包含了平台用到的所有数据,比如线程池,内存分配等信息

主要看一下,它显示声明的构造函数具体实现,因为基本重要的数据均在这里初始化

DefaultPlatform::DefaultPlatform(
    int thread_pool_size, IdleTaskSupport idle_task_support,
    std::unique_ptr<v8::TracingController> tracing_controller)
    : thread_pool_size_(thread_pool_size),
      idle_task_support_(idle_task_support),
      tracing_controller_(std::move(tracing_controller)),
      page_allocator_(std::make_unique<v8::base::PageAllocator>()) {
  if (!tracing_controller_) {
    tracing::TracingController* controller = new tracing::TracingController();
#if !defined(V8_USE_PERFETTO)
    controller->Initialize(nullptr);
#endif
    tracing_controller_.reset(controller);
  }
  if (thread_pool_size_ > 0) {
    EnsureBackgroundTaskRunnerInitialized();
  }
}

再重点看一下v8::base::PageAllocator的构造,因为这个PageAllocator以后会多次出现

// page-allocator.cc
PageAllocator::PageAllocator()
    : allocate_page_size_(base::OS::AllocatePageSize()),
      commit_page_size_(base::OS::CommitPageSize()) {}

// platform-starborad.cc
// 具体数值,根据平台而定
size_t OS::AllocatePageSize() { return kSbMemoryPageSize; }
size_t OS::CommitPageSize() { return kSbMemoryPageSize; }

PageAllocator构造函数,主要是根据平台确定其两个成员变量数值,这里用lldb简单调试一下,看看mac下这个数值是多少

image-20220725163533590

allocate_page_size_mac下是4096

platform其他变量初始化差不多,主要就是一些平台相关的变量初始化,部分数据初始化由它下一句v8::V8::InitializePlatform(platform.get())实现。

这里面的内容还有很多,比如task初始化,调用栈跟踪初始化处理等等,比如跟PageAllocator一起初始化的v8::TracingControllertaskDefaultWorkerThreadsTaskRunner,这部分在有需要的时候再回头来看

梳理一下调用关系

v8::platform::NewDefaultPlatform
  |
  |-> std::make_unique<DefaultPlatform>
  |         |
  |         |-> std::make_unique<v8::base::PageAllocator>
  |                 |
  |                 |-> PageAllocator::PageAllocator
  |
  |-> thread_pool_size (线程池大小)
  |   idle_task_support (空闲任务)
  |   in_process_stack_dumping (栈转储)
  |
  |-> v8::TracingController (trace event, --enable-tracing)

v8::V8::Initialize 初始化

中间涉及到多次的模板实现,直接代码跟踪会有点绕,直接利用lldb跟踪一下

image-20220721162031911

到这里算是真正进入v8初始化过程,来具体分析一下该函数干了哪些事

void V8::Initialize() {
  AdvanceStartupState(V8StartupState::kV8Initializing);
  CHECK(platform_);

  // Update logging information before enforcing flag implications.
  FlagValue<bool>* log_all_flags[] = {&FLAG_turbo_profiling_log_builtins,
                                      ...
                                      &FLAG_log_ic,
                                      &FLAG_log_maps};
  // 一系列的log设置
  if (FLAG_log_all) {
   ...
  }

  FlagList::EnforceFlagImplications();

  if (FLAG_predictable && FLAG_random_seed == 0) {
    // Avoid random seeds in predictable mode.
    FLAG_random_seed = 12347;
  }
  
  // 一些flag判断处理,包括turbo优化等
  ...
  // The --jitless and --interpreted-frames-native-stack flags are incompatible
  // since the latter requires code generation while the former prohibits code
  // generation.
  CHECK(!FLAG_interpreted_frames_native_stack || !FLAG_jitless);

  
  base::OS::Initialize(FLAG_hard_abort, FLAG_gc_fake_mmap);

  if (FLAG_random_seed) {
    GetPlatformPageAllocator()->SetRandomMmapSeed(FLAG_random_seed);
    GetPlatformVirtualAddressSpace()->SetRandomSeed(FLAG_random_seed);
  }

  if (FLAG_print_flag_values) FlagList::PrintValues();

  // Initialize the default FlagList::Hash.
  FlagList::Hash();

  // Before initializing internals, freeze the flags such that further changes
  // are not allowed. Global initialization of the Isolate or the WasmEngine
  // already reads flags, so they should not be changed afterwards.
  if (FLAG_freeze_flags_after_init) FlagList::FreezeFlags();

#if defined(V8_ENABLE_SANDBOX)
  // If enabled, the sandbox must be initialized first.
  GetProcessWideSandbox()->Initialize(GetPlatformVirtualAddressSpace());
  CHECK_EQ(kSandboxSize, GetProcessWideSandbox()->size());
#endif

#if defined(V8_USE_PERFETTO)
  if (perfetto::Tracing::IsInitialized()) TrackEvent::Register();
#endif
  IsolateAllocator::InitializeOncePerProcess();
  Isolate::InitializeOncePerProcess();

#if defined(USE_SIMULATOR)
  Simulator::InitializeOncePerProcess();
#endif
  CpuFeatures::Probe(false);
  ElementsAccessor::InitializeOncePerProcess();
  Bootstrapper::InitializeOncePerProcess();
  CallDescriptors::InitializeOncePerProcess();
#if V8_ENABLE_WEBASSEMBLY
  wasm::WasmEngine::InitializeOncePerProcess();
#endif  // V8_ENABLE_WEBASSEMBLY

  ExternalReferenceTable::InitializeOncePerProcess();

  AdvanceStartupState(V8StartupState::kV8Initialized);
}

V8::Initialize主要涉及以下内容

// 内存分配
GetPlatformPageAllocator()->SetRandomMmapSeed(FLAG_random_seed);
GetPlatformVirtualAddressSpace()->SetRandomSeed(FLAG_random_seed);

// 沙盒
GetProcessWideSandbox()->Initialize(GetPlatformVirtualAddressSpace());

// isolate相关
IsolateAllocator::InitializeOncePerProcess();
Isolate::InitializeOncePerProcess();

// 仿真器相关初始化(arm/arm64/risv等)
Simulator::InitializeOncePerProcess();

// 硬件cpu指令集特性支持
CpuFeatures::Probe(false);
  
// elements初始化(fast/slow elements等)
ElementsAccessor::InitializeOncePerProcess();

// v8扩展相关初始化
Bootstrapper::InitializeOncePerProcess();

// 调用约定相关初始化
CallDescriptors::InitializeOncePerProcess();

// wasm相关初始化
wasm::WasmEngine::InitializeOncePerProcess();

// 外部引用表
ExternalReferenceTable::InitializeOncePerProcess();

// 初始化状态数据
AdvanceStartupState(V8StartupState::kV8Initialized);

因为我用的是mac调试,所以调用的是platform-posix.cc中的初始化函数,功能比较简单,设置了一下标志位

void PosixInitializeCommon(bool hard_abort, const char* const gc_fake_mmap) {
  g_hard_abort = hard_abort;
  g_gc_fake_mmap = gc_fake_mmap;
}

#if !V8_OS_FUCHSIA
void OS::Initialize(bool hard_abort, const char* const gc_fake_mmap) {
  PosixInitializeCommon(hard_abort, gc_fake_mmap);
}
#endif 

这里的初始化是平台相关的,直接看代码如果不对的话,就用调试器调试一下即可

image-20220721164249154

继续,这部分主要涉及v8内存分配等相关操作

内存分配

if (FLAG_random_seed) {
    GetPlatformPageAllocator()->SetRandomMmapSeed(FLAG_random_seed);
    GetPlatformVirtualAddressSpace()->SetRandomSeed(FLAG_random_seed);
  }

继续

// 惰性初始化
DEFINE_LAZY_LEAKY_OBJECT_GETTER(PageAllocatorInitializer,
                                GetPageAllocatorInitializer)

// We will attempt allocation this many times. After each failure, we call
// OnCriticalMemoryPressure to try to free some memory.
const int kAllocationTries = 2;

}  // namespace

v8::PageAllocator* GetPlatformPageAllocator() {
  DCHECK_NOT_NULL(GetPageAllocatorInitializer()->page_allocator());
  return GetPageAllocatorInitializer()->page_allocator();
}

v8::VirtualAddressSpace* GetPlatformVirtualAddressSpace() {
#if defined(LEAK_SANITIZER)
  static base::LeakyObject<base::LsanVirtualAddressSpace> vas(
      std::make_unique<base::VirtualAddressSpace>());
#else
  static base::LeakyObject<base::VirtualAddressSpace> vas;
#endif
  return vas.get();
}

有关于惰性初始化的宏DEFINE_LAZY_LEAKY_OBJECT_GETTER

#define DEFINE_LAZY_LEAKY_OBJECT_GETTER(T, FunctionName, ...) \
  T* FunctionName() {                                         \
    static ::v8::base::LeakyObject<T> object{__VA_ARGS__};    \
    return object.get();                                      \
  }

根据宏定义展开GetPageAllocatorInitializer

 PageAllocatorInitializer* GetPageAllocatorInitializer() {
    static ::v8::base::LeakyObject<PageAllocatorInitializer> object{};
    return object.get();
  }

其中PageAllocatorInitializer

class PageAllocatorInitializer {
 public:
  PageAllocatorInitializer() {
    page_allocator_ = V8::GetCurrentPlatform()->GetPageAllocator();
    if (page_allocator_ == nullptr) {
      static base::LeakyObject<base::PageAllocator> default_page_allocator;
      page_allocator_ = default_page_allocator.get();
    }
    ...

page_allocator_来源于V8::GetCurrentPlatform

v8::Platform* V8::GetCurrentPlatform() {
  v8::Platform* platform = reinterpret_cast<v8::Platform*>(
      base::Relaxed_Load(reinterpret_cast<base::AtomicWord*>(&platform_)));
  DCHECK(platform);
  return platform;
}

platform_变量即上面platform初始化相关内容

再来看一下新的内存分配管理器

GetPlatformVirtualAddressSpace()->SetRandomSeed(FLAG_random_seed);

还是按照上面的步骤,找到最后的构造函数

VirtualAddressSpace::VirtualAddressSpace()
    : VirtualAddressSpaceBase(OS::CommitPageSize(), OS::AllocatePageSize(),
                              kNullAddress,
                              std::numeric_limits<uintptr_t>::max(),
                              PagePermissions::kReadWriteExecute) {
#if V8_OS_WIN
  // On Windows, this additional step is required to lookup the VirtualAlloc2
  // and friends functions.
  OS::EnsureWin32MemoryAPILoaded();
#endif  // V8_OS_WIN
  DCHECK(bits::IsPowerOfTwo(page_size()));
  DCHECK(bits::IsPowerOfTwo(allocation_granularity()));
  DCHECK_GE(allocation_granularity(), page_size());
  DCHECK(IsAligned(allocation_granularity(), page_size()));
}

其调用了父类VirtualAddressSpaceBase的构造函数

class VirtualAddressSpaceBase
    : public NON_EXPORTED_BASE(::v8::VirtualAddressSpace) {
 public:
  using VirtualAddressSpace::VirtualAddressSpace;

 private:
  friend VirtualAddressSubspace;
  // Called by a subspace during destruction. Responsible for freeing the
  // address space reservation and any other data associated with the subspace
  // in the parent space.
  virtual void FreeSubspace(VirtualAddressSubspace* subspace) = 0;
};

其继承了v8::VirtualAddressSpace,这里注意区分上面的v8::base::VirtualAddressSpace

继续看一下v8::VirtualAddressSpace,它几乎包含了我们在虚拟化地址过程中的所有数据

class VirtualAddressSpace {
 public:
  using Address = uintptr_t;

  VirtualAddressSpace(size_t page_size, size_t allocation_granularity,
                      Address base, size_t size,
                      PagePermissions max_page_permissions)
      : page_size_(page_size),
        allocation_granularity_(allocation_granularity),
        base_(base),
        size_(size),
        max_page_permissions_(max_page_permissions) {}

  virtual ~VirtualAddressSpace() = default;

  /**
   * The page size used inside this space. Guaranteed to be a power of two.
   * Used as granularity for all page-related operations except for allocation,
   * which use the allocation_granularity(), see below.
   *
   * \returns the page size in bytes.
   */
  size_t page_size() const { return page_size_; }

  /**
   * The granularity of page allocations and, by extension, of subspace
   * allocations. This is guaranteed to be a power of two and a multiple of the
   * page_size(). In practice, this is equal to the page size on most OSes, but
   * on Windows it is usually 64KB, while the page size is 4KB.
   *
   * \returns the allocation granularity in bytes.
   */
  size_t allocation_granularity() const { return allocation_granularity_; }
   ...

这个类内容比较多,但是如果是功能类开发用到的机会不多,如果想要写v8漏洞利用,那建议好好熟悉一下这部分的内容。我在参考文档中,给出了这部分内容的部分官方内存设计文档,有需要可以多看看,这里就先说这么多。

开始下一个分析前,先对上面两个部分的调用关系做一下简单的梳理

v8::V8::Initialize
  |-> i::V8::Initialize
        |-> GetPlatformPageAllocator        // 老的内存管理器
        |       |-> GetPageAllocatorInitializer // 宏引入
        |               |-> PageAllocatorInitializer
        |                  |-> V8::GetCurrentPlatform()->GetPageAllocator()// platform初始化创建
        |
        |-> GetPlatformVirtualAddressSpace  // 新的内存管理器
              |-> base::VirtualAddressSpace
                    |-> VirtualAddressSpaceBase
                          |-> v8::VirtualAddressSpace  // 最后真正的处理类

继续分析V8::Initialize的沙盒模式,这个需要flags来启用它,如果是用fuzz来测试浏览器,可以考虑暂时关闭该flags

#if defined(V8_ENABLE_SANDBOX)
  // If enabled, the sandbox must be initialized first.
  GetProcessWideSandbox()->Initialize(GetPlatformVirtualAddressSpace());
  CHECK_EQ(kSandboxSize, GetProcessWideSandbox()->size());
#endif

因为沙盒这部分对于安全方面来说比较重要,我们简单跟一下里面的初始化逻辑,具体的沙盒操作,初始化还不会涉及

延迟载入Sandbox

#ifdef V8_ENABLE_SANDBOX
DEFINE_LAZY_LEAKY_OBJECT_GETTER(Sandbox, GetProcessWideSandbox)
#endif

进入Sandbox定义

class V8_EXPORT_PRIVATE Sandbox {
 public:
  // 沙盒中的内存分配 
  
  // +-  ~~~  -+----------------------------------------  ~~~  -+-  ~~~  -+
  // |  32 GB  |                 (Ideally) 1 TB                 |  32 GB  |
  // |         |                                                |         |
  // | Guard   |      4 GB      :  ArrayBuffer backing stores,  | Guard   |
  // | Region  |    V8 Heap     :  WASM memory buffers, and     | Region  |
  // | (front) |     Region     :  any other sandboxed objects. | (back)  |
  // +-  ~~~  -+----------------+-----------------------  ~~~  -+-  ~~~  -+
  //           ^                                                ^
  //           base                                             end
  //           < - - - - - - - - - - - size - - - - - - - - - - >
  // < - - - - - - - - - - - - - reservation_size - - - - - - - - - - - - >

  // 使用默认构造函数
  Sandbox() = default;

  Sandbox(const Sandbox&) = delete;
  Sandbox& operator=(Sandbox&) = delete;

  /**
   * Initializes this sandbox.
   *
   * This will allocate the virtual address subspace for the sandbox inside the
   * provided virtual address space. If a subspace of the required size cannot
   * be allocated, this method will insted initialize this sandbox as a
   * partially-reserved sandbox. In that case, a smaller virtual address space
   * reservation will be used and an EmulatedVirtualAddressSubspace instance
   * will be created on top of it to back the sandbox. If not enough virtual
   * address space can be allocated for even a partially-reserved sandbox, then
   * this method will fail with an OOM crash.
   */
  void Initialize(v8::VirtualAddressSpace* vas);
  ....

默认构造函数完成初始化后,调用初始化函数来完成真正的初始化,这部分主要完成地址和一些地址防护设置

bool Sandbox::Initialize(v8::VirtualAddressSpace* vas, size_t size,
                         bool use_guard_regions) {
  CHECK(!initialized_);
  CHECK(base::bits::IsPowerOfTwo(size));
  CHECK(vas->CanAllocateSubspaces());

  size_t reservation_size = size;
  if (use_guard_regions) {
    reservation_size += 2 * kSandboxGuardRegionSize;
  }

  Address hint = RoundDown(vas->RandomPageAddress(), kSandboxAlignment);
  ...
  if (use_guard_regions) {
    Address front = reservation_base_;
    Address back = end_;
    // These must succeed since nothing was allocated in the subspace yet.
    CHECK(address_space_->AllocateGuardRegion(front, kSandboxGuardRegionSize));
    CHECK(address_space_->AllocateGuardRegion(back, kSandboxGuardRegionSize));
  }

  initialized_ = true;

  InitializeConstants();

  DCHECK(!is_partially_reserved());
  ...
}

// 填充数据
void Sandbox::InitializeConstants() {
#ifdef V8_SANDBOXED_POINTERS
  // Place the empty backing store buffer at the end of the sandbox, so that any
  // accidental access to it will most likely hit a guard page.
  constants_.set_empty_backing_store_buffer(base_ + size_ - 1);
#endif
}
  • isolate初始化
IsolateAllocator::InitializeOncePerProcess();
Isolate::InitializeOncePerProcess();

isolate是什么,isolate是一个JavaScript的代码执行环境(JavaScript execution environment)

跟进

void IsolateAllocator::InitializeOncePerProcess() {
#ifdef V8_COMPRESS_POINTERS_IN_SHARED_CAGE
  PtrComprCageReservationParams params;
  base::AddressRegion existing_reservation;
#ifdef V8_ENABLE_SANDBOX
  // The pointer compression cage must be placed at the start of the sandbox.
  auto sandbox = GetProcessWideSandbox();
  CHECK(sandbox->is_initialized());
  Address base = sandbox->address_space()->AllocatePages(
      sandbox->base(), params.reservation_size, params.base_alignment,
      PagePermissions::kNoAccess);
  CHECK_EQ(sandbox->base(), base);
  existing_reservation = base::AddressRegion(base, params.reservation_size);
  params.page_allocator = sandbox->page_allocator();
#endif
  // 通过cage控制分配的空间
  if (!GetProcessWidePtrComprCage()->InitReservation(params,
                                                     existing_reservation)) {
    V8::FatalProcessOutOfMemory(
        nullptr,
        "Failed to reserve virtual memory for process-wide V8 "
        "pointer compression cage");
  }
#endif
}

主要完成了一些地址空间的初始化,为将来的空间分配做准备,比较重要的是这个GetProcessWidePtrComprCage

它也是通过宏定义载入

#ifdef V8_COMPRESS_POINTERS_IN_SHARED_CAGE
namespace {
DEFINE_LAZY_LEAKY_OBJECT_GETTER(VirtualMemoryCage, GetProcessWidePtrComprCage)
}

跟一下VirtualMemoryCage

class VirtualMemoryCage {
 public:
  VirtualMemoryCage();
  virtual ~VirtualMemoryCage();

  VirtualMemoryCage(const VirtualMemoryCage&) = delete;
  VirtualMemoryCage& operator=(VirtualMemoryCage&) = delete;

  VirtualMemoryCage(VirtualMemoryCage&& other) V8_NOEXCEPT;
  VirtualMemoryCage& operator=(VirtualMemoryCage&& other) V8_NOEXCEPT;

  Address base() const { return base_; }
  size_t size() const { return size_; }

  base::BoundedPageAllocator* page_allocator() const {
    return page_allocator_.get();
  ...

可以看一下它的具体空间分配构成

 +------------+-----------+------------ ~~~ --+- ~~~ -+
 |     ...    |    ...    |   ...             |  ...  |
 +------------+-----------+------------ ~~~ --+- ~~~ -+
 ^            ^           ^
 start        cage base   allocatable base

 <------------>           <------------------->
 base bias size              allocatable size
              <------------------------------->
                          cage size
 <---------------------------------------------------->
                   reservation size

可以发现这部分跟sandbox的空间分配基本是一样的

// sandbox空间
// +-  ~~~  -+----------------------------------------  ~~~  -+-  ~~~  -+
// |  32 GB  |                 (Ideally) 1 TB                 |  32 GB  |
// |         |                                                |         |
// | Guard   |      4 GB      :  ArrayBuffer backing stores,  | Guard   |
// | Region  |    V8 Heap     :  WASM memory buffers, and     | Region  |
// | (front) |     Region     :  any other sandboxed objects. | (back)  |
// +-  ~~~  -+----------------+-----------------------  ~~~  -+-  ~~~  -+
//           ^                                                ^
//           base                                             end
//           < - - - - - - - - - - - size - - - - - - - - - - >
// < - - - - - - - - - - - - - reservation_size - - - - - - - - - - - - >

其实sandbox的地址空间就是VirtualMemoryCage的具体化。

v8的内存分配为什么要设计的这么复杂?

主要的原因有两个,一个是节省空间,一个是增强安全性。

  1. 节省空间

    现在普遍使用的机器是64位,跟32位机器相比,cpu能够访问的内存地址扩大了,同样的在32位机器下,一个指针只需要占用4个字节,在64位就需要占用8个字节。所以chrome为了节省指针所用的空间,引入了Pointer Compression,即将64位指针分为两个部分,一个部分存放指针基址,另一部分存放其偏移地址(offset),如果是smi(small integer),则直接存放其值,最后用一个标识位来表示值还是偏移。

                |----- 32 bits -----|----- 32 bits -----|
    Pointer:    |________base_______|______offset_____w1|
    
                |----- 32 bits -----|----- 32 bits -----|
    Smi:        |sssssssssssssssssss|____int31_value___0|
    
  2. 增强安全性

    在正常的v8漏洞利用过程中,是通过内存破坏(oob rw/uaf/type confusion/off by one)去读取或者写入某个地址,以达到执行恶意代码的目的,为了缓解这部分的漏洞利用,chromePointer Compression的基础上,引入了Cage Pointer的概念,将所有需要分配地址(compressed pointer)集中在某个区域,比如下图的1 TB空间,基址只能是该空间的某一处,并且在前后端位置各预留32 GB的保护空间,这样在cage内的地址,无论如何调整offset,也不可能访问到其他地址。

     +-  ~~~  -+----------------------------------------  ~~~  -+-  ~~~  -+
     |  32 GB  |                 (Ideally) 1 TB                 |  32 GB  |
     |         |                                                |         |
     | Guard   |      4 GB      :  ArrayBuffer backing stores,  | Guard   |
     | Region  |    V8 Heap     :  WASM memory buffers, and     | Region  |
     | (front) |     Region     :  any other sandboxed objects. | (back)  |
     +-  ~~~  -+----------------+-----------------------  ~~~  -+-  ~~~  -+
    

回到InitializeOncePerProcess

void Isolate::InitializeOncePerProcess() {
  // 利用pthread_key_create创建线程独立的local key
  isolate_key_ = base::Thread::CreateThreadLocalKey();
  bool expected = false;
  CHECK(isolate_key_created_.compare_exchange_strong(
      expected, true, std::memory_order_relaxed));
  per_isolate_thread_data_key_ = base::Thread::CreateThreadLocalKey();

  Heap::InitializeOncePerProcess();
}

跟进Heap::InitializeOncePerProcess

void Heap::InitializeOncePerProcess() {
#ifdef V8_ENABLE_ALLOCATION_TIMEOUT
  HeapAllocator::InitializeOncePerProcess();
#endif
  MemoryAllocator::InitializeOncePerProcess();
}

跟进HeapAllocator::InitializeOncePerProcess

// static
void HeapAllocator::InitializeOncePerProcess() {
  SetAllocationGcInterval(FLAG_gc_interval);
}

// static
void HeapAllocator::SetAllocationGcInterval(int allocation_gc_interval) {
  allocation_gc_interval_.store(allocation_gc_interval,
                                std::memory_order_relaxed);
}

// static
std::atomic<int> HeapAllocator::allocation_gc_interval_{-1};

可以发现该函数的作用,根据启动参数设置gc_interval,默认值为-1

--gc_interval (garbage collect after <n> allocations)
   type: int  default: -1

跟进MemoryAllocator::InitializeOncePerProcess

void MemoryAllocator::InitializeOncePerProcess() {
  commit_page_size_ =
      FLAG_v8_os_page_size > 0 ? FLAG_v8_os_page_size * KB : CommitPageSize();
  CHECK(base::bits::IsPowerOfTwo(commit_page_size_));
  commit_page_size_bits_ = base::bits::WhichPowerOfTwo(commit_page_size_);
}

该函数的作用是设置两个变量的值

  1. commit_page_size_: 在platform初始化已经介绍过,如果未定义则根据平台设置,macOS目前是4096

  2. commit_page_size_bits_: 根据commit_page_size_计算2的指数值,4096对应的应该是12

    image-20220829162954201

调用逻辑

Isolate::InitializeOncePerProcess
   |-> base::Thread::CreateThreadLocalKey   // 创建isolate key
   |
   |-> Heap::InitializeOncePerProcess      // 堆初始化设置
         |-> HeapAllocator::InitializeOncePerProcess
         |     |-> SetAllocationGcInterval(FLAG_gc_interval)
         |            |-> allocation_gc_interval_.store
         |
         |-> MemoryAllocator::InitializeOncePerProcess
         |      |-> commit_page_size_/commit_page_size_bits_

回到V8::Initialize,继续

#if defined(USE_SIMULATOR)
  Simulator::InitializeOncePerProcess();
#endif

// static
void SimulatorBase::InitializeOncePerProcess() {
  DCHECK_NULL(redirection_mutex_);
  redirection_mutex_ = new base::Mutex();

  DCHECK_NULL(i_cache_mutex_);
  i_cache_mutex_ = new base::Mutex();

  DCHECK_NULL(i_cache_);
  i_cache_ = new base::CustomMatcherHashMap(&Simulator::ICacheMatch);
}

这部分代码主要是关于v8各个指令集的模拟器的准备,可以在x64平台调试arm/arm64/riscv64/mips/mips64等平台代码,不需要底层硬件的支持,因为不涉及真正的代码解释,所以在初始化过程中比较简单。

继续分析CpuFeatures::Probe(false)

static void Probe(bool cross_compile) {
    static_assert(NUMBER_OF_CPU_FEATURES <= kBitsPerInt);
    if (initialized_) return;
    initialized_ = true;
    ProbeImpl(cross_compile);
  }
  
// x64平台指令集判断
void CpuFeatures::ProbeImpl(bool cross_compile) {
  // Only use statically determined features for cross compile (snapshot).
  if (cross_compile) return;

#if V8_HOST_ARCH_IA32 || V8_HOST_ARCH_X64
  base::CPU cpu;
  CHECK(cpu.has_sse2());  // SSE2 support is mandatory.
  CHECK(cpu.has_cmov());  // CMOV support is mandatory.

  if (cpu.has_sse42()) SetSupported(SSE4_2);
  if (cpu.has_sse41()) SetSupported(SSE4_1);
  if (cpu.has_ssse3()) SetSupported(SSSE3);
  if (cpu.has_sse3()) SetSupported(SSE3);
  if (cpu.has_avx() && cpu.has_osxsave() && OSHasAVXSupport()) {
    SetSupported(AVX);
    if (cpu.has_avx2()) SetSupported(AVX2);
    if (cpu.has_fma3()) SetSupported(FMA3);
    ...

主要包含对cpu支持的指令集判断,会cpu支持的指令集不同,生成对应的指令集代码。

继续分析Bootstrapper::InitializeOncePerProcess()

void Bootstrapper::InitializeOncePerProcess() {
  v8::RegisterExtension(std::make_unique<GCExtension>(GCFunctionName()));
  v8::RegisterExtension(std::make_unique<ExternalizeStringExtension>());
  v8::RegisterExtension(std::make_unique<StatisticsExtension>());
  v8::RegisterExtension(std::make_unique<TriggerFailureExtension>());
  v8::RegisterExtension(std::make_unique<IgnitionStatisticsExtension>());
  if (isValidCpuTraceMarkFunctionName()) {
    v8::RegisterExtension(
        std::make_unique<CpuTraceMarkExtension>(FLAG_expose_cputracemark_as));
  }
#ifdef ENABLE_VTUNE_TRACEMARK
  v8::RegisterExtension(
      std::make_unique<VTuneDomainSupportExtension>("vtunedomainmark"));
#endif  // ENABLE_VTUNE_TRACEMARK
}

这部分主要是实现了v8扩展的初始化加载,主要包括

GCExtensionz
ExternalizeStringExtension
StatisticsExtension
TriggerFailureExtension
IgnitionStatisticsExtension
CpuTraceMarkExtension
VTuneDomainSupportExtension

跟一下v8::RegisterExtension的实现

void RegisterExtension(std::unique_ptr<Extension> extension) {
  RegisteredExtension::Register(std::move(extension));
}

void RegisteredExtension::Register(std::unique_ptr<Extension> extension) {
  RegisteredExtension* new_extension =
      new RegisteredExtension(std::move(extension));
  new_extension->next_ = first_extension_;
  first_extension_ = new_extension;
}

RegisteredExtension::RegisteredExtension(std::unique_ptr<Extension> extension)
    : extension_(std::move(extension)) {}

通过RegisteredExtension实现了一个扩展链表

class RegisteredExtension {
 public:
  static void Register(std::unique_ptr<Extension>);
  static void UnregisterAll();
  Extension* extension() const { return extension_.get(); }
  RegisteredExtension* next() const { return next_; }
  static RegisteredExtension* first_extension() { return first_extension_; }

 private:
  explicit RegisteredExtension(Extension*);
  explicit RegisteredExtension(std::unique_ptr<Extension>);
  std::unique_ptr<Extension> extension_;
  RegisteredExtension* next_ = nullptr;
  static RegisteredExtension* first_extension_;
};

其中一个extension初始化过程ExternalizeStringExtension

class ExternalizeStringExtension : public v8::Extension {
 public:
  ExternalizeStringExtension() : v8::Extension("v8/externalize", kSource) {}
  v8::Local<v8::FunctionTemplate> GetNativeFunctionTemplate(
      v8::Isolate* isolate, v8::Local<v8::String> name) override;
  static void Externalize(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void IsOneByte(const v8::FunctionCallbackInfo<v8::Value>& args);

 private:
  static const char* const kSource;
};

Extension::Extension(const char* name, const char* source, int dep_count,
                     const char** deps, int source_length)
    : name_(name),
      source_length_(source_length >= 0
                         ? source_length
                         : (source ? static_cast<int>(strlen(source)) : 0)),
      dep_count_(dep_count),
      deps_(deps),
      auto_enable_(false) {
  source_ = new ExtensionResource(source, source_length_);
  CHECK(source != nullptr || source_length_ == 0);
}

class ExtensionResource : public String::ExternalOneByteStringResource {
 public:
  ExtensionResource() : data_(nullptr), length_(0) {}
  ExtensionResource(const char* data, size_t length)
      : data_(data), length_(length) {}
  const char* data() const override { return data_; }
  size_t length() const override { return length_; }
  void Dispose() override {}

 private:
  const char* data_;
  size_t length_;
};

可以发现,过程相对比较简单,是一些相关数据的初始化

继续CallDescriptors::InitializeOncePerProcess

void CallDescriptors::InitializeOncePerProcess() {
#define INTERFACE_DESCRIPTOR(name, ...) \
  name##Descriptor().Initialize(&call_descriptor_data_[CallDescriptors::name]);
  INTERFACE_DESCRIPTOR_LIST(INTERFACE_DESCRIPTOR)
#undef INTERFACE_DESCRIPTOR

  DCHECK(ContextOnlyDescriptor{}.HasContextParameter());
  DCHECK(!NoContextDescriptor{}.HasContextParameter());
  DCHECK(!AllocateDescriptor{}.HasContextParameter());
  DCHECK(!AbortDescriptor{}.HasContextParameter());
  DCHECK(!WasmFloat32ToNumberDescriptor{}.HasContextParameter());
  DCHECK(!WasmFloat64ToNumberDescriptor{}.HasContextParameter());
}

其中最主要的是INTERFACE_DESCRIPTOR_LIST宏,宏展开后大概是这样的

#define INTERFACE_DESCRIPTOR_LIST(V)                 \
  V(Abort)                                           \
  V(Allocate)                                        \
  V(ApiCallback)                                     \
  V(ApiGetter)                                       \
  V(ArrayConstructor)                                \
  V(ArrayNArgumentsConstructor)                      \
  V(ArrayNoArgumentConstructor)       
  ...
  // 展开
  AbortDescriptor().Initialize(&call_descriptor_data_[CallDescriptors::Abort]);
  AllocateDescriptor().Initialize(&call_descriptor_data_[CallDescriptors::Allocate]);
  ...

该部分最主要的功能是设置了一些函数调用规定,比如参数个数,寄存器入站顺序等,实现类在CallInterfaceDescriptor

class V8_EXPORT_PRIVATE CallInterfaceDescriptor {
 public:
  using Flags = CallInterfaceDescriptorData::Flags;

  CallInterfaceDescriptor() : data_(nullptr) {}
  ~CallInterfaceDescriptor() = default;

  explicit CallInterfaceDescriptor(CallDescriptors::Key key)
      : data_(CallDescriptors::call_descriptor_data(key)) {}

  Flags flags() const { return data()->flags(); }

  bool HasContextParameter() const {
    return (flags() & CallInterfaceDescriptorData::kNoContext) == 0;
  }

  bool AllowVarArgs() const {
    return flags() & CallInterfaceDescriptorData::kAllowVarArgs;
  }

  bool CalleeSaveRegisters() const {
    return flags() & CallInterfaceDescriptorData::kCalleeSaveRegisters;
  }

  int GetReturnCount() const { return data()->return_count(); }

  MachineType GetReturnType(int index) const {
    DCHECK_LT(index, data()->return_count());
    return data()->return_type(index);
  }
  ...

继续WasmEngine webassembly初始化

void WasmEngine::InitializeOncePerProcess() {
  // PKU的支持
  base::MemoryProtectionKey::InitializeMemoryProtectionKeySupport();
  DCHECK_NULL(global_wasm_state);
  // 创建WasmCodeManager和WasmEngine
  global_wasm_state = new GlobalWasmState();
}

继续base::MemoryProtectionKey::InitializeMemoryProtectionKeySupport

void MemoryProtectionKey::InitializeMemoryProtectionKeySupport() {
  // Flip {pkey_initialized} (in debug mode) and check the new value.
  DCHECK_EQ(true, pkey_initialized = !pkey_initialized);
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
  // PKU was broken on Linux kernels before 5.13 (see
  // https://lore.kernel.org/all/[email protected]/).
  // A fix is also included in the 5.4.182 and 5.10.103 versions ("x86/fpu:
  // Correct pkru/xstate inconsistency" by Brian Geffon <[email protected]>).
  // Thus check the kernel version we are running on, and bail out if does not
  // contain the fix.
  struct utsname uname_buffer;
  CHECK_EQ(0, uname(&uname_buffer));
  int kernel, major, minor;
  // Conservatively return if the release does not match the format we expect.
  if (sscanf(uname_buffer.release, "%d.%d.%d", &kernel, &major, &minor) != 3) {
    return;
  }
  bool kernel_has_pkru_fix =
      kernel > 5 || (kernel == 5 && major >= 13) ||   // anything >= 5.13
      (kernel == 5 && major == 4 && minor >= 182) ||  // 5.4 >= 5.4.182
      (kernel == 5 && major == 10 && minor >= 103);   // 5.10 >= 5.10.103
  if (!kernel_has_pkru_fix) return;

  // Try to find the pkey functions in glibc.
  void* pkey_alloc_ptr = dlsym(RTLD_DEFAULT, "pkey_alloc");
  if (!pkey_alloc_ptr) return;

  // If {pkey_alloc} is available, the others must also be available.
  void* pkey_free_ptr = dlsym(RTLD_DEFAULT, "pkey_free");
  void* pkey_mprotect_ptr = dlsym(RTLD_DEFAULT, "pkey_mprotect");
  void* pkey_get_ptr = dlsym(RTLD_DEFAULT, "pkey_get");
  void* pkey_set_ptr = dlsym(RTLD_DEFAULT, "pkey_set");
  CHECK(pkey_free_ptr && pkey_mprotect_ptr && pkey_get_ptr && pkey_set_ptr);

  pkey_alloc = reinterpret_cast<pkey_alloc_t>(pkey_alloc_ptr);
  pkey_free = reinterpret_cast<pkey_free_t>(pkey_free_ptr);
  pkey_mprotect = reinterpret_cast<pkey_mprotect_t>(pkey_mprotect_ptr);
  pkey_get = reinterpret_cast<pkey_get_t>(pkey_get_ptr);
  pkey_set = reinterpret_cast<pkey_set_t>(pkey_set_ptr);
#endif
}

主要是pku支持的判断,并在支持的情况下,设置相关分配、释放等函数。

继续ExternalReferenceTable::InitializeOncePerProcess

void ExternalReferenceTable::InitializeOncePerProcess() {
  int index = 0;

  // kNullAddress is preserved through serialization/deserialization.
  AddIsolateIndependent(kNullAddress, &index);
  AddIsolateIndependentReferences(&index);
  AddBuiltins(&index);
  AddRuntimeFunctions(&index);
  AddAccessors(&index);

  CHECK_EQ(kSizeIsolateIndependent, index);
}

v8相关漏洞

v8正则表达式漏洞:

https://bugs.chromium.org/p/chromium/issues/detail?id=1307610

https://www.anquanke.com/post/id/276964

参考

  1. V8环境搭建(anquanke.灰豆)
  2. learning-v8
  3. Custom startup snapshots
  4. Elements kinds in V8
  5. fast properties in V8
  6. V8 hidden class and inline cache
  7. nested classes
  8. learning v8 about heap
  9. V8 Sandbox - Address Space
  10. V8 Sandbox - Sandboxed Pointers
  11. V8 Sandbox
  12. V8 Virtual Memory Cage
  13. Pointer Compression in V8
  14. 简单理解 V8 Turbofan
  15. Visualizing memory management in V8 Engine
  16. v8 debug arm
  17. PKU Pitfalls: Attacks on PKU-based Memory Isolation Systems
@xinali xinali changed the title v8学习 v8初始化分析 Apr 21, 2023
@xinali xinali changed the title v8初始化分析 001-v8初始化分析 Apr 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant