dotnet 簡單寫一個 pdb 符號檔案下載器

lindexi發表於2024-11-27

本文將以拉取 ntdll.dll 為例子告訴大家如何從 msdl.microsoft.com 下載符號

我先將自己電腦上的 ntdll.dll 複製到輸出路徑,方便我進行訪問。讀取 C 盤的 Windows 資料夾的 DLL 檔案也是有讀取許可權的,即不複製到輸出路徑也不會有任何的問題的

整體步驟是先讀取 dll 的 PE 檔案資訊,獲取到除錯資訊,即 PDB 名加 Guid 和 Age 引數,由以上三個引數拼接為下載地址

本文使用的 PEReader 等輔助類為 .NET Core 1.0 引入,如需在 .NET Framework 使用,請安裝 System.Reflection.Metadata

本文建立的是 dotnet 9 的控制檯檔案

先使用 File.OpenRead 開啟要讀取的 DLL 檔案,程式碼如下

var file = @"ntdll.dll";

using var fileStream = File.OpenRead(file);

接著放入到 PEReader 裡面讀取,如此即可非常方便完成對 PE 檔案的讀取,程式碼如下

var peReader = new PEReader(fileStream);

嘗試讀取傳入的 PE 檔案的除錯目錄資訊,程式碼如下

var debugDirectoryEntries = peReader.ReadDebugDirectory();

這裡的除錯目錄可能有很多,咱只需使用 DebugDirectoryEntryType.CodeView 型別的即可

foreach (var debugDirectoryEntry in debugDirectoryEntries)
{
    if (debugDirectoryEntry.Type != DebugDirectoryEntryType.CodeView)
    {
        continue;
    }

    ...
}

在 DebugDirectoryEntry 裡面存放的只是一些地址偏移和大小的資訊,需要進一步呼叫 ReadCodeViewDebugDirectoryData 方法讀取到更多資訊,程式碼如下

foreach (var debugDirectoryEntry in debugDirectoryEntries)
{
    if (debugDirectoryEntry.Type != DebugDirectoryEntryType.CodeView)
    {
        continue;
    }

    var readCodeViewDebugDirectoryData = peReader.ReadCodeViewDebugDirectoryData(debugDirectoryEntry);
    var path = readCodeViewDebugDirectoryData.Path;
    var guid = readCodeViewDebugDirectoryData.Guid;
    var age = readCodeViewDebugDirectoryData.Age;

    var pdbName = path;

    ...
}

如此即可獲取到下載地址拼接的資訊

    var downloadUrl = $"http://msdl.microsoft.com/download/symbols/{pdbName}/{(guid.ToString("N").ToUpperInvariant() + age.ToString())}/{pdbName}";

繼續使用 HttpClient 將其下載下來,程式碼如下

    var httpClient = new HttpClient();

    using var httpResponseMessage = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);

    var pdbFile = Path.GetFullPath(pdbName);
    await using var downloadFileStream = File.Create(pdbFile);
    await httpResponseMessage.Content.CopyToAsync(downloadFileStream);

下載檔案的方法十分簡單,先是準備要下載的檔案存放的路徑,我這裡使用的是相對工作路徑的方式,即 var pdbFile = Path.GetFullPath(pdbName); 簡單的方式拿到絕對路徑。這裡獲取絕對路徑僅僅只是為了方便除錯而已,無實際邏輯意義

接著使用 File.Create 方法建立檔案,且返回 FileStream 物件,方便進行下載內容的 CopyToAsync 寫入到檔案

以上程式碼的另一個細節是請求的時候帶上了 HttpCompletionOption.ResponseHeadersRead 引數,帶上這個引數將告訴 HttpClient 不要等待內容都拉取完了再讓 GetAsync 方法返回,而是隻要 http 的頭資訊拿到了,就應該返回了。這一點對下載檔案來說,比較有最佳化,大部分下載的檔案的檔案長度都不小,全等待下載完成再讓 GetAsync 返回,再複製到檔案,這個邏輯相對來說是比較虧的。只有對 API 呼叫形的後臺訪問,才合適使用預設的 HttpCompletionOption.ResponseContentRead 引數,全部完成再返回,如此可以更簡化異常處理情況,確保網路通訊完成再返回

透過上文簡單的方式即可完成對 DLL 的符號檔案下載

以上程式碼其實還隱藏了另一個功能,那就是自己組建符號伺服器,可以自己在構建完成之後,根據如上資訊,將 PDB 符號檔案存放到合適的路徑裡面或記錄到資料庫裡面,不依賴 symstore 工具

本文的 Program.cs 程式碼如下

using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using System.Text;

var file = @"ntdll.dll";

using var fileStream = File.OpenRead(file);

var peReader = new PEReader(fileStream);
var httpClient = new HttpClient();

var debugDirectoryEntries = peReader.ReadDebugDirectory();
foreach (var debugDirectoryEntry in debugDirectoryEntries)
{
    if (debugDirectoryEntry.Type != DebugDirectoryEntryType.CodeView)
    {
        continue;
    }

    var readCodeViewDebugDirectoryData = peReader.ReadCodeViewDebugDirectoryData(debugDirectoryEntry);
    var path = readCodeViewDebugDirectoryData.Path;
    var guid = readCodeViewDebugDirectoryData.Guid;
    var age = readCodeViewDebugDirectoryData.Age;

    var pdbName = path;

    var downloadUrl = $"http://msdl.microsoft.com/download/symbols/{pdbName}/{(guid.ToString("N").ToUpperInvariant() + age.ToString())}/{pdbName}";
    using var httpResponseMessage = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);

    var pdbFile = Path.GetFullPath(pdbName);
    await using var downloadFileStream = File.Create(pdbFile);
    await httpResponseMessage.Content.CopyToAsync(downloadFileStream);
}

本文程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 6bc0a171f53f4da8a968257c621c9df7a6de77a3

以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼。如果依然拉取不到程式碼,可以發郵件向我要程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 6bc0a171f53f4da8a968257c621c9df7a6de77a3

獲取程式碼之後,進入 Workbench/WerjiwaijogoNemcuryecho 資料夾,即可獲取到原始碼

更多技術部落格,請參閱 部落格導航

相關文章