教你如何除錯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
複製程式碼
根據提示,我們可以在網頁上開啟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,@loader_path/../../.. -Wl,-pie -Xlinker -rpath -Xlinker @executable_path/Frameworks -o ./dart -Wl,./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,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,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,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命令列工具解析,執行,希望對有興趣的同學有所幫助。我認為當一項新技術出現時,我們只有對其溯本求源,才能有更大的可能性,如果只是會用是遠遠不夠的~