操作V8抽象语法树

8

我打算直接在v8代码中实现js代码覆盖率。我的初始目标是为抽象语法树中的每个语句添加一个简单的打印功能。我看到有一个AstVisitor类,它允许您遍历AST。因此,我的问题是如何在visitor当前访问的语句之后向AST添加语句?


基本块是控制流图的构造,而不是AST。您打算从AST创建CFG吗? - user395760
哪些节点?无论如何,我不知道有哪些常见的AST节点与基本块匹配(尽管可能有一种数据结构也维护CFG-ish信息并将其称为“AST”)。例如,循环通常是AST节点,但许多循环由几个BB组成。循环节点可能包含语句节点列表,但其中一些语句对应于BB的部分(例如简单赋值),而其他语句则扩展为多个BB(例如任何内联条件或嵌套循环)。也许您误用了“基本块”这个术语? - user395760
你说得对,我混淆了这两个问题,AST 不能帮助我添加命令。V8 在解析时使用 CFG 吗? - user2240085
我不知道,事实上我从来没有深入研究过V8的内部。 - user395760
1
您可以通过实现自己的ASTVisitor来检查/添加语句和表达式。这需要修改V8。我总结了您可以在这里拦截它的位置:https://dev59.com/G2ox5IYBdhLWcg3wFAY1 我还成功修改了AST,例如插入和替换任意语句和表达式。但是,这不兼容Crankshaft,并且当我更改驱动控制流的节点时会导致问题,我无法在有限的时间内解决。我猜测如何解决这个问题,但不确定。 - Jonas
显示剩余2条评论
1个回答

7

好的,我将总结我的实验。首先,我所写的内容适用于Chromium版本r157275中使用的V8,因此可能不再适用-但我仍将链接到当前版本的位置。

如前所述,您需要自己的AST访问者,比如MyAstVisior,它继承自AstVisitor,并必须从那里实现一堆VisitXYZ方法。检查执行代码所需的唯一方法是VisitFunctionLiteral。执行的代码可以是函数,也可以是源代码中的一组松散语句,V8会将其包装在一个函数中,然后执行。

然后,在解析的AST转换为代码之前,这里(将松散语句制作成函数的编译)和那里(运行时编译,当首次执行预定义函数时),您将访问者传递给函数文字,它将调用访问者上的VisitFunctionLiteral

MyAstVisitor myAV(info);
info->function()->Accept(&myAV);
// next line is the V8 compile call
if (!MakeCode(info)) {

我将CompilationInfo指针info传递给自定义访问者,因为需要它来修改AST。构造函数如下:

MyAstVisitor(CompilationInfo* compInfo) :
    _ci(compInfo), _nf(compInfo->isolate(), compInfo->zone()), _z(compInfo->zone()){};

_ci、_nf 和 _z 是指向 CompilationInfoAstNodeFactory<AstNullVisitor>Zone 的指针。

现在,在 VisitFunctionLiteral 中,您可以遍历函数体,并在需要时插入语句。

void MyAstVisitor::VisitFunctionLiteral(FunctionLiteral* funLit){
    // fetch the function body
    ZoneList<Statement*>* body = funLit->body();
    // create a statement list used to collect the instrumented statements
    ZoneList<Statement*>* _stmts = new (_z) ZoneList<Statement*>(body->length(), _z);
    // iterate over the function body and rewrite each statement
    for (int i = 0; i < body->length(); i++) {
       // the rewritten statements are put into the collector
       rewriteStatement(body->at(i), _stmts);
    }
    // replace the original function body with the instrumented one
    body->Clear();
    body->AddAll(_stmts->ToVector(), _z);
}

rewriteStatement方法中,现在可以检查语句。指针_stmts保存了语句列表,最终将替换原始函数体。因此,要在每个语句后添加打印语句,首先添加原始语句,然后添加自己的打印语句:

void MyAstVisitor::rewriteStatement(Statement* stmt, ZoneList<Statement*>* collector){
    // add original statement
    collector->Add(stmt, _z);

    // create and add print statement, assuming you define print somewhere in JS:

    // 1) create handle (VariableProxy) for print function
    Vector<const char> fName("print", 5);
    Handle<String> fNameStr = Isolate::Current()->factory()->NewStringFromAscii(fName, TENURED);
    fNameStr = Isolate::Current()->factory()->SymbolFromString(fNameStr);
    // create the proxy - (it is vital to use _ci->function()->scope(), _ci->scope() crashes)
    VariableProxy* _printVP = _ci->function()->scope()->NewUnresolved(&_nf, fNameStr, Interface::NewUnknown(_z), 0);

    // 2) create message
    Vector<const char> tmp("Hello World!", 12);
    Handle<String> v8String = Isolate::Current()->factory()->NewStringFromAscii(tmp, TENURED);
    Literal* msg = _nf.NewLiteral(v8String);

    // 3) create argument list, call expression, expression statement and add the latter to the collector
    ZoneList<Expression*>* args = new (_z) ZoneList<Expression*>(1, _z);
    args->Add(msg);
    Call* printCall = _nf.NewCall(_printVP, args, 0);
    ExpressionStatement* printStmt = _nf.NewExpressionStatement(printCall);
    collector->Add(printStmt, _z);   
}
NewCallNewUnresolved的最后一个参数是一个数字,用于指定脚本中的位置。我认为这是用于调试或错误信息,以便告诉出现错误的具体位置。至少我从未遇到过将其设置为0的问题(也有一个常量kNoPosition)。
最后几句话:实际上,这不会在每个语句后添加打印语句,因为块(例如循环体)是表示一组语句的语句,而循环是具有条件表达式和主体块的语句。因此,您需要检查当前处理的语句类型,并递归地查看其中的内容。重写块与重写函数主体基本相同。
但是,当您开始替换或修改现有语句时,您将遇到问题,因为AST也携带有关分支的信息。因此,如果您替换某些条件的跳转目标,则会破坏代码。我想,如果直接向单个表达式和语句类型添加重写功能,而不是创建新的重写功能来替换它们,则可以解决此问题。
到目前为止,希望能对您有所帮助。

考虑一下我在“简化任意语言的分支覆盖”中概述的方法,作为替代方案。http://www.semdesigns.com/Company/Publications/TestCoverage.pdf - Ira Baxter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接