quickjs: 调试

2,618 阅读3分钟

QuickJS是一个小型并且可嵌入的Javascript引擎,它支持ES2020规范,包括模块,异步生成器和代理器。

quickjs 是由 Fabrice Bellard 发布的一款小而美的js引擎,是一款学习javascript实现原理性价比较高的引擎,本系列介绍js规范在quickjs中是如何实现的,包括变量,对象,函数,原型,闭包,作用域,垃圾回收等内容。

为什么要自己学习quickjs引擎?学习的过程其实是一个自我否定和肯定的过程;在初学时往往道听途说,而不知其所以然;当要进入授业阶段时,需要本着严谨求实,不误人子弟的态度,必须要从根源上明白WWW;从javascript引擎角度来说,quickjs 只是一个起点,然后慢慢过渡到v8这个庞然大物上。

绕不开的GDB

无论是学习quickjs还是v8,都必须学会基本的搞gdb命令,所以gdb是入门的第一课(no vscode, no xcode)。

  1. 编译quickjs

quickjs 是纯c编写的,编译起来相对简单,我们只需要关闭gcc的优化选项,搜索 Makefile 中的 CFLAGS_OPT, 将 O2 修改为O0,然后在根目录 make 即可

$ make
  1. 进入 gdb

make 编译完毕后会产生一个 qjs 文件,准备测试的javascript代码,执行

gdb --args ./qjs path-of-javascript
  1. 几个gdb指令

常用的gdb命令: b-断点,r-启动,c-继续,s-进入;p-打印,x-打印

断点可以通过函数名设置,也可以通过[文件路径:行数]来设置

b main
b quickjs.c:1000

打印通常包括打印基本数据类型,指针,结构体,枚举等

set print pretty on # 格式化打印
p v
p *v
p /d V

单纯从学习引擎的实现原理考虑,感觉用这几个基本指令已经够用了

据个例子

学过编译原理的应该对过程都比较清楚,可惜我没有学过(还是忘了,一丢丢印象都没有)。先抛开所有的概念,动手调试下,看看过程。

  1. 测试文件 example/variable.js
const a = 1;
const b = 2;
const c = a + b;
  1. 进入 gdb 调试
$ gdb --args ./qjs example/variable.js
$ b main
$ b JS_CallInternal # 字节码执行函数
$ b quickjs.c:16627 # 解析字节码
$ r                 # 执行
$ n
...
$ s
...
$ c
  1. 获取字节码buf

捯饬捯饬,....,此时断点停留在 quickjs:16627, 打印字节码(开始被这一坨给吓倒了)

# p b->byte_code_len #字节码长度
# (gdb) x/66db b->byte_code_buf # 以10进制打印每个字节大的内容
0x55acbb8a08c0: 63      60      2       0       0       -128    63      61
0x55acbb8a08c8: 2       0       0       -128    63      62      2       0
0x55acbb8a08d0: 0       -128    62      60      2       0       0       -128
0x55acbb8a08d8: 62      61      2       0       0       -128    62      62
0x55acbb8a08e0: 2       0       0       -128    -74     58      60      2
0x55acbb8a08e8: 0       0       -73     58      61      2       0       0
0x55acbb8a08f0: 56      60      2       0       0       56      61      2
0x55acbb8a08f8: 0       0       -99     58      62      2       0       0
0x55acbb8a0900: -59     40
  1. 字节码逻辑

按照字节码的执行逻辑格式化,第一列时操作符,后面是数据

 63      60      2       0       0       -128 (OP_check_define_var)            // 查看变量是否有定义, 依次对应 a, b,c
 63      61      2       0       0       -128 (OP_check_define_var)   
 63      62      2       0       0       -128 (OP_check_define_var)  
 62      60      2       0       0       -128 (OP_define_var)                  // 定义变量,一次对应 a, b, c
 62      61      2       0       0       -128 (OP_define_var)   
 62      62      2       0       0       -128 (OP_define_var)  
-74                                           (OP_push_0 - OP_push_7)          // 入栈
 58      60      2       0       0            (OP_put_var_init / OP_put_var)   // 出栈,设置到变量中
-73                                           (OP_push_0 - OP_push_7)          // 入栈
 58      61      2       0       0            (OP_put_var_init / OP_put_var)   // 出栈,设置到变量中
 56      60      2       0       0            (OP_get_var)                     // 获取变量, 入栈
 56      61      2       0       0            (OP_get_var)                     // 获取变量, 入栈
-99                                           (OP_add)                         // 栈运算,结果入栈
 58      62      2       0       0            (OP_put_var_init / OP_put_var)   // 出栈,设置到变量中
-59                                           (OP_get_loc0)                    
 40                                           (OP_return)                       // 返回值

能够大值猜测到:

60 2 0 0 代表 a

61 2 0 0 代表 b

62 2 0 0 代表 c

  1. 栈式虚拟机

quickjs是基于堆栈式虚拟机实现的,可以通过栈数据变化观察下实现逻辑

栈动作栈数据栈数据
1 入栈1
1 出栈
2 入栈2
2 出栈
1 入栈1
2 入栈12
取栈数据,加法运算,结果入栈3
3 出栈
返回值入栈0
返回值出栈0

好麻烦哦,进进出出,辛苦了,这就是栈式虚拟机的缺点

总结

算是 quickjs 调试入门了,可以看到我们js代码被翻译成自定义的字节码,然后在自定义的虚拟机中执行。那后面就是看下字节码生成咯。

关注我的微信公众号"SUNTOPO WLOG",欢迎留言讨论,我会尽可能回复,感谢您的阅读。