除錯資訊遷移:從內建函式到記錄

我們計劃從 LLVM 中移除除錯資訊內建函式,因為它們速度慢、笨拙,並且如果最佳化過程沒有預期到它們,可能會造成混淆。與其使用如下所示的指令序列

    %add = add i32 %foo, %bar
    call void @llvm.dbg.value(metadata %add, ...
    %sub = sub i32 %add, %tosub
    call void @llvm.dbg.value(metadata %sub, ...
    call void @a_normal_function()

使用 dbg.value 內建函式表示除錯資訊記錄,它會被印成如下形式

    %add = add i32 %foo, %bar
      #dbg_value(%add, ...
    %sub = sub i32 %add, %tosub
      #dbg_value(%sub, ...
    call void @a_normal_function()

除錯記錄不是指令,不會出現在指令列表中,並且除非您刻意挖掘,否則它們不會出現在您的最佳化過程中。

太棒了,我需要做什麼呢?

我們已經大致完成了遷移。剩下的問題是,接下來,指令必須使用迭代器而不是指令指標插入到基本區塊中。在幾乎所有情況下,您都可以直接對指令指標呼叫 getIterator – 但是,如果您呼叫一個傳回基本區塊開頭的函式,例如

  1. BasicBlock::begin

  2. BasicBlock::getFirstNonPHIIt

  3. BasicBlock::getFirstInsertionPt

那麼您必須將該迭代器不經修改地傳遞到插入函式中(迭代器帶有一個除錯資訊位元)。就這樣!請繼續閱讀更詳細的說明。

API 變更

有兩個重要的變更需要注意。首先,我們正在向 BasicBlock::iterator 類別添加一個與除錯相關的資料位元(這是為了讓我們可以確定範圍是否打算在區塊的開頭包含除錯資訊)。這表示在編寫插入 LLVM IR 指令的 pass 時,您需要使用 BasicBlock::iterator 而不是僅僅使用裸指標 Instruction * 來識別位置。大多數時候,這表示在確定您打算插入內容的位置後,您還必須在指令位置上呼叫 getIterator – 但是,當在區塊的開頭插入時,您必須使用 getFirstInsertionPtgetFirstNonPHIItbegin,並使用該迭代器進行插入,而不是僅僅獲取指向第一個指令的指標。

第二件事是,如果您手動將指令序列從一個位置傳輸到另一個位置,例如重複使用 moveBefore,而您可能已經使用了 splice,那麼您應該改為使用方法 moveBeforePreservingmoveBeforePreserving 將會傳輸附加到指令的除錯資訊記錄。這是今天自動發生的事情 – 如果您在指令序列的每個元素上使用 moveBefore,那麼除錯內建函式將會在您的程式碼的正常過程中被移動,但是對於非指令除錯資訊,我們會失去這種行為。

有關如何更新現有程式碼以支援除錯記錄的更深入概述,請參閱下面的指南

文字 IR 變更

當我們從使用除錯內建函式變更為除錯記錄時,任何依賴解析 LLVM 產生的 IR 的工具都需要處理新的格式。在大多數情況下,除錯內建函式呼叫的列印形式與除錯記錄之間的差異很小

  1. 新增了額外的 2 個空格縮排。

  2. 文字 (tail|notail|musttail)? call void @llvm.dbg.<type> 被替換為 #dbg_<type>

  3. 前導的 metadata 從內建函式的每個引數中移除。

  4. DILocation 從格式為 !dbg !<Num> 的指令附件變更為普通引數,即 !<Num>,作為最後一個引數傳遞給除錯記錄。

遵循這些規則,我們有這個除錯內建函式和等效除錯記錄的範例

; Debug Intrinsic:
  call void @llvm.dbg.value(metadata i32 %add, metadata !10, metadata !DIExpression()), !dbg !20
; Debug Record:
    #dbg_value(i32 %add, !10, !DIExpression(), !20)

測試更新

任何在主要 LLVM 儲存庫下游測試 LLVM 的 IR 輸出的測試都可能因為變更為使用記錄而失敗。根據上面的更新規則,更新單個測試以預期記錄而不是內建函式應該是微不足道的。但是,更新許多測試可能會很麻煩;為了更新主要儲存庫中的 lit 測試,使用了以下步驟

  1. 將失敗的 lit 測試列表收集到單個檔案 failing-tests.txt 中,並以換行符分隔(並以換行符結尾)。

  2. 使用以下行將失敗的測試拆分為使用 update_test_checks 的測試和不使用的測試

    $ while IFS= read -r f; do grep -q "Assertions have been autogenerated by" "$f" && echo "$f" >> update-checks-tests.txt || echo "$f" >> manual-tests.txt; done < failing-tests.txt
    
  3. 對於使用 update_test_checks 的測試,執行適當的 update_test_checks 腳本 - 對於主要的 LLVM 儲存庫,這是通過以下方式實現的

    $ xargs ./llvm/utils/update_test_checks.py --opt-binary ./build/bin/opt < update-checks-tests.txt
    $ xargs ./llvm/utils/update_cc_test_checks.py --llvm-bin ./build/bin/ < update-checks-tests.txt
    
  4. 剩餘的測試可以手動更新,但是如果測試數量很大,則以下腳本可能會很有用;首先,一個用於從檔案中提取 check-line 前綴的腳本

    $ cat ./get-checks.sh
    #!/bin/bash
    
    # Always add CHECK, since it's more effort than it's worth to filter files where
    # every RUN line uses other check prefixes.
    # Then detect every instance of "check-prefix(es)=..." and add the
    # comma-separated arguments as extra checks.
    for filename in "$@"
    do
        echo "$filename,CHECK"
        allchecks=$(grep -Eo 'check-prefix(es)?[ =][A-Z0-9_,-]+' $filename | sed -E 's/.+[= ]([A-Z0-9_,-]+).*/\1/g; s/,/\n/g')
        for check in $allchecks; do
            echo "$filename,$check"
        done
    done
    

    然後是第二個腳本,用於執行實際更新每個失敗測試中的 check-line 的工作,並使用一系列簡單的替換模式

    $ cat ./substitute-checks.sh
    #!/bin/bash
    
    file="$1"
    check="$2"
    
    # Any test that explicitly tests debug intrinsic output is not suitable to
    # update by this script.
    if grep -q "write-experimental-debuginfo=false" "$file"; then
        exit 0
    fi
    
    sed -i -E -e "
    /(#|;|\/\/).*$check[A-Z0-9_\-]*:/!b
    /DIGlobalVariableExpression/b
    /!llvm.dbg./bpostcall
    s/((((((no|must)?tail )?call.*)?void )?@)?llvm.)?dbg\.([a-z]+)/#dbg_\7/
    :postcall
    /declare #dbg_/d
    s/metadata //g
    s/metadata\{/{/g
    s/DIExpression\(([^)]*)\)\)(,( !dbg)?)?/DIExpression(\1),/
    /#dbg_/!b
    s/((\))?(,) )?!dbg (![0-9]+)/\3\4\2/
    s/((\))?(, ))?!dbg/\3/
    " "$file"
    

    這兩個腳本結合起來可以用於 manual-tests.txt 中的列表,如下所示

    $ cat manual-tests.txt | xargs ./get-checks.sh | sort | uniq | awk -F ',' '{ system("./substitute-checks.sh " $1 " " $2) }'
    

    這些腳本成功處理了 clang/testllvm/test 中的絕大多數檢查。

  5. 驗證結果測試是否通過,並檢測任何失敗的測試

    $ xargs ./build/bin/llvm-lit -q < failing-tests.txt
    ********************
    Failed Tests (5):
    LLVM :: DebugInfo/Generic/dbg-value-lower-linenos.ll
    LLVM :: Transforms/HotColdSplit/transfer-debug-info.ll
    LLVM :: Transforms/ObjCARC/basic.ll
    LLVM :: Transforms/ObjCARC/ensure-that-exception-unwind-path-is-visited.ll
    LLVM :: Transforms/SafeStack/X86/debug-loc2.ll
    
    
    Total Discovered Tests: 295
    Failed: 5 (1.69%)
    
  6. 某些測試可能已失敗 - 更新腳本很簡單,並且不跨行保留上下文,因此在某些情況下它們將無法處理;剩餘的情況必須手動更新(或通過其他腳本處理)。

C-API 變更

已新增的一些新函式是暫時性的,將在未來被棄用。目的是它們將幫助下游專案在過渡期間適應。

Deleted functions
-----------------
LLVMDIBuilderInsertDeclareBefore   # Insert a debug record (new debug info format) instead of a debug intrinsic (old debug info format).
LLVMDIBuilderInsertDeclareAtEnd    # Same as above.
LLVMDIBuilderInsertDbgValueBefore  # Same as above.
LLVMDIBuilderInsertDbgValueAtEnd   # Same as above.

New functions (to be deprecated)
--------------------------------
LLVMIsNewDbgInfoFormat     # Returns true if the module is in the new non-instruction mode.
LLVMSetIsNewDbgInfoFormat  # Convert to the requested debug info format.

New functions (no plans to deprecate)
-------------------------------------
LLVMGetFirstDbgRecord                    # Obtain the first debug record attached to an instruction.
LLVMGetLastDbgRecord                     # Obtain the last debug record attached to an instruction.
LLVMGetNextDbgRecord                     # Get next debug record or NULL.
LLVMGetPreviousDbgRecord                 # Get previous debug record or NULL.
LLVMDIBuilderInsertDeclareRecordBefore   # Insert a debug record (new debug info format).
LLVMDIBuilderInsertDeclareRecordAtEnd    # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordBefore  # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordAtEnd   # Same as above. See info below.

LLVMPositionBuilderBeforeDbgRecords          # See info below.
LLVMPositionBuilderBeforeInstrAndDbgRecords  # See info below.

LLVMDIBuilderInsertDeclareRecordBeforeLLVMDIBuilderInsertDeclareRecordAtEndLLVMDIBuilderInsertDbgValueRecordBeforeLLVMDIBuilderInsertDbgValueRecordAtEnd 正在取代已刪除的 LLVMDIBuilderInsertDeclareBefore-style 函式。

LLVMPositionBuilderBeforeDbgRecordsLLVMPositionBuilderBeforeInstrAndDbgRecords 的行為與 LLVMPositionBuilderLLVMPositionBuilderBefore 相同,只是插入位置設定在目標指令之前的除錯記錄之前。請注意,這並不表示會跳過所選指令之前的除錯內建函式,而僅會跳過除錯記錄(與除錯記錄不同,除錯記錄本身不是指令)。

如果您不知道要呼叫哪個函式,請遵循此規則:如果您嘗試在區塊的開頭插入,或為了任何其他原因而有意跳過除錯內建函式以確定插入點,則呼叫新的函式。

LLVMPositionBuilderLLVMPositionBuilderBefore 沒有改變。它們在指示的指令之前插入,但在任何附加的除錯記錄之後插入。

LLVMGetFirstDbgRecordLLVMGetLastDbgRecordLLVMGetNextDbgRecordLLVMGetPreviousDbgRecord 可用於迭代附加到指令的除錯記錄(以 LLVMValueRef 的形式提供)。

LLVMDbgRecordRef DbgRec;
for (DbgRec = LLVMGetFirstDbgRecord(Inst); DbgRec;
     DbgRec = LLVMGetNextDbgRecord(DbgRec)) {
  // do something with DbgRec
}
LLVMDbgRecordRef DbgRec;
for (DbgRec = LLVMGetLastDbgRecord(Inst); DbgRec;
     DbgRec = LLVMGetPreviousDbgRecord(DbgRec)) {
  // do something with DbgRec
}

新的「除錯記錄」模型

以下是取代除錯內建函式的新表示法的簡要概述;有關更新舊程式碼的指導指南,請參閱此處

您究竟用什麼取代了除錯內建函式?

我們正在使用一個專用的 C++ 類別,名為 DbgRecord 來儲存除錯資訊,在任何 LLVM IR 程式中,每個除錯內建函式的實例與每個 DbgRecord 物件之間都存在一對一的關係;這些 DbgRecord 在 IR 中表示為非指令除錯記錄,如 [Source Level Debugging](project:SourceLevelDebugging.rst#Debug Records) 文件中所述。這個類別有一組子類別,它們儲存與除錯內建函式中儲存的資訊完全相同的資訊。每個子類別也幾乎具有完全相同的方法集,這些方法的行為方式相同

https://llvm.dev.org.tw/docs/doxygen/classllvm_1_1DbgRecord.html https://llvm.dev.org.tw/docs/doxygen/classllvm_1_1DbgVariableRecord.html https://llvm.dev.org.tw/docs/doxygen/classllvm_1_1DbgLabelRecord.html

這允許您將 DbgVariableRecord 視為 dbg.value/dbg.declare/dbg.assign 內建函式,在大多數時候,例如在泛型(自動參數)lambda 中,DbgLabelRecorddbg.label 也是如此。

這些 DbgRecords 如何融入指令流?

像這樣

                 +---------------+          +---------------+
---------------->|  Instruction  +--------->|  Instruction  |
                 +-------+-------+          +---------------+
                         |
                         |
                         |
                         |
                         v
                  +-------------+
          <-------+  DbgMarker  |<-------
         /        +-------------+        \
        /                                 \
       /                                   \
      v                                     ^
 +-------------+    +-------------+   +-------------+
 |  DbgRecord  +--->|  DbgRecord  +-->|  DbgRecord  |
 +-------------+    +-------------+   +-------------+

每個指令都有一個指向 DbgMarker 的指標(這將變成可選的),其中包含 DbgRecord 物件的列表。任何除錯記錄都不會出現在指令列表中。DbgRecord 具有指向其擁有的 DbgMarker 的父指標,並且每個 DbgMarker 都具有指向其擁有的指令的回指指標。

未顯示的是從 DbgRecord 到 Value/Metadata 層次結構的其他部分的連結:DbgRecord 子類別具有指向它們使用的 DIMetadata 的追蹤指標,並且 DbgVariableRecord 具有對儲存在 DebugValueUser 基底類別中的 Value 的參考。這指的是通過 TrackingMetadata 機制引用 ValueValueAsMetadata 物件。

各種除錯內建函式(value、declare、assign、label)都儲存在 DbgRecord 子類別中,其中 “RecordKind” 欄位區分 DbgLabelRecordDbgVariableRecord,並且 DbgVariableRecord 類別中的 LocationType 欄位進一步消除它可以表示的各種除錯變數內建函式的歧義。

如何更新現有程式碼

任何以某種方式與除錯內建函式交互的現有程式碼都需要更新為以相同方式與除錯記錄交互。在更新程式碼時,請記住以下幾個快速規則

  • 在迭代指令時,將看不到除錯記錄;要查找緊接在指令之前的除錯記錄,您需要迭代 Instruction::getDbgRecordRange()

  • 除錯記錄具有與除錯內建函式相同的介面,這表示任何對除錯內建函式進行操作的程式碼都可以輕鬆地應用於除錯記錄。此規則的例外情況是 InstructionCallInst 方法,這些方法在邏輯上不適用於除錯記錄,以及 isa/cast/dyn_cast 方法,這些方法被 DbgRecord 類別本身的方法取代。

  • 除錯記錄不能出現在也包含除錯內建函式的模組中;兩者是互斥的。由於除錯記錄是未來的格式,因此在新程式碼中應優先處理正確處理記錄。

  • 在不再支援內建函式之前,對於僅處理除錯內建函式且難以更新的程式碼,一個有效的臨時解決方案是使用 Module::setIsNewDbgInfoFormat 將模組轉換為內建函式格式,然後再轉換回來。

    • 這也可以使用類別 ScopedDbgInfoFormatSetter 在模組或單個函式的詞法範圍內執行

    void handleModule(Module &M) {
      {
        ScopedDbgInfoFormatSetter FormatSetter(M, false);
        handleModuleWithDebugIntrinsics(M);
      }
      // Module returns to previous debug info format after exiting the above block.
    }
    

以下是關於如何更新目前支援除錯內建函式的現有程式碼以支援除錯記錄的粗略指南。

建立除錯記錄

當啟用新格式時,除錯記錄將由 DIBuilder 類別自動建立。與指令一樣,也可以呼叫 DbgRecord::clone 來建立現有記錄的未附加副本。

跳過除錯記錄、忽略 Values 的除錯用途、穩定地計數指令等。

這一切都將透明地發生,而無需考慮!

for (Instruction &I : BB) {
  // Old: Skips debug intrinsics
  if (isa<DbgInfoIntrinsic>(&I))
    continue;
  // New: No extra code needed, debug records are skipped by default.
  ...
}

尋找除錯記錄

諸如 findDbgUsers 之類的工具現在具有一個可選引數,該引數將傳回引用 ValueDbgVariableRecord 記錄集。您應該能夠像處理內建函式一樣處理它們。

// Old:
  SmallVector<DbgVariableIntrinsic *> DbgUsers;
  findDbgUsers(DbgUsers, V);
  for (auto *DVI : DbgUsers) {
    if (DVI->getParent() != BB)
      DVI->replaceVariableLocationOp(V, New);
  }
// New:
  SmallVector<DbgVariableIntrinsic *> DbgUsers;
  SmallVector<DbgVariableRecord *> DVRUsers;
  findDbgUsers(DbgUsers, V, &DVRUsers);
  for (auto *DVI : DbgUsers)
    if (DVI->getParent() != BB)
      DVI->replaceVariableLocationOp(V, New);
  for (auto *DVR : DVRUsers)
    if (DVR->getParent() != BB)
      DVR->replaceVariableLocationOp(V, New);

在位置檢查除錯記錄

呼叫 Instruction::getDbgRecordRange() 以取得附加到指令的 DbgRecord 物件的範圍。

for (Instruction &I : BB) {
  // Old: Uses a data member of a debug intrinsic, and then skips to the next
  // instruction.
  if (DbgInfoIntrinsic *DII = dyn_cast<DbgInfoIntrinsic>(&I)) {
    recordDebugLocation(DII->getDebugLoc());
    continue;
  }
  // New: Iterates over the debug records that appear before `I`, and treats
  // them identically to the intrinsic block above.
  // NB: This should always appear at the top of the for-loop, so that we
  // process the debug records preceding `I` before `I` itself.
  for (DbgRecord &DR = I.getDbgRecordRange()) {
    recordDebugLocation(DR.getDebugLoc());
  }
  processInstruction(I);
}

這也可以通過函式 filterDbgVars 傳遞,以專門迭代 DbgVariableRecords,這些記錄更常用。

for (Instruction &I : BB) {
  // Old: If `I` is a DbgVariableIntrinsic we record the variable, and apply
  // extra logic if it is an `llvm.dbg.declare`.
  if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
    recordVariable(DVI->getVariable());
    if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
      recordDeclareAddress(DDI->getAddress());
    continue;
  }
  // New: `filterDbgVars` is used to iterate over only DbgVariableRecords.
  for (DbgVariableRecord &DVR = filterDbgVars(I.getDbgRecordRange())) {
    recordVariable(DVR.getVariable());
    // Debug variable records are not cast to subclasses; simply call the
    // appropriate `isDbgX()` check, and use the methods as normal.
    if (DVR.isDbgDeclare())
      recordDeclareAddress(DVR.getAddress());
  }
  // ...
}

處理個別除錯記錄

在大多數情況下,任何對除錯內建函式進行操作的程式碼都可以提取到模板函式或自動 lambda(如果尚未在其中),這可以應用於除錯內建函式和除錯記錄 - 儘管請記住主要例外情況是 isa/cast/dyn_cast 不適用於 DbgVariableRecord 類型。

// Old: Function that operates on debug variable intrinsics in a BasicBlock, and
// collects llvm.dbg.declares.
void processDbgInfoInBlock(BasicBlock &BB,
                           SmallVectorImpl<DbgDeclareInst*> &DeclareIntrinsics) {
  for (Instruction &I : BB) {
    if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
      processVariableValue(DebugVariable(DVI), DVI->getValue());
      if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
        Declares.push_back(DDI);
      else if (!isa<Constant>(DVI->getValue()))
        DVI->setKillLocation();
    }
  }
}

// New: Template function is used to deduplicate handling of intrinsics and
// records.
// An overloaded function is also used to handle isa/cast/dyn_cast operations
// for intrinsics and records, since those functions cannot be directly applied
// to DbgRecords.
DbgDeclareInst *DynCastToDeclare(DbgVariableIntrinsic *DVI) {
  return dyn_cast<DbgDeclareInst>(DVI);
}
DbgVariableRecord *DynCastToDeclare(DbgVariableRecord *DVR) {
  return DVR->isDbgDeclare() ? DVR : nullptr;
}

template<typename DbgVarTy, DbgDeclTy>
void processDbgVariable(DbgVarTy *DbgVar,
                       SmallVectorImpl<DbgDeclTy*> &Declares) {
    processVariableValue(DebugVariable(DbgVar), DbgVar->getValue());
    if (DbgDeclTy *DbgDeclare = DynCastToDeclare(DbgVar))
      Declares.push_back(DbgDeclare);
    else if (!isa<Constant>(DbgVar->getValue()))
      DbgVar->setKillLocation();
};

void processDbgInfoInBlock(BasicBlock &BB,
                           SmallVectorImpl<DbgDeclareInst*> &DeclareIntrinsics,
                           SmallVectorImpl<DbgVariableRecord*> &DeclareRecords) {
  for (Instruction &I : BB) {
    if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I))
      processDbgVariable(DVI, DeclareIntrinsics);
    for (DbgVariableRecord *DVR : filterDbgVars(I.getDbgRecordRange()))
      processDbgVariable(DVR, DeclareRecords);
  }
}

移動和刪除除錯記錄

您可以使用 DbgRecord::removeFromParent 從其標記中取消連結 DbgRecord,然後使用 BasicBlock::insertDbgRecordBeforeBasicBlock::insertDbgRecordAfterDbgRecord 重新插入到其他位置。您不能在 DbgRecord 列表中的任意點插入 DbgRecord(如果您使用 llvm.dbg.value 執行此操作,則不太可能是正確的)。

通過呼叫 eraseFromParent 來刪除 DbgRecord

// Old: Move a debug intrinsic to the start of the block, and delete all other intrinsics for the same variable in the block.
void moveDbgIntrinsicToStart(DbgVariableIntrinsic *DVI) {
  BasicBlock *ParentBB = DVI->getParent();
  DVI->removeFromParent();
  for (Instruction &I : ParentBB) {
    if (auto *BlockDVI = dyn_cast<DbgVariableIntrinsic>(&I))
      if (BlockDVI->getVariable() == DVI->getVariable())
        BlockDVI->eraseFromParent();
  }
  DVI->insertBefore(ParentBB->getFirstInsertionPt());
}

// New: Perform the same operation, but for a debug record.
void moveDbgRecordToStart(DbgVariableRecord *DVR) {
  BasicBlock *ParentBB = DVR->getParent();
  DVR->removeFromParent();
  for (Instruction &I : ParentBB) {
    for (auto &BlockDVR : filterDbgVars(I.getDbgRecordRange()))
      if (BlockDVR->getVariable() == DVR->getVariable())
        BlockDVR->eraseFromParent();
  }
  DVR->insertBefore(ParentBB->getFirstInsertionPt());
}

懸空的除錯記錄呢?

如果您有像這樣的區塊

    foo:
      %bar = add i32 %baz...
      dbg.value(metadata i32 %bar,...
      br label %xyzzy

您的最佳化 pass 可能希望刪除終結符,然後對區塊執行某些操作。當除錯資訊保留在指令中時,這很容易做到,但是使用 DbgRecord,一旦終結符被刪除,則在上面的區塊中沒有尾隨指令可以附加變數資訊。對於這種退化區塊,DbgRecord 暫時儲存在 LLVMContext 中的映射中,並且當終結符重新插入到區塊或在 end() 插入其他指令時,會重新插入。

從技術上講,這可能會在極其罕見的情況下導致問題,即最佳化 pass 刪除終結符,然後決定刪除整個區塊。(我們建議不要這樣做)。

還有其他嗎?

上面的指南並未全面涵蓋可能適用於除錯內建函式的每種模式;如指南開頭所述,您可以臨時將目標模組從除錯記錄轉換為內建函式作為權宜之計。可以在除錯內建函式上執行的大多數操作都具有除錯記錄的完全對應項,但是如果您遇到任何例外情況,閱讀類別文件(連結此處)可能會提供一些見解,現有程式碼庫中可能有一些範例,您始終可以在論壇上尋求幫助。