教你如何调试DartSDK

2020/6/14 23:25:38

本文主要是介绍教你如何调试DartSDK,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

利用--observer指令调试dart内置库

本文基于dart2.7,讲述了如何调试dart:core内置库的代码。

背景

有心的朋友会发现,当我们调试Flutter程序时,无法断点进入dart:core内部的代码。这份代码就是dart程序已经被内置的部分代码,我尝试在dart2.7下用VSCode进行调试:

import 'dart:io';
void main() async {
    final client = HttpClient();   // 断点处 
    final request = await client.getUrl(Uri.parse("https://www.baidu.com/"));
    final response = await request.close();    
}

复制代码

跳进HttpClient()却发现到了不是我们期待的实现位置

一、无法调试之谜?

回想下我们的C++程序是如何做到可以调试,当一个程序跑起来的时候,实际上跑的就是你读不懂二进制码,而你的程序因为断点中断挂起的原因是程序陷入了INT 3中断。INT 3是CPU的一个单字节指令0xCC,每次你给代码设置断点时,编译器都会先找代码对应的二进制码位置,将其指令修改为0xCC来达到调试中断目的。
Dart程序与标准程序不同的是,它无需CPU中断,因为所有指令都会经过dart虚拟机,只要标记了某条指令中断即可,所以首先问题的关键就是从文本代码位置到指令码位置的转换。内置库之所以无法被断点,很大可能是因为它已经没有了与文本源之间的关系,就算你下了断点,他也不知道标记哪个位置的指令码。

二、如何看Dart的构建依赖?

虽然目前我们还不确定问题出在哪里,但肯定在Dart命令行工具上。因为Dart程序是从命令行工具启动的,dart:core也是内嵌于Dart内部,所以现在我们需要重新编译一个Dart,如果你是做Flutter的,那Flutter Engine的源码依赖里便有了dart的源码,否则得上官网拉取。

2.1 构建unopt版本的dart

如何编译Dart可以关注下[《手把手教你编译Flutter engine》] (juejin.im/post/5c24ac…) , 大致上是一样的。为了待会能正常调试dart工具,特别注意需要修改下dart的编译选项,默认情况下dart工具会带优化编译,这会导致无法正确断点或者无法查阅变量,修改位置如下:

// src/third_party/dart/runtime/runtime_args.gni

- dart_debug = false
+ dart_debug = true

- dart_debug_optimization_level = "2"
+ dart_debug_optimization_level = "0"
复制代码

改完后,我们就可以执行构建了:

  1. 先执行 ./flutter/tools/gn --unoptimized 生成工程Host工程;

  2. 再执行Ninja指令时,ninja -C out/host_debug_unopt ,生成的工程里就有Dart命令行工具程序了

2.2 查看Dart构建依赖

Dart是使用GN构建的,GN的特点不仅是跨平台,他可以在构建的中间过程执行各种规则命令包括Python脚本,这也增加了阅读复杂性,好在谷歌有自知之明做了一些辅助工具,在src目录下,我们执行一下命令:

ninja -C ./out/host_debug_unopt  -t browse --port=8000 --no-browser dart
复制代码

PS: 此处不对GN和Ninja展开,可以参考GN官方文档和 Ninja官方文档

根据提示,我们可以在网页上打开LocalHost查看dart的构建依赖,如图:

上面从GN的角度上只包含两个角色,一个rulelink和N个target(dart,dill.o等都是)。就像普通构建是先编译最后链接一样,在dart工具最终构建前,会先编译下面的各个target,而后使用规则link将他们链接集成。
下面我们先来看下rule link 是什么内容。

2.3 查看Rule

GN所有的规则都在构建目录下的toolchain.ninja文件里,我在里面便可以看到link的内容:

// src/out/host_debug_unopt/toolchain.ninja
rule link
  command = ../../buildtools/mac-x64/clang/bin/clang++ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -mmacosx-version-min=10.11.0  ${ldflags}  -Xlinker -rpath -Xlinker @executable_path/Frameworks -o ${root_out_dir}/${target_output_name}${output_extension} -Wl,-filelist,${root_out_dir}/${target_output_name}${output_extension}.rsp ${solibs} ${libs}
  description = LINK ${root_out_dir}/${target_output_name}${output_extension}
  rspfile = ${root_out_dir}/${target_output_name}${output_extension}.rsp
  rspfile_content = ${in_newline}
复制代码

这样看还是不知道具体参数,我们可以再借助GN的辅助工具,执行以下指令:

ninja -C ./out/host_debug_unopt  -t commands dart
复制代码

从上面指令的输出,我们可以看到构建Dart过程所有的命令操作,拿最后一个就是link执行的内容

../../buildtools/mac-x64/clang/bin/clang++ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -mmacosx-version-min=10.11.0  -rdynamic -arch x86_64 -march=core2 -stdlib=libc++ -Wl,-search_paths_first -L. -Wl,-rpath,@loader_path/. -Wl,-rpath,@loader_path/../../.. -Wl,-pie  -Xlinker -rpath -Xlinker @executable_path/Frameworks -o ./dart -Wl,-filelist,./dart.rsp  -ldl -lpthread -framework CoreFoundation -framework Security -framework CoreServices
复制代码

上面指令就是一个标准的链接指令,输入是./dart.rsp里已编译好的文件,输出是./dart可执行文件。

三、dart文件如何执行?

当dart的构建完成后,我们就可以在Debug下跑最开始准备的调试代码了。

Dart文本如何通过工具被执行,可以先阅读介绍资料

下面我们会讲下关键步骤的执行

3.1 初始化 VM Isolate

一个dart程序由一个vm_isolate和一个或多个isolate组成,vm-isolate的数据和代码被所有isolate共享,结构如图:

kDartVmSnapshotData是vm-isolate的snapshot,在初始化isolate之前,需要先初始化一次vm-isolate,

// Initialize the Dart VM.
const uint8_t* vm_snapshot_data = kDartVmSnapshotData;
const uint8_t* vm_snapshot_instructions = kDartVmSnapshotInstructions;

Dart_InitializeParams init_params;
memset(&init_params, 0, sizeof(init_params));
init_params.vm_snapshot_data = vm_snapshot_data;
init_params.vm_snapshot_instructions = vm_snapshot_instructions;
Dart_Initialize(&init_params);
复制代码
3.2 初始化 kernel-service isolate

kernel-serivce主要用于将.dart文件编译为kernel文件,一个.dart从文本到可执行的流程如下:

kKernelServiceDill 与snapshot不一样,但同样是用于初始化isolate。如果说snapshot类似以前PC装机的ghost,那dill就是一个全自动安装包,前者是硬盘拷贝,后者就是挨个启动软件安装。

// Initialize Kernel-service isolate
const uint8_t* kernel_buffer = kKernelServiceDill;
intptr_t kernel_buffer_size = kKernelServiceDillSize;

const char* script_uri = "kernel-service";
const char* name = "kernel-service";
Dart_CreateIsolateGroupFromKernel(script_uri, name, kernel_buffer, kernel_buffer_size)

复制代码

执行kernel-service的main方法后获取isolate端口

// 将字符串加载进kernel_service isolate中
const String& entry_name = String::Handle(Z, String::New("main"));
// 获取kernel_service isolate的main方法引用
const Function& entry = Function::Handle(
        Z, root_library.LookupFunctionAllowPrivate(entry_name));
// 执行main方法,获取result返回值
const Object& result = Object::Handle(
        Z, DartEntry::InvokeFunction(entry, Object::empty_array()));
// 获取kernel_service isolate的端口号
kernel_port_ = result.Id();
复制代码
3.3 将.dart文件交给kernel-service isolate进行编译

代码里的 kPlatformStrongDilldart:core经过compile_kern.dart编译后的kernel binary

// sanitized_uri 是.dart文件路径
const char* sanitized_uri = "./test.dart";
const uint8_t* platform_kernel = kPlatformStrongDill;
intptr_t platform_kernel_size, = kPlatformStrongDillSize;

Dart_CompileToKernel(sanitized_uri, platform_kernel, platform_kernel_size);
复制代码

platform kernel会作为参数一起发送给kernel-service,因为语法树需要有引用的完整定义,如果.dart文件使用了dart:core的方法,platform kernel便可提供定义

// 准备dart消息对象
Dart_CObject message;
Dart_CObject* message_arr[] = {...,
				&uri, // sanitized_uri
				&dart_platform_kernel, // platform_kernel
				...};
message.value.as_array.values = message_arr;
message.value.as_array.length = ARRAY_SIZE(message_arr);

// 通过isolate端口与isolate通信
Dart_PostCObject(kernel_port, &message);
复制代码

获取编译后的kernel二进制对象

void LoadKernelFromResponse(Dart_CObject* response) {
	result_.kernel_size = response->value.as_typed_data.length;
	result_.kernel = static_cast<uint8_t*>(malloc(result_.kernel_size));
	memmove(result_.kernel, response->value.as_typed_data.values,
            result_.kernel_size);
}


复制代码
3.4 初始化主isolate

kDartCoreIsolateSnapshotData是主isolate的snapshot,主isolate是我们写的dart代码的执行主体

// main.cc ::CreateIsolateGroupAndSetupHelper
const uint8_t* isolate_snapshot_data = kDartCoreIsolateSnapshotData;
const uint8_t* isolate_snapshot_instructions =
    kDartCoreIsolateSnapshotInstructions;

const char* script_uri = "./test.dart";
const char* name = "main";
Dart_CreateIsolateGroup(script_uri, name, isolate_snapshot_data, isolate_snapshot_instructions);

复制代码

加载经过由kernel_service编译的.dart文件的kernel

// dart_api_impl.cc ::Dart_LoadScriptFromKernel
const auto& td = ExternalTypedData::Handle(ExternalTypedData::New(
      kExternalTypedDataUint8ArrayCid, const_cast<uint8_t*>(buffer),
      buffer_size, Heap::kOld));
std::unique_ptr<kernel::Program> program = kernel::Program::ReadFromTypedData(td, &error);      
kernel::KernelLoader::LoadEntireProgram(program.get());
复制代码

执行主isolate的main方法,即调用我们写的main方法

// 将字符串加载进kernel_service isolate中
const String& entry_name = String::Handle(Z, String::New("main"));
// 获取kernel_service isolate的main方法引用
const Function& entry = Function::Handle(
        Z, root_library.LookupFunctionAllowPrivate(entry_name));
// 执行main方法,获取result返回值
const Object& result = Object::Handle(
        Z, DartEntry::InvokeFunction(entry, Object::empty_array()));

复制代码
3.5 总结一下

对于我们的调试文件,Dart工具先是初始化了 vm isolate,然后又初始化了 kernel-service isolate 用于将调试文件编译为kernel,最后初始化了主 isolate 来执行编译出来的kernel。

四、Dart里的dill和snapshotdata从何而来?

通过上面我们知道,snapshot和dill在Dart程序的编译和执行都至关重要,而我们想要调试的dart:coresdk内置库也是提前被编成dill或者snapshot。下面我们会挨个弄清楚其数据来源。

我们可以先回到1.2的构建依赖界面,对其溯本求源。

4.1 kPlatformStrongDill

依赖路径:vm_platform_strong.dill.ovm_platform_strong.dill.Svm_platform_strong.dill

我们一个个从后往前推

vm_platform_strong.dill:

toolchain.ninja搜索对应的rule,会得到一个python命令,但实际上gn_run_binary.py就是拉起一个子进程跑dart,简化后的变成:

../../third_party/dart/tools/sdks/dart-sdk/bin/dart --packages=../../third_party/dart/.packages --dfe=../../third_party/dart/tools/sdks/dart-sdk/bin/snapshots/kernel-service.dart.snapshot ../../third_party/dart/pkg/front_end/tool/_fasta/compile_platform.dart dart$:core -Ddart.vm.product=false -Ddart.developer.causal_async_stacks=true -Ddart.isVM=true --single-root-scheme=org-dartlang-sdk --single-root-base=../../third_party/dart/ org-dartlang-sdk$:///sdk/lib/libraries.json vm_outline_strong.dill vm_platform_strong.dill vm_outline_strong.dill
复制代码

上面的命令意思就是运行compile_platform.dart,输入参数为../../third_party/dart/sdk/lib/libraries.json(内置库的路径),输出产物为vm_platform_strong.dillvm_outline_strong.dill

所以vm_platform_strong.dill就是dart:core内置库的kernel形式

vm_platform_strong.dill.S:

对应的指令:

../../third_party/dart/runtime/tools/bin_to_assembly.py --input ../../out/host_debug_unopt/vm_platform_strong.dill --output ../../out/host_debug_unopt/vm_platform_strong.dill.S --symbol_name kPlatformStrongDill --target_os mac --size_symbol_name kPlatformStrongDillSize --target_arch x64
复制代码

Python脚本bin_to_assembly.py创建一个汇编格式的文件bin.S,将bin文件的内容写入,如果为instruction,则为’.text’段,如果为data,则为’.global’段。他生成汇编文件的目的就是为了声明kPlatformStrongDill变量,并将上面生成的vm_platform_strong.dill文件内容赋值给它。

vm_platform_strong.dill.o:

对应的指令:

../../buildtools/mac-x64/clang/bin/clang -MD -MF obj/out/host_debug_unopt/dart_kernel_platform_cc.vm_platform_strong.dill.o.d -DUSE_OPENSSL=1 -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D_FORTIFY_SOURCE=2 -D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS -D_DEBUG -I../.. -Igen -fno-strict-aliasing -fstack-protector-all -arch x86_64 -march=core2 -fcolor-diagnostics -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -mmacosx-version-min=10.11.0  -c vm_platform_strong.dill.S -o obj/out/host_debug_unopt/dart_kernel_platform_cc.vm_platform_strong.dill.o
复制代码

clang可以直接利用汇编文件生成目标文件,后续生成Dart命令行工具时便可以通过链接vm_platform_strong.dill.o文件,从而找到kPlatformStrongDill和kPlatformStrongDillSize这两个符号。

4.2 kKernelServiceDill

依赖路径:kernel_service.dill.okernel_service.dill.Skernel_service.dill

kernel_service.dill:

对应的指令:

../../third_party/dart/tools/sdks/dart-sdk/bin/dart --depfile=../../out/host_debug_unopt/gen/kernel_service_dill.d --depfile_output_filename=gen/kernel_service.dill --dfe=../../third_party/dart/tools/sdks/dart-sdk/bin/snapshots/kernel-service.dart.snapshot ../../third_party/dart/pkg/vm/bin/gen_kernel.dart --packages=org-dartlang-kernel-service$:///.packages --platform=/Users/levi/Desktop/Flutter/env/engines/1.12.13-7/src/out/host_debug_unopt/vm_platform_strong.dill --filesystem-root=../../third_party/dart/ --filesystem-scheme=org-dartlang-kernel-service --no-aot --no-embed-sources --output=../../out/host_debug_unopt/gen/kernel_service.dill org-dartlang-kernel-service$:///pkg/vm/bin/kernel_service.dart
复制代码

上面的指令就是通过gen_kernel.dart,把kernel_service.dart文件编译成 kernel kernel_service.dill文件。

前两步和4.1一样

4.3 kDartCoreIsolateSnapshotData

依赖路径:isolate_snapshot_data.bin.oisolate_snapshot_data.bin.Sisolate_snapshot_data.binvm_platform_strong_stripped.dill

由于前两个流程同上不再累述,我们只讲后两个。

vm_platform_strong_stripped.dill:

对应指令:

../../third_party/dart/tools/sdks/dart-sdk/bin/dart --packages=../../third_party/dart/.packages --dfe=../../third_party/dart/tools/sdks/dart-sdk/bin/snapshots/kernel-service.dart.snapshot ../../third_party/dart/pkg/front_end/tool/_fasta/compile_platform.dart dart$:core -Ddart.vm.product=false -Ddart.developer.causal_async_stacks=true -Ddart.isVM=true --exclude-source --single-root-scheme=org-dartlang-sdk --single-root-base=../../third_party/dart/ org-dartlang-sdk$:///sdk/lib/libraries.json vm_outline_strong_stripped.dill vm_platform_strong_stripped.dill vm_outline_strong_stripped.dill
复制代码

上面的指令我们发现与vm_platform_strong.dill相比,多了一个--exclude-source。source其实就是源码,也就是说vm_platform_strong_stripped.dill 只有单纯的AST结构而没有源码索引。

isolate_snapshot_data.bin.S:

对应指令:

gen_snapshot --deterministic --snapshot_kind=core --vm_snapshot_data=gen/third_party/dart/runtime/bin/vm_snapshot_data.bin --vm_snapshot_instructions=gen/third_party/dart/runtime/bin/vm_snapshot_instructions.bin --isolate_snapshot_data=gen/third_party/dart/runtime/bin/isolate_snapshot_data.bin --isolate_snapshot_instructions=gen/third_party/dart/runtime/bin/isolate_snapshot_instructions.bin ../../out/host_debug_unopt/vm_platform_strong_stripped.dill
复制代码

注意这里不再是用bin_to_assembly.py来生成汇编文件,而是使用了gen_snapshot

输入:

  1. vm_platform_strong_stripped.dill(SDK内置库的kernel文件)

输出:

  1. vm_snapshot_data.bin(vm isolate的堆数据snapshot)
  2. vm_snapshot_instructions.bin(vm isolate的代码snapshot)
  3. isolate_snapshot_data.bin(主isolate的堆数据snapshot)
  4. isolate_snapshot_instructions.bin(主isolate的代码snapshot)

gen_snapshot跟Dart,都是可执行文件,它生成snapshot只有三步:

  1. 初始化一个不加载任何指令和数据的vm isolate
  2. 传入vm_platform_strong_stripped.dill,加载一个带上了内置库的主isolate
  3. 当isolate加载完成后,再从内存中把两份堆数据vm_snapshot_dataisolate_snapshot_data拷贝出来

是不是发现还少了两段Instructions数据?因为命令指定生成的snapshot类型是kernel,如果是AOT,会经过Dart_precompile后,生成两端平台相关的Instructions数据。

4.4 总结一下

  1. kPlatformStrongDill 是内置库dart:core的 kernel 代码
  2. kKernelServiceDill 是 kernel_service.dart 的 kernel 代码
  3. kDartVmSnapshotData, kDartVmSnapshotInstructions, kDartCoreIsolateSnapshotData, kDartCoreIsolateSnapshotInstructions 都来源于内置库,且是去掉source后的snapshot版本

在执行我们调试代码的时候,主isolate加载的是移除了source后的内置库,所以断点无法生效也是正常的。

五、构建一个带Source的Dart工具

知道大致原因后,接下来我们需要为snapshot带上source。我们打开toolchain.ninja文件,搜索下4.3的规则__third_party_dart_runtime_vm_vm_platform_stripped___build_toolchain_mac_clang_x64__rule,将 --exclude-source 参数删掉,重新跑ninja -C out/host_debug_unopt。 细心的同学会发现现在构建目录下的 vm_platform_strong_stripped.dillvm_platform_strong.dill 一样大小了。

六、用--observe调试Dart代码

--observe是Dart里的一个命令,它启动dart的内置调试器,并在网页上进行调试。不过我们需要修改下调试代码:

import 'dart:io';
import 'dart:developer';
void main() async {
    final client = HttpClient();    
    debugger();        
    final request = await client.getUrl(Uri.parse("https://www.baidu.com/"));
    final response = await request.close();    
}

复制代码

用刚构建出来的Dart执行命令: ./dart --observe ./test.dart

根据提示打开网页就会看到以下界面:

6.1 observe调试常用指令

  1. 设置断点:

    输入 break 8,即在当前文件的第八行设置断点

  2. 逐步调试:

    输入 s,执行下一条dart语句;

    输入 n,往下执行知道遇到下一个断点

6.2 如何把断点设置在sdk的代码上

我们发现observe提供的指令没有step into的选项,所以需要把具体文件名也一起传进去,这样我们就需要修改一点JS代码

  1. 修改 main.dart.js 文件

    首先我们需要让调试页面支持我们多传个参数,需要修改的文件位置在构建目录下 gen/third_party/dart/runtime/observatory/observatory/web/main.dart.js

    // 声明变量存文件名 
    // Line 25
    var breakPointScript = null;
    
    // 解析输入框的参数赋值
    // Line 42350
    if (args.length > 2) {
    	var element = args.pop();
    	breakPointScript = element;
    } else {
    	breakPointScript = null;
    }
    
    // 将breakPointScript传入dart 
    // Line 49555
    if (breakPointScript != null) {
    	params.$indexSet(0, "breakPointAtScript", breakPointScript);
    }
    
    // 注释掉原来的参数限制
    // Line 57745
    // if (t2) {
    //   t1.console.print$1(0, "line number must be in range [1.." + script.lines.length + "]");
    //   // goto return
    //   $async$goto = 1;
    //   break;
    // }
    
    // Line 57877
    // if (t2 < 1 || t2 > script.lines.length) {
    //   t1.console.print$1(0, "line number must be in range [1.." + script.lines.length + "]");
    //   // goto return
    //   $async$goto = 1;
    //   break;
    // }
    复制代码
  2. Dart工具解析JS传参

    参数解析的代码位置在:

    // service.cc
    static bool AddBreakpointCommon(Thread* thread, JSONStream* js, const String& script_uri) {
    	const char* breakPointAtScript = js->LookupParam("breakPointAtScript");
    }
    复制代码

6.3 修改Dart代码

理论上,接下来我们就可以结合调试界面进行调试了。但是Dart原本就没打算让我们调试内置库,所以它在加载内置库时,并没有加载内置库的Source。所以我们还需要在几个地方进行修改,具体的操作可以参考下面链接:

fab2e90eb54f5480d665411ba52d39af98010837

6.4 最终调试

现在,我们终于可以调试内置库了,在网页调试界面输入:

// 在http_impl.dart文件的2271行处设置断点
break 2271 org-dartlang-sdk:///sdk/lib/_http/http_impl.dart
复制代码

然后输出n继续执行,我们就可以看见它跳入我们期望的位置了:

七、总结

本文从构建入手描述将Dart文件如何被Dart命令行工具解析,执行,希望对有兴趣的同学有所帮助。我认为当一项新技术出现时,我们只有对其溯本求源,才能有更大的可能性,如果只是会用是远远不够的~



这篇关于教你如何调试DartSDK的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程