教你如何调试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" 复制代码
改完后,我们就可以执行构建了:
-
先执行
./flutter/tools/gn --unoptimized
生成工程Host工程; -
再执行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进行编译
代码里的 kPlatformStrongDill 是dart: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:core
sdk内置库也是提前被编成dill或者snapshot。下面我们会挨个弄清楚其数据来源。
我们可以先回到1.2的构建依赖界面,对其溯本求源。
4.1 kPlatformStrongDill
依赖路径:vm_platform_strong.dill.o
》vm_platform_strong.dill.S
》vm_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.dill
和vm_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.o
》 kernel_service.dill.S
》 kernel_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.o
》 isolate_snapshot_data.bin.S
》 isolate_snapshot_data.bin
》 vm_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
。
输入:
vm_platform_strong_stripped.dill
(SDK内置库的kernel文件)
输出:
vm_snapshot_data.bin
(vm isolate的堆数据snapshot)vm_snapshot_instructions.bin
(vm isolate的代码snapshot)isolate_snapshot_data.bin
(主isolate的堆数据snapshot)isolate_snapshot_instructions.bin
(主isolate的代码snapshot)
gen_snapshot
跟Dart,都是可执行文件,它生成snapshot只有三步:
- 初始化一个不加载任何指令和数据的vm isolate
- 传入
vm_platform_strong_stripped.dill
,加载一个带上了内置库的主isolate - 当isolate加载完成后,再从内存中把两份堆数据
vm_snapshot_data
和isolate_snapshot_data
拷贝出来
是不是发现还少了两段Instructions数据?因为命令指定生成的snapshot类型是kernel,如果是AOT,会经过
Dart_precompile
后,生成两端平台相关的Instructions数据。
4.4 总结一下
- kPlatformStrongDill 是内置库
dart:core
的 kernel 代码 - kKernelServiceDill 是
kernel_service.dart
的 kernel 代码 - 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.dill
和 vm_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调试常用指令
-
设置断点:
输入
break 8
,即在当前文件的第八行设置断点 -
逐步调试:
输入
s
,执行下一条dart语句;输入
n
,往下执行知道遇到下一个断点
6.2 如何把断点设置在sdk的代码上
我们发现observe提供的指令没有step into的选项,所以需要把具体文件名也一起传进去,这样我们就需要修改一点JS代码
-
修改 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; // } 复制代码
-
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的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值