美文网首页
2019-06-18

2019-06-18

作者: 吴亨 | 来源:发表于2019-06-18 20:41 被阅读0次

    [TOC]

    Debug是主流语言一般都会提供的能力,方便开发者在遇到问题时跟踪当前调用堆栈和变量的情况。但是对于其中的原理我相信大部分同学是没有研究过的。最近在做一个Javascript的debug工具,对debug相关的知识有了接触,这里做一个总结和大家交流下

    原理概述

    deubg架构图:利用adb forward将PC端口9420转发到手机端口9420实现双方socket通信


    image.png

    debug时序图:


    image.png

       如上图,Debug分为工具端和程序端,工具端是展示源码和运行堆栈的ide或者编辑器;程序端就是我们运行中的要debug的程序。
       工具端通过socket给程序端发送attach setbreakpoint等指令,程序端有一个read线程专门接收相关指令,最终将相关指令执行或者发送到其他线程执行。

    Socket通信

    利用通用的sys/socket.h提供的socket接口进行读写通信。
    这里定了一个简单的协议:
    1.头四个字节存储指令长度,后面拼接指令内容;
    2.指令body用json来承载

    {
      "id": 1,
      "domain": "Debugger",
      "commands": {
        "name": "SetBreakpointByUrl",
        "parameters": [{
          "proj_dir":"blabla-game",
          "lineNumber": 73,
          "url": "BoxRotate.js"
        }]
      }
    }
    

    Deubg相关接口

    一个语言之所以能够debug肯定是他的运行环境对外提供了相关接口,让我们有机会再它运行的时候进行拦截。Js的解析引擎是Javascript Core(简称JSC),它就提供了相关的debug接口,下面我们来详细看下这些接口。
    首先这些dubug接口都是在JSC的私有头文件并没有对外暴露,所以我们想使用这些接口,就必须将这些私有的头文件include进来。但是呢每个版本的私有头文件是有变动的,所以我们必要要保证使用的头文件和依赖的JSC.so是同一个版本。
    最终我们include这些头文件:

    JSC和debug相关的接口位于debugger目录的Debugger.h

    初始化

    这里的接口用起来也很方便,只需要实例化一个Debugger,再调用起attach方法即可。

    Debugger(VM&);
    void attach(JSGlobalObject*);
    
    JSGlobalContextRef ctx = reinterpret_cast<JSGlobalContextRef>(jsGlobalContext);
    JSC::JSGlobalObject *pGlobalObject = toJS(ctx)->vmEntryGlobalObject();
    jsGlobalObject.vm()
    

    我们可以看到这里需要一个JSGlobalObject,这个获取也很简单,就是JS虚拟机初始化的时候创建的context转换而来:

    #include <cassert>
    #include <runtime/JSGlobalObject.h>
    JSGlobalContextRef jsGlobalContext = JSGlobalContextCreate(NULL);
    JSC::JSGlobalObject *pGlobalObject = toJS(ctx)->vmEntryGlobalObject();
    

    有了JSGlobalObject,可以直接通过vm()获取vm对象,这样我们就可以实例化一个Debugger了,接着我们再调用attach方法就可以将当前debugger设置到JS虚拟机中进行运行插桩了。但是我们还要调用另外两个方法:setBreakpoint设置具体断点以及activateBreakpoints将断点激活。

    这样我们就做完了第一步(实例化Debugger的时候需要将virtualj接口都有实现,我们可以继承Debugger进行简单实现),我们来编译运行看下。
    果不其然,事情并没有这么简单,直接编译报错:

    error: undefined reference to 'typeinfo for JSC::Debugger'

    由于对c++不是很熟,所以不明白为什么会报错找不到Debugger,明明已经include了。研究了下,最后请教c++老手知道了是由于开了rtti导致的,rtti表示运行时类型识别,进行dynamic_cast时要用到。所以这里我们只需要在编译选项里关闭rtti就行了:

    cppFlags "-fno-rtti"

    activateBreakpoints线程问题

    调用activateBreakpoints之后会调用recompileAllJSFunctions

    void JSDebuggerImpl::recompileAllJSFunctions() {
      JSC::JSLockHolder lock(vm());
      Debugger::recompileAllJSFunctions();
    }
    

    我们可以看到这里有个lock方法,这是JSC在进行一些关键操作时的加锁,所以必须运行在JS线程。但是我们收到相关指令是在socket线程,所以这里我们必须要将相关指令转移到JS线程运行,就如文章开头所绘的时序图。

    JS线程Runnable队列

    JS线程有个loop循环不断处理相关事件(比如JS加载、图片加载等),我们在这个loop里面加入runnable队列的处理逻辑:

        std::list<std::function<void()>> runnableList;
        void handleRunnable(){
            if(runnableList.empty()){
                return;
            }
            std::list<std::function<void()>> tempList;
            pthread_mutex_lock(&runnableMutex);
            for(std::function<void()> funcAndArg:runnableList){
                tempList.push_back(funcAndArg);
            }
            runnableList.clear();
            pthread_mutex_unlock(&runnableMutex);
            for(std::function<void()> function:tempList){
                function();
            }
            runnableList.clear();
        }
    
          void runInJsThread(std::function<void()> runnable) {
            pthread_mutex_lock(&runnableMutex);
            runnableList.push_back(runnable);
            pthread_mutex_unlock(&runnableMutex);
        }
    

    我们将需要执行的逻辑放到闭包队列中,这样就不需要设计特定的Event了,可以当做通用的消息队列来处理各种逻辑。

    didParseSource

    调用JSEvaluateScript加载JS文件之后,ScriptDebugServer的didParseSource会被回调用于记录url和sourceId和映射关系:

    void JSDebuggerImpl::didParseSource(JSC::SourceID sourceId, const Script& script){
      NSLog("didParseSource sourceId=%d",sourceId);
      String url = script.url;
      if (!url.isEmpty())
      {
          m_mapSourceIdToUrl.add(sourceId, url);
          m_mapSourceUrlToId.add(url, sourceId);
      }
    }
    

    这里的sourceId在之后设置断点时会用到。

    setBreakpoint

    Breakpoint(SourceID sourceID, unsigned line, unsigned column...)
    

    显而易见,断点由sourceID、行号、列号组成,这很好理解。

    didPause

    以上Debug初始化设置之后,本以为就可以将断点断下来了。但是实际上并没有,当时很困惑,不知道哪里出错了。虽然没有断下来,但是却发现Debugger的didPause方法会在执行到断点逻辑时被回调,这里貌似有些文章。最后在另外一个同学那里了解到这是正常的。原来执行到断点时JSC只是给我们一个回调,至于怎么做需要我们来处理。所以我们需要在这里将JS线程wait住即可达到断点暂停的效果。同时这个方法也会将当前的栈帧信息推过来,这样我们就可以获取Stack Frame 和变量推给工具端展示给开发者。

    Stack Frame & Properties

    void didPause(JSC::ExecState &es, JSC::JSValue callFrames, JSC::JSValue exception)
    

    这个方法是整个debug的核心,我们可以看到这里将callFrames和exception推过来,我们需要对其进行处理来获取整个调用栈和变量。

    Stack Frame

    JSValueRef lineRef = GetNativeProperty(ctx, callFrame, JSStringCreateWithUTF8CString("line"));
    JSValueRef functionNameRef = GetNativeProperty(ctx, callFrame, JSStringCreateWithUTF8CString("functionName"));
    JSValueRef sourceIdRef = GetNativeProperty(ctx, callFrame, JSStringCreateWithUTF8CString("sourceID"));
    

    这里是对callFrame的处理,可以获取方法名、所在源码文件以及行号用于工具端定位到相应源码。
    当然这只是对栈顶方法的处理,我们要获取整个调用栈的话还要递归调用:

    JSValueRef callerRef = GetNativeProperty(ctx, currentFrame, JSStringCreateWithUTF8CString("caller"));
    

    这样我们就可以获取所有调用方法的栈帧了。

    Get Properties

    scopeChain & thisObject

    Debug另一个重要作用就是获取当前方法的局部变量和全局变量信息,我们来看下这里的处理逻辑:

    JSValueRef scopeChainRef = GetNativeProperty(ctx, callFrame, JSStringCreateWithUTF8CString("scopeChain"));
    

    scopeChain,即作用域链,用于在标识符解析中变量查找。简单说就是通过它可以获取当前方法的所有变量,包括局部变量、upvalues及global变量。

    bool JSDebuggerImpl::ProcessScopeChain(JSContextRef ctx, JSObjectRef scopeChain, tiny::xarray &locals, tiny::xarray &upValues, tiny::xarray &globals, JSValueRef thisValueRef, tiny::TinyJson &mapId2Var, std::map<JSValueRef, IdType> &crossTable)
    {      JSPropertyNameArrayRef nameArrayRef = JSObjectCopyPropertyNames(ctx, scopeChain);
           size_t nameArrayCount = JSPropertyNameArrayGetCount(nameArrayRef);
                for (size_t i = 0; i < nameArrayCount; ++i)
                {
                    JSValueRef scopeRef = JSObjectGetPropertyAtIndex(ctx, scopeChain, static_cast<unsigned>(i), &exception);
                    if (scopeRef && !exception && JSValueIsObject(ctx, scopeRef))
                    {
                        JSObjectRef scope = JSValueToObject(ctx, scopeRef, &exception);
                        if (i == 0)
                            {//index 0 for "locals"
                                ProcessScope(ctx, scope, locals, mapId2Var, crossTable);
                            }
                            else if (i >= 1 && i <= nameArrayCount - 2)
                            {//index 1 to (n-2) for "upvalues"
                                ProcessScope(ctx, scope, upValues, mapId2Var, crossTable);
                            }
                            else if (i == nameArrayCount - 1)
                            {//index (n-1) for "globals"
                                if (globals.Count() == 0)
                                {
                                    ProcessScope(ctx, scope, globals, mapId2Var, crossTable);
                                }
                            }
    
        }
    }
    

    我们可以看到scopeChain是个数组,第一个存放的是local变量;中间存放的是upValues;最后一个元素存放的是global变量。

    JSValueRef thisObjectRef = GetNativeProperty(ctx, callFrame, JSStringCreateWithUTF8CString("thisObject"));
    

    这里面的thisObject也是global对象,我们也要解析它放到global中。

    变量解析

        bool JSObjectProcessProperty(JSContextRef ctx, JSObjectRef objRef, std::function<void(JSStringRef, JSValueRef)> fun)
        {
            JSPropertyNameArrayRef nameArrayRef = JSObjectCopyPropertyNames(ctx, objRef);
        auto nameArraySize = JSPropertyNameArrayGetCount(nameArrayRef);
        for (decltype(nameArraySize) i = 0; i < nameArraySize; ++i){
               JSStringRef nameRef = JSPropertyNameArrayGetNameAtIndex(nameArrayRef, i);    
               JSValueRef valRef = JSObjectGetProperty(ctx, objRef, nameRef, &exception);
            }
        }
    

    这里遍历JSObjectRef所有成员变量,对每一个成员变量再根据其类型进行处理

    switch (JSValueGetType(ctx, valRef)){
            case kJSTypeUndefined:
                value.put(Variable_Key_Type,"undefined");
                break;
            case kJSTypeNull:
                value.put(Variable_Key_Type,"null");
                break;
            case kJSTypeBoolean:
                value.put(Variable_Key_Type,"boolean");
                value.put(Variable_Key_Value,JSValueToBoolean(ctx, valRef));
                break;
            case kJSTypeNumber:
                value.put(Variable_Key_Type,"number");
                number = JSValueToNumber(ctx, valRef, nullptr);
                break;
            case kJSTypeString:
                value.put(Variable_Key_Type,"string");
                value.put(Variable_Key_Value,JSStringRefToStdString(JSValueToStringCopy(ctx, valRef, nullptr), ctx));
                break;
            case kJSTypeObject:
            {
               JSObjectRef objRef = JSValueToObject(ctx, valRef, &exception);
                if (objRef && !exception)
                {
                    if (JSObjectIsFunction(ctx, objRef))
                    {//function
                        value.put(Variable_Key_Type,"function");
                    } else{//normal
                        value.put(Variable_Key_Type,"object");
                        m_varJSValue.emplace(IntegerToString(varId),objRef);
                }
    }
    

    将Object放到std::map<std::string, JSObjectRef> m_varJSValue;缓存起来,以待GetProperties时使用.
    这样我们就获取了当前栈帧的所有变量。我们即可将栈帧和变量信息推给工具端。

    {
      "method":"Debugger.Paused",
      "parms":{
        "globals":[1,2,3],
        "frames":[
          {
            "url":"BoxRotate.js",
            "lineNumber":34,
            "funcName":"fun1",
            "upvalues":[1,2,3],
            "locals":[1,2,3]
          }
        ]
      }
    }
    

    工具端收到栈帧信息即展示,同时定位到相应源码文件。这块逻辑课参考Debug插件端原理

    GetProperties

    工具端在展示栈帧的同时,也有一块变量区,我们点开即会发送指令给程序端,让其解析相关变量:

    {
      "id": 1,
      "domain": "Debugger",
      "commands": {
        "name": "GetProperties",
        "parameters": [1,18,19,36,37,38]
      }
    }
    

    使用变量id从m_pMapToValue获取对应的值,如果是object类型,从m_varJSValue获取对应的object对象,接着解析之。

    bool JSDebuggerImpl::analyticJSObject(std::string vardId,tiny::xarray &varIdTable)
    {
        auto iter = m_varJSValue.find(vardId);
        if (iter != m_varJSValue.end() && m_debugCtx)
        {
            return JSObjectProcessProperty(m_debugCtx, iter->second, [&](JSStringRef nameRef, JSValueRef valRef) {
                IdType subId = BuildValueAndGetId(m_debugCtx, valRef, nameRef, *m_pMapId2Var, crossTable);;
                if (subId > 0)
                {
                    varIdTable.add(subId);
                }
            });
        }
        return false;
    }
    

    但是这里有个线程问题,就是JSObjectCopyPropertyNames必须在JS线程执行,而我们收到的指令是在socket线程,所以必须转到JS线程。我们之前设计了JS线程通用事件处理对列,是不是可以直接用呢?
    很明显不行。因为此时的JS线程处于wait状态,我们把事件放到对列里面也不会立即执行。所以这里我们需要临时notify JS线程,再获取变量值。

    断点后事件队列

    int BreakpointHitCallback(...) {
    
        m_breakpointCV.wait(lck);
        handleLogicAfterBreakPoint();
    
    }
    

    收到GetProperties指令时,我们将相关逻辑放到runnableList里,同时临时m_breakpointCV.notify_one唤醒JS线程,进入相关事件队列处理逻辑handleLogicAfterBreakPoint

            std::list<std::function<void()>> runnableList;
            /**
             * 断点pause之后吧,我们可能需要js处理一些事情,譬如请求部分变量,
             这时我们必须在js线程请求,就需要临时notify js线程
             */
            void runLogicAfterBreakPoint(std::function<void()> runnable) {
                pthread_mutex_lock(&runnableMutex);
                runnableList.push_back(runnable);
                pthread_mutex_unlock(&runnableMutex);
            }
    
            void handleLogicAfterBreakPoint(){
                while(!runnableList.empty()){
                    std::list<std::function<void()>> tempList;
                    pthread_mutex_lock(&runnableMutex);
                    for(std::function<void()> funcAndArg:runnableList){
                        tempList.push_back(funcAndArg);
                    }
                    runnableList.clear();
                    pthread_mutex_unlock(&runnableMutex);
                    for(std::function<void()> function:tempList){
                        function();
                    }
                    tempList.clear();
                    //处理完临时需求之后,我们需要再次把js线程挂起,
                    // 这里要再次判断下队列是否有新进入的待处理任务,fix bug
                    if(runnableList.empty()){
                        std::unique_lock<std::mutex> lck(m_breakpointMtx);
                        m_breakpointCV.wait(lck);
                    }
                }
            }      
    

    这里需要循环卡在这里,每次执行完之后再将JS线程wait起来知道下次再被唤醒,如果此时runnableList为空,就说明没有事件要处理了,只是将JS线程恢复运行。

    } else if (command == "GetProperties") {
        runLogicAfterBreakPoint([this,id, paramsString]() {
                m_pDebugger->analyticJSObject(val, varIdTable);
        });
        //将断点唤醒,去取变量
        m_breakpointCV.notify_one();
    

    GetPoperties Crash

    对一些稍微复杂点的JS程序,我们在解析变量时遇到了一个很诡异的crash:
    譬如在解析QGBindingCanvas时,却拿到了QGBindingCanvasContext的变量,这时取变量时肯定就因为访问非法地址而crash。
    这里先解释一下JSC的赋值和取值原理:

    JSC只是js语法的解析引擎,真正执行的是底层实现,譬如canvas2D接口需要我们在底层调用opengl实现。
    所以我们需要在c++层构建一个c++对象与JSObjectRef绑定,这样真正执行的就是这个绑定的c++对象了。

    JSObjectRef qg_callAsConstructor(JSContextRef ctx, JSObjectRef constructor, size_t argc, const JSValueRef argv[], JSValueRef* exception) {
        QGBindingBase* pClass = (QGBindingBase*)(JSObjectGetPrivate( constructor ));
        JSClassRef jsClass = QGApp::instance()->getJSClassForClass(pClass,false);
        JSObjectRef obj = JSObjectMake( ctx, jsClass, NULL );
        QGBindingBase* instance = (QGBindingBase*)NSClassFromString(pClass->toString().c_str());
        instance->initWithContext(ctx, obj, argc, argv);
        JSObjectSetPrivate( obj, (void *)instance );
        return obj;
    }
    

    再来看下构建JSObjectRef的过程:

        NSObjectFactory::fuc_map_type* base = NSObjectFactory::getFunctionMap();
        for(NSObjectFactory::fuc_map_type::iterator it = base->begin(); it != base->end(); it++)
        {
           if( name.find("_get_") != string::npos ) {
                            int is_member_func = name.find(base_obj_tmp);
                            if (is_member_func != string::npos)
                {
                    // We only look for getters - a property that has a setter, but no getter will be ignored
                    properties->setObject(NSStringMake(name.substr(pos + strlen("_get_"))), name);
                }
            }
        JSClassDefinition classDef = kJSClassDefinitionEmpty;
        classDef.staticValues = values;
        classDef.staticFunctions = functions;
        JSClassRef js_class = JSClassCreate(&classDef);
    

    这里我们可以看到,主要是在fuc_map里面匹配查找pClass的方法的属性,再赋给JSClassRef,这样在JS层使用时就把它的方法和属性限定好了。我们在调用相应方法或者获取属性值时也是在binding层调用对应方法获取实际的值。
    但是这有一个隐藏bug:

    int is_member_func = name.find(base_obj_tmp);
    

    这里通过类名局部匹配:
    譬如方法"_ptr_to_QGBindingCanvas_get_height"会匹配给QGBindingCanvas,这没问题;但是按现在规则"_ptr_to_QGBindingCanvasContext_get_scale"也会匹配给QGBindingCanvas。
    所以我们解析变量的时候会解析到异常地址而crash.
    这里只需如此修改:

    int is_member_func = name.find("_"+base_obj_tmp+"_");
    

    步调step

    stepInto

    进入子方法
    

    stepOver

    当前方法按步调试
    

    stepOut

    跳出当前方法
    

    小结

    本文对debug主要的流程和概念做了简单的总结:


    image.png

    以上。

    相关文章

      网友评论

          本文标题:2019-06-18

          本文链接:https://www.haomeiwen.com/subject/dobqqctx.html