V8源码边缘试探-黑魔法指针偏移
这博客是越来越难写了,参考资料少,难度又高,看到什么写什么吧!
众多周知,在JavaScript中有几个基本类型,包括字符串、数字、布尔、null、undefined、Symbol,其中大部分都可以在我之前那篇博客(https://www.cnblogs.com/QH-Jimmy/p/9212923.html)中找到,均继承于Primitive类。但是仔细看会发现少了两个,null和undefined呢?这一节,就来探索一下,V8引擎是如何处理null、undefined两种类型的。
在没有看源码之前,我以为是这样的:
class Null : public Primitive {
public:
// Type testing.
bool IsNull() const { return true; }
// ...
}
然而实际上没有这么简单粗暴,V8对null、undefined(实际上还包括了true、false、空字符串)都做了特殊的处理。
回到故事的起点,是我在研究LoadEnvironment函数的时候发现的。上一篇博客其实就是在讲这个方法,包装完函数名、函数体,最后一步就是配合函数参数来执行函数了,代码如下:
// Bootstrap internal loaders
Local<Value> bootstrapped_loaders;
if (!ExecuteBootstrapper(env, loaders_bootstrapper,
arraysize(loaders_bootstrapper_args),
loaders_bootstrapper_args,
&bootstrapped_loaders)) {
return;
}
这里的参数分别为:
1、env => 当前V8引擎的环境变量,包含Isolate、context等。
2、loaders_bootstrapper => 函数体
3、arraysize(loaders_bootstrapper_args) => 参数长度,就是4
4、loaders_bootstrapper_args => 参数数组,包括process对象及3个C++内部方法
5、&bootstrapped_loaders => 一个局部变量指针
参数是啥并不重要,进入方法,源码如下:
static bool ExecuteBootstrapper(Environment* env, Local<Function> bootstrapper,
int argc, Local<Value> argv[],
Local<Value>* out) {
bool ret = bootstrapper->Call(
env->context(), Null(env->isolate()), argc, argv).ToLocal(out);
if (!ret) {
env->async_hooks()->clear_async_id_stack();
} return ret;
}
看起来就像JS里面的call方法,其中函数参数包括context、null、形参数量、形参,当时看到Null觉得比较好奇,就仔细的看了一下实现。
这个方法其实很简单,但是实现的方式非常有意思,源码如下:
Local<Primitive> Null(Isolate* isolate) {
typedef internal::Object* S;
typedef internal::Internals I;
// 检测当前V8引擎实例是否存活
I::CheckInitialized(isolate);
// 核心方法
S* slot = I::GetRoot(isolate, I::kNullValueRootIndex);
// 类型强转 直接是Primitive类而不是继承
return Local<Primitive>(reinterpret_cast<Primitive*>(slot));
}
只有GetRoot是真正生成null值的地方,注意第二个参数 I::kNullValueRootIndex ,这是一个静态整形值,除去null还有其他几个,所有的类似值定义如下:
static const int kUndefinedValueRootIndex = ;
static const int kTheHoleValueRootIndex = ;
static const int kNullValueRootIndex = ;
static const int kTrueValueRootIndex = ;
static const int kFalseValueRootIndex = ;
static const int kEmptyStringRootIndex = ;
上面的数字就是区分这几个类型的关键所在,继续进入GetRoot方法:
V8_INLINE static internal::Object** GetRoot(v8::Isolate* isolate,int index) {
// 获取当前isolate地址并进行必要的空间指针偏移
// static const int kIsolateRootsOffset = kExternalMemoryLimitOffset + kApiInt64Size + kApiInt64Size + kApiPointerSize + kApiPointerSize;
uint8_t* addr = reinterpret_cast<uint8_t*>(isolate) + kIsolateRootsOffset;
// 根据上面的数字以及当前操作系统指针大小进行偏移
// const int kApiPointerSize = sizeof(void*); // NOLINT
return reinterpret_cast<internal::Object**>(addr + index * kApiPointerSize);
}
这个方法就对应了标题,指针偏移。
实际上根本不存在一个正规的null类来生成一个对应的对象,而只是把一个特定的地址当成一个null值。
敢于用这个方法,是因为对于每一个V8引擎来说isolate对象是独一无二的,所以在当前引擎下,获取到的isolate地址也是唯一的。
如果还不明白,我这个灵魂画手会让你明白,超级简单:

最后返回一个地址,这个地址就是null,强转成Local<Primitive>也只是为了垃圾回收与类型区分,实际上并不关心这个指针指向什么,因为null本身不存在任何方法可以调用,大多数情况下也只是用来做变量重置。
就这样,只用了很小的空间便生成了一个null值,并且每一次获取都会返回同一个值。
验证的话就很简单了,随意的在node启动代码里加一段:
auto test = Null(env->isolate());
然后看局部变量的调试框,当前isolate的地址如下:

第一次指针偏移后,addr的地址为:

通过简单计算,这个差值是72(16进制的48),跟第一次偏移量大小一致,这里根本不关心指针指向什么东西,所以字符无效也没事。
第二次偏移后,得到的null地址为:

通过计算得到差值为48(16进制的30),算一算,刚好是6*8。
最后对这个地址进行强转,返回一个Local<Primitive>类型的null对象。
------------------------------------------------------------------------------------------------分割线-------------------------------------------------------------------------------------------
虽然解释的差不多了,但是还是有必要做一个补充,就是关于一个类是否为null值的判断。
正推过去很简单,指定地址的值就是null,如果反推的话,那么想想也很简单,判断当前类的地址是否与指定地址相等。但是在源码里,这个过程可以说是相当的恶心了……
这里简单的过一遍,首先是测试代码:
auto t = Null(env->isolate());
t->IsNull();
每一个生成的null虽然是个废物,但是爸爸很厉害,父类Value有一个方法IsNull专门检测当前类是否是null值。
这个方法非常简单:
bool Value::IsNull() const {
#ifdef V8_ENABLE_CHECKS
return FullIsNull();
#else
return QuickIsNull();
#endif
}
根据情况有两种检测,一种快速的,一种完全体的。默认都是走完全检测分支,里面会同时调用快速检测。
方法源码如下:
bool Value::FullIsNull() const {
// 通过这个可以获取到当前的isolate实例
i::Handle<i::Object> object = Utils::OpenHandle(this);
bool result = false;
// 判断object是否为空值
if (!object->IsSmi()) {
// 内部方法
result = object->IsNull(i::HeapObject::cast(*object)->GetIsolate());
}
// 调用快速检测与返回结果进行比对
DCHECK_EQ(result, QuickIsNull());
return result;
}
那个内部方法,就是完全体的核心,看似简单,实则跟厕所里的石头一样,又臭又硬。因为从这里开始,就要进入宏的地狱了。
因为调试模式对于宏的跳转十分不友好,所以只能一个一个的把宏复制到本地,然后进行拼接,看看最后出来的是什么。这里仅仅给出一系列的截图,看看什么是宏的地狱:


这两步,还原了object->IsNull(Isolate* isolate)究竟是个什么东西,整理后如下:
bool Object::IsNull(Isolate* isolate) const {
return this == isolate->heap()->null_value();
}
看起来很简单,这里的null_value又是一个坑,如下:



这三个宏定义了isolate->heap()->null_value()是个什么东西,整理后如下:
Oddball* Heap::null_value() { return Oddball::cast(roots_[kNullValueRootIndex]); }
你以为这就完了???nonono,这个Oddball::cast(Object* object)又要搞事,如下:


转换成人话如下:
Oddball* Oddball::cast(Object* object) {
SLOW_DCHECK(object->IsOddball());
return reinterpret_cast<type*>(object);
}
发现没,SLOW_DCHECK,大写+下划线分割,又是一个宏,真的是无穷无尽,不过这个宏只是检测表达式是否为真。
这个Oddball是继承于HeapObject,而HeapObject继承于Object,这里只是简单判断当前类是否来源于Object,在上面生成null值的最后转换有这么一行代码:
return reinterpret_cast<internal::Object**>(addr + index * kApiPointerSize);
因此如果是null值,其内置类型必然为Object。
抛去一切上面无关的因素,最终判断条件其实就是那一行代码:Heap::null_value() { Oddball::cast(roots_[kNullValueRootIndex]); }
而这个roots_定义也是很魔性,来源于heap.h,简单的一行:
Object* roots_[kRootListLength];
这个length长达511,定义非常非常多的特殊值,初始化方式也是宏,这里仅仅调出null的定义:

简单讲,roots_在V8引擎初始化时已经预存了所有特殊值的地址,这里直接取this的地址与root_中保存的null值地址进行比较,最后得出结果。
因为宏调试很不直观,也很不方便,这里就不贴图了。
V8源码边缘试探-黑魔法指针偏移的更多相关文章
- 观V8源码中的array.js,解析 Array.prototype.slice为什么能将类数组对象转为真正的数组?
在官方的解释中,如[mdn] The slice() method returns a shallow copy of a portion of an array into a new array o ...
- 科普 | 编译 V8 源码
2017-02-13 justjavac 象尘说 对于JavaScript程序员来说,可以瞧一瞧justjavac给大家写的科普类读物,V8引擎的分析,“也许你不懂C++”,但是你可以了解一下,总是好 ...
- linux源码阅读笔记 void 指针
void 指针的步长为1,而其他类型的指针的步长与其所定义的数据结构有关. example: 1 #include<stdio.h> 2 main() 3 { 4 int a[10]; 5 ...
- v8 源码获取与build
最近准备在工作之余研究下v8,下班时间鼓捣了2天,现在终于能下载,能gclient sync了. 刚开始的目的就是跑一个hello world,按照wiki上的例子来: https://github. ...
- 以V8中js源码为例了解GitHub查看代码功能
GitHub作为开源仓库,许多开源项目仓库这里,当然不乏十分优秀的,比如Node.V8,我一直比较好奇js源码,像java的话,因为环境是JDK,我们结合IDE很容易就能跳转到其源码内部去查看实现,但 ...
- 解读 v8 排序源码
前言 v8 是 Chrome 的 JavaScript 引擎,其中关于数组的排序完全采用了 JavaScript 实现. 排序采用的算法跟数组的长度有关,当数组长度小于等于 10 时,采用插入排序,大 ...
- Qt源码解析之-从PIMPL机制到d指针
一.PIMPL机制 PIMPL ,即Private Implementation,作用是,实现 私有化,力图使得头文件对改变不透明,以达到解耦的目的 pimpl 用法背后的思想是把客户与所有关于类的私 ...
- V8中的快慢数组(附源码、图文更易理解😃)
接上一篇掘金 V8 中的快慢属性,本篇分析V8 中的快慢数组,了解数组全填充还是带孔.快慢数组.快慢转化.动态扩缩容等等.其实很多语言底层都采用类似的处理方式,比如:Golang中切片的append操 ...
- 深入出不来nodejs源码-V8引擎初探
原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...
随机推荐
- RxSwift学习笔记3:生命周期/订阅
有了 Observable,我们还要使用 subscribe() 方法来订阅它,接收它发出的 Event. let observal = Observable.of("a",&qu ...
- Web应用安全之Response Header里的敏感信息
Web应用安全之Response Header 文/玄魂 目录 Web应用安全之Response Header 前言 1.1 那些敏感的header 1.2 删除敏感的header 1.2.1 删除 ...
- .netcore-FreeSql的使用-搭建context
之前用netcore搭建了一个小项目,数据库操作用的是要手写sql语句的connection和command,一直想调个EFCore或者类似SOA那样的框架 今天看到了DotNet公众号提到的.NET ...
- 剑指offer编程题Java实现——面试题3二维数组中的查找
题目描述 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数. 下面是我实现的代码 ...
- 「雅礼集训 2017 Day2」解题报告
「雅礼集训 2017 Day2」水箱 我怎么知道这种题目都能构造树形结构. 根据高度构造一棵树,在树上倍增找到最大的小于约束条件高度的隔板,开一个 \(vector\) 记录一下,然后对于每个 \(v ...
- 动态分析小示例| 08CMS SQL 注入分析
i春秋作家:yanzm 0×00 背景 本周,拿到一个源码素材是08cms的,这个源码在官网中没有开源下载,需要进行购买,由某师傅提供的,审计的时候发现这个CMS数据传递比较复杂,使用静态分析的方式不 ...
- postgresql和redis
redis 和postgresql区别以及其优缺点 一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一夜有三十须臾. 那么,经过周密的计算,一瞬间为0.36 秒, ...
- 继承extends、super、this、方法重写overiding、final、代码块_DAY08
1:Math类的随机数(掌握) 类名调用静态方法. 包:java.lang 类:Math 方法:public static double random(): Java.lang包下的类是不用导包就可 ...
- 监督学习——AdaBoost元算法提高分类性能
基于数据的多重抽样的分类器 可以将不通的分类器组合起来,这种组合结果被称为集成方法(ensemble method)或者元算法(meta-algorithom) bagging : 基于数据随机抽样的 ...
- 源码分析篇 - Android绘制流程(二)measure、layout、draw流程
performTraversals方法会经过measure.layout和draw三个流程才能将一帧View需要显示的内容绘制到屏幕上,用最简化的方式看ViewRootImpl.performTrav ...