1. 程式人生 > Android開發 >教你如何除錯DartSDK

教你如何除錯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,@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進行編譯

程式碼裡的 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,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: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命令列工具解析,執行,希望對有興趣的同學有所幫助。我認為當一項新技術出現時,我們只有對其溯本求源,才能有更大的可能性,如果只是會用是遠遠不夠的~