1 Star 0 Fork 0

Yellow-Sky-Proton / OpenHarmony-build-docs

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
调试模块变量展示功能内部逻辑.md 15.28 KB
一键复制 编辑 原始数据 按行查看 历史

调试模块变量展示功能内部逻辑

调试模块变量值展示功能逻辑主体可以分为两个部分:对于变量相关信息的提取和缓存,调试时获取变量值并展示。

变量相关信息的提取和缓存

DebugInfo的提取

DebugInfo包含了一个方法的program counter,在源代码中的行号,和方法中的local variable之间的映射关系。这其中与调试时变量展示相关的是储存在line number program中的local variable table,其内部存储的是local variable info对象,包含local variable的变量名、寄存器编号,以及作用域开始和结束对应的offset。DebugInfo提取的流程如下:

当以调试模式启动应用时,.abc文件以添加【--debug】flag的方式编译,然后通过调用方法

debug_info_extractor->Extract();

触发提取动作,该方法在获取到需要的参数之后,会进一步调用

debug_info_extractor->ExtractorMethodDebugInfo();

生成并提取MethodDebugInfo。MethodDebugInfo是方法调式信息结构体,结构如下,并且里面包含了与变量展示相关的local variable table,line/column number table等,存储在debug info extractor类中。

struct MethodDebugInfo {
    std::string sourceFile; //源文件
    std::string sourceCode; //源代码
    LineNumberTable lineNumberTable; //行号表
    ColumnNumberTable columnNumberTable; //列号表
    LocalVariableTable localVariableTable; //本地变量表
};

调用该方法之后,在其内部会初始化:1. line program state,主要是作为中间媒介来生成program counter和行列以及本地变量之间的映射关系,其中会存储{1. address:方法指令对应的program counter;2. line/column: 源码中对应的行号/列号;3. const_pool:常量池的地址};2. line number program handler,line number program对应的handler,其中存储 line program state,local variable table等;3. line number program processor,line number program的执行类,其中存储 line number program handler, 以及包含读取opcode,执行opcode,以及针对不同的opcode的handle method。

初始化完成后,通过调用

programProcessor.Process();

显式触发opcode的读取并开始提取信息。line number program中与当前相关的opcode是【START_LOCAL】/ 【START_LOCAL_EXTENDED】,代表一个本地变量作用域的开始,同时也会将存储该变量的寄存器编码在该opcode中。在该方法的handle method中,就会根据需要将该local variable的debugInfo封装后存入local variable table

bool HandleStartLocal(int32_t regNumber, uint32_t nameId, [[maybe_unused]] uint32_t typeId)
{
    uint32_t start_offset = state_->GetAddress(), end_offset = 0; //初始化该变量作用域边界
    const char *name = GetStringFromConstantPool(state_->GetPandaFile(), nameId); //从常量池中获取变量名称
    lvt_.push_back({name, regNumber, start_offset, end_offset}); //local variable table中存入该封装对象
    return true;
}

当line number program processor遇到【END_SEQUENCE】时,执行结束,回到上层方法中,并将生成的MethodDebugInfo封装,并保存在当前debug info extractor实例的method_对象(Map)中。

MethodDebugInfo methodDebugInfo = {sourceFile, sourceCode, handler.GetLineNumberTable(),
        handler.GetColumnNumberTable(), handler.GetLocalVariableTable()};
methods_.emplace(offset, std::move(methodDebugInfo));

至此,debugInfo的提取结束

local variable的缓存

当应用以调试模式启动时,debugger start,client和debugger server连接建立,scripts loaded and parsed后,debugger会先paused:break on start,从而调用到

DebuggerImpl::NotifyPaused(std::optional<JSPtLocation> location, PauseReason reason)

来处理暂停事件并通知前端pause event。在此方法中,会调用GenerateCallFrames方法去生成callframes

if (!GenerateCallFrames(&callFrames)) {
    LOG_DEBUGGER(ERROR) << "NotifyPaused: GenerateCallFrames failed";
    return;
}

在该方法中会初始化一个爬栈对象StackWalker,并对每一个不对应Native Method的栈帧初始化callframe对象,并调用GenerateCallFrame方法创建对应的callframe

if (!GenerateCallFrame(callFrame.get(), frameHandler, callFrameId)) {
    if (callFrameId == 0) {
    	return StackState::FAILED;
    }
}

该方法中会先获取method对象和methodId,通过

method->GetJSPandaFile();

获得当前栈帧对应的method的JSPandaFile,然后再通过

DebugInfoExtractor *extractor = GetExtractor(jsPandaFile);

获取debug info extractor对象,里面存储了已经经过第一部分【DebugInfo的提取】后提取出的debuginfo。然后,会先初始化一个scopeChain对象,并调用以下三个方法分别获取local,module,global作用域的作用域信息。

GetLocalScopeChain(frameHandler, &thisObj);
GetModuleScopeChain();
GetGlobalScopeChain();
GetLocalScopeChain:

该方法按照顺序第一个被调用,来获取local作用域信息。该方法首先获取到methodId,debug info extractor(同上),然后会初始化一个localObj来储存作用域信息

Local<ObjectRef> localObj = ObjectRef::New(vm_);

而后,在runtimeImpl中的runtime对象中的【properties_】对象(Map)中 Index = 【curObjectId】的位置存入该localObj对象,并increment curObjectId,因为第一个被调用,local作用域对应的objectId = 0

runtime_->properties_[runtime_->curObjectId_++] = Global<JSValueRef>(vm_, localObj);

在初始化一个thisVal来代表this variable后,该方法调用了以下两个方法来获取local variable和closure variable

GetLocalVariables(frameHandler, methodId, jsPandaFile, thisVal, localObj);
GetClosureVariables(frameHandler, thisVal, localObj);
GetLocalVariables:

该方法用来获取和缓存本地变量。其通过以下方法获取到了debug info extractor中的local variable table

extractor->GetLocalVariableTable(methodId)

然后,loop over local variable table中的local variable info对象,并进行如下操作

for (auto &localVariableInfo : extractor->GetLocalVariableTable(methodId)) {
    uint32_t bcOffset = DebuggerApi::GetBytecodeOffset(frameHandler); //获取当前bytecodeOffset
    std::string varName = localVariableInfo.name; //获取变量名
    int32_t regIndex = localVariableInfo.reg_number; //获取寄存器编号
    if (!IsWithinVariableScope(localVariableInfo, bcOffset)) { //新增:判断当前变量是否在作用域内,若不在则跳过
        continue;
    }
    if (varName == "4newTarget" || varName == "0this" || varName == "0newTarget" || varName == "0funcObj") {
        continue;
    }
    value = DebuggerApi::GetVRegValue(vm_, frameHandler, regIndex); //从寄存器中获取当前变量的value
    if (varName == "this") {
        LOG_DEBUGGER(INFO) << "find 'this' in local variable table";
        thisVal = value; //赋值给thisVal
        continue;
    }
    Local<JSValueRef> name = JSValueRef::Undefined(vm_);
    if (varName == "4funcObj") {
        if (value->IsFunction()) {
            auto funcName = Local<FunctionRef>(value)->GetName(vm_)->ToString();
            name = StringRef::NewFromUtf8(vm_, funcName.c_str());
        } else {
            continue;
        }
    } else {
        name = StringRef::NewFromUtf8(vm_, varName.c_str()); //保存varname
    }
    PropertyAttribute descriptor(value, true, true, true);
    localObj->DefineProperty(vm_, name, descriptor); //代表local作用域信息的localObj存入当前变量property的信息
}
GetClosureVariables:

该方法用来获取闭包变量。一开始先获取当前词法环境lexEnv,然后通过调用以下方法获取scopeDebugInfo

auto *scopeDebugInfo = reinterpret_cast<ScopeDebugInfo *>(JSNativePointer::Cast(lexEnv->GetScopeInfo().GetTaggedObject())->GetExternalPointer());

然后通过loop over scopeDebugInfo中的scopeInfo,获取变量名和slot number

for (const auto &[varName, slot] : scopeDebugInfo->scopeInfo) {
    if (varName == "4newTarget") {
        continue;
    }
    Local<JSValueRef> value = JSNApiHelper::ToLocal<JSValueRef>(
        JSHandle<JSTaggedValue>(thread, lexEnv->GetProperties(slot))); //获取当前变量的value
    if (varName == "this") {
        if (thisVal->IsHole()) {
            LOG_DEBUGGER(INFO) << "find 'this' in current lexical env";
            thisVal = value; //如果找到this变量,赋值thisVal
        }
        continue;
    }
    Local<JSValueRef> name = StringRef::NewFromUtf8(vm_, varName.c_str()); //处理保存变量名
    PropertyAttribute descriptor(value, true, true, true);
    localObj->DefineProperty(vm_, name, descriptor);//代表local作用域信息的localObj存入当前变量property的信息
}

如果当前词法环境中没有找到this,去当前层的上一层继续找。

GetModuleScopeChain:

在local scope chain已经处理完成后,该方法第二个被调用,来获取module的作用域。与local类似,该方法会先初始化一个moduleObj存储该作用域信息

Local<ObjectRef> moduleObj = ObjectRef::New(vm_);

然后同样在【properties_】上 index = 【curObjectId】的位置缓存该对象。特殊的是,因为先前GetLocalScopeChain中如果找到了'this'对应,即thisVal有值,在调用GetModuleScopeChain之前会先缓存该‘this’对象

*thisObj = RemoteObject::FromTagged(vm_, thisVal);
runtime_->CacheObjectIfNeeded(thisVal, (*thisObj).get()); //缓存this对象

所以对于module域来说,其对应的objectId可能为1或2

runtime_->properties_[runtime_->curObjectId_++] = Global<JSValueRef>(vm_, moduleObj);

之后通过thread获取currentModule对象,并调用以下三个方法获取module域下的三类变量

DebuggerApi::GetLocalExportVariables(vm_, moduleObj, currentModule, false);
DebuggerApi::GetIndirectExportVariables(vm_, moduleObj, currentModule);
DebuggerApi::GetImportVariables(vm_, moduleObj, currentModule);

这三个方法的实现都相似,通过获取currentModule中的NameDictionary,遍历其中的localExportEntries/indirectExportEntries/importEntries 三个数组来获取变量名称和变量值

GetGlobalScopeChain:

在module scope chain处理之后,该方法最后一个被调用,来获取global的作用域,该方法会直接调用JSNApi接口获得当前vm对应的global object,然后缓存在【properties_】 上 index = 【curObjectId】的位置,因为module域对应的objectId的不确定性,所以global域对应的objectId可能为2或3

runtime_->properties_[runtime_->curObjectId_++] = Global<JSValueRef>(vm_, JSNApi::GetGlobalObject(vm_));

在这三个方法执行完成之后,GenerateCallFrame方法组装生成的callframes并返回结果,然后外层GenerateCallFrames返回StackState::CONTINUE走向下一个栈帧,并重复以上操作。

调试时获取变量值并展示

当应用以singlestep调试,调试遇到断点,或者在刚启动后的break on start时,debugger会进去paused状态,此时会先执行clear操作,与此处相关的是clean【properties_】上缓存的所有object,和重置【curObjectId】

runtime_->curObjectId_ = 0;
runtime_->properties_.clear();

之后,在notifyPaused方法中会重新调用GenerateCallFrames生成最新版本的callframe信息,以及里面包含的作用域scope信息,并会重新刷新【properties_】上缓存的变量信息/作用域信息,最后组装返回给前端。

然后,为了获取当前所有展示变量的信息,前端会发起2组共4条请求,请求的method为Runtime.getProperties,并会传入包含请求对象的objectId在内的4个参数

RemoteObjectId objectId_ {};
std::optional<bool> ownProperties_ {};
std::optional<bool> accessorPropertiesOnly_ {};
std::optional<bool> generatePreview_ {};

DispatchResponse RuntimeImpl::GetProperties(const GetPropertiesParams &params,
    std::vector<std::unique_ptr<PropertyDescriptor>> *outPropertyDesc,
    [[maybe_unused]] std::optional<std::vector<std::unique_ptr<InternalPropertyDescriptor>>> *outInternalDescs,
    [[maybe_unused]] std::optional<std::vector<std::unique_ptr<PrivatePropertyDescriptor>>> *outPrivateProps,
    [[maybe_unused]] std::optional<std::unique_ptr<ExceptionDetails>> *outExceptionDetails)

其中,当用户还没有点击展开任意一个object时,前端会先发送以下2组4条请求,请求的objectId分别为0和1,也就是请求local域和('this'或者module域)的变量信息

WsServer OnMessage: {"id":553,"method":"Runtime.getProperties","params":{"accessorPropertiesOnly":true,"generatePreview":true,"objectId":"0","ownProperties":false}}

WsServer OnMessage: {"id":554,"method":"Runtime.getProperties","params":{"accessorPropertiesOnly":false,"generatePreview":true,"objectId":"0","ownProperties":true}}

{"id":557,"method":"Runtime.getProperties","params":{"accessorPropertiesOnly":true,"generatePreview":true,"objectId":"1","ownProperties":false}}

{"id":558,"method":"Runtime.getProperties","params":{"accessorPropertiesOnly":false,"generatePreview":true,"objectId":"1","ownProperties":true}}

当accessorPropertiesOnly为true且ownProperties为false的时候,请求的是【proto】的信息,当accessorPropertiesOnly为false且ownProperties为true时,请求的是该对象和该对象内properties即变量的信息。在调用的runtime_impl的GetProperties方法中,其先将参数中的objectId作为key在【properties_】中查找以该参数做为key的entry

auto iter = properties_.find(objectId); //从properties中找到当前objectid的对象

找到后,获取到该key在【properties_】上缓存的值

if (!Local<ObjectRef>(value)->GetOwnProperty(vm_, name, jsProperty)) { //获取value内的object properties
    continue;
}
Local<JSValueRef> value = Local<JSValueRef>(vm_, iter->second); //获取value

当获取到的value是容器时,调用对应的容器处理方法去处理容器类的value,如果不是,将会获取value中存储的properties,并loop over its names arrays,对于每一个变量,初始化一个debuggerProperty,存入变量名和变量值,再在【properties_】上 index = 【curObjectId】的位置缓存该变量信息,最后返回前端该debuggerProperty对象

if (debuggerProperty->HasValue()) {
    Local<JSValueRef> vValue = jsProperty.GetValue(vm_);
    if (vValue->IsObject() && !vValue->IsProxy()) {
        debuggerProperty->GetValue()->SetObjectId(curObjectId_);
        properties_[curObjectId_++] = Global<JSValueRef>(vm_, vValue);
    }
}

处理完之后,该方法还会调用两个方法获取proto(如果参数指示请求的为proto)和additional properties,最后返回。

当开发者手动点击展开某一个特定的变量时,前端会发送objectId为该变量对应的objectId值的1组2条请求,其中,后端在返回的property中有objectId的,说明该变量为object,可以进一步点击查看。

1
https://gitee.com/huangtianzhi/open-harmony-build-docs.git
git@gitee.com:huangtianzhi/open-harmony-build-docs.git
huangtianzhi
open-harmony-build-docs
OpenHarmony-build-docs
master

搜索帮助