模擬.NET應用場景,綜合應用反編譯、第三方庫除錯、攔截、一庫多版本相容方案

Dotnet9個人部落格發表於2023-09-26

免責宣告

使用者本人對於傳播和利用本公眾號提供的資訊所造成的任何直接或間接的後果和損失負全部責任。公眾號及作者對於這些後果不承擔任何責任。如果造成後果,請自行承擔責任。謝謝!

大家好,我是沙漠盡頭的狼。

本文首發於Dotnet9,結合前面兩篇(如何在沒有第三方.NET庫原始碼的情況下除錯第三庫程式碼?攔截、篡改、偽造.NET類庫中不限於public的類和方法),本文將設計一個案例,手把手地帶大家應用這兩篇文章中涉及的技能,並介紹一種支援多個版本的庫的相容性解決方案(涉及第三方庫的反編譯和強簽名)。

本文的目錄如下:

  1. 前言
  2. 案例設計
  3. 使用dnSpy進行除錯
  4. 使用Lib.Harmony攔截
  5. 引入高版本Lib.Harmony:支援多個版本的庫的相容性使用
  6. 總結

1. 前言

技術的存在即合理,關鍵在於如何使用。在前面的文章中,有讀者留言:

Lib.Harmony似乎不是一個正經的庫,有什麼合法的場景需要使用它嗎?

站長回答:非常正經。當你使用一個第三方庫,並且確定了版本並已經上線,有時候不能隨意升級第三方庫,因為可能存在潛在的風險。這時,你只能修改自己的程式碼,而不動第三方庫。

還有讀者說得很有道理:

這個工具非常強大,但有時也很可怕。

既然讀者有疑問,所以我寫了這篇文章,儘量模擬一個看起來比較實際的應用場景。你可以跟著做一做,看看這個工具到底是不是正經的。本文提供了詳細的手把手教程。

2. 案例設計

這是一個小動畫遊戲,我已經將其釋出到NuGet上:Dotnet9Games。在這個小動畫遊戲中,我設定了兩個陷阱。我們將按照我的步驟一一解決這些問題。首先,我們建立一個.NET Framework 4.6.1的WPF空專案【Dotnet9Playground】。我認為大部分人都會使用這個版本的桌面應用程式,如果不是,請在評論中告訴我。

2.1. 引入Dotnet9Games包

我已經將製作好的(虛構的)遊戲釋出在[NuGet](NuGet Gallery | Dotnet9Games 1.0.2)上作為第三方包使用。為了模擬一個比較真實的場景,直接安裝最新版本即可:

2.2. 新增目標遊戲

開啟MainWindow.xaml,引入Dotnet9Games名稱空間:

xmlns:dotnet9="https://dotnet9.com"

MainWindow.xaml完整程式碼如下:

<Window
    x:Class="Dotnet9Playground.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:dotnet9="https://dotnet9.com"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="綜合小案例:模擬.NET應用場景,綜合應用反編譯、第三方庫除錯、攔截、一庫多版本相容"
    Width="800"
    Height="450"
    Background="Bisque"
    Icon="Resources/favicon.ico"
    mc:Ignorable="d">
    <Border Padding="10">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="40" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <StackPanel
                Grid.Row="0"
                VerticalAlignment="Center"
                Orientation="Horizontal">
                <TextBlock
                    VerticalAlignment="Center"
                    FontSize="20"
                    Foreground="Blue"
                    Text="生成" />
                <TextBox
                    x:Name="TextBoxBallCount"
                    Width="50"
                    Height="25"
                    Margin="10,0"
                    VerticalAlignment="Center"
                    HorizontalContentAlignment="Center"
                    VerticalContentAlignment="Center"
                    FontSize="20"
                    Foreground="Red"
                    Text="{Binding ElementName=MyBallGame, Path=BallCount, Mode=TwoWay}" />
                <TextBlock
                    Margin="0,0,10,0"
                    VerticalAlignment="Center"
                    FontSize="20"
                    Foreground="Blue"
                    Text="個氣球,點選" />
                <Button
                    Padding="15,2"
                    Background="White"
                    BorderBrush="DarkGreen"
                    BorderThickness="2"
                    Click="StartGame_OnClick"
                    Content="開始遊戲"
                    FontSize="20"
                    Foreground="DarkOrange" />
            </StackPanel>
            <dotnet9:BallGame
                x:Name="MyBallGame"
                Grid.Row="1"
                BallCount="8" />
        </Grid>
    </Border>
</Window>

MainWindow.xaml.cs程式碼如下:

using System.Windows;

namespace Dotnet9Playground;

/// <summary>
///     綜合小案例:模擬.NET應用場景,綜合應用反編譯、第三方庫除錯、攔截、一庫多版本相容
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void StartGame_OnClick(object sender, RoutedEventArgs e)
    {
        MyBallGame.StartGame();
    }
}

準備操作完成,執行程式:

這個遊戲比較簡單,主要包含以下幾個步驟:

  1. 在主介面提供一個文字輸入框,用於填寫生成的氣球個數。可以透過資料繫結將文字框的值繫結到遊戲的BallCount屬性。
  2. 提供一個開始遊戲按鈕,點選按鈕後會觸發MyBallGame.StartGame()方法,用於生成氣球並播放動畫。

2.3. 引入第一個陷阱

氣球生成8個可能太少了,讓我們來生成80個氣球吧:

怎麼彈出一個紅色的大圓,氣球都消失了?這就是陷阱!

3. 使用dnSpy進行除錯

3.1. 分析

輸入80個氣球后,我們點選開始遊戲是呼叫了遊戲的方法StartGame(), 我們開啟[dnSpy](Releases · dnSpyEx/dnSpy (github.com))(這個連結提供32位和64位下載連結),拖入Dotnet9Games.dll,找到該方法程式碼:

// Token: 0x06000022 RID: 34 RVA: 0x000022AC File Offset: 0x000004AC
public void StartGame()
{
    bool flag = this.BallCount > 9;
    if (flag)
    {
        this.PlayBrokenHeartAnimation();
    }
    else
    {
        this.GenerateBalloons();
    }
}

原來是當氣球個數多於9個時呼叫了PlayBrokenHeartAnimation()方法,這個方法幹啥的呢?看程式碼:

大致看出來了嗎?首先是清空氣球控制元件,然後又新增了一個紅色的圓動畫,我們除錯試試呢?

3.2. 除錯驗證

大致說下步驟:

  1. StartGame()方法第一行打上斷點;
  2. 點選dnSpy【啟動】按鈕;
  3. 在彈出的【除錯程式】介面裡,"除錯引擎"預設選擇.NET Framework,"可執行程式"選擇我們的WPF主程式Exe【Dotnet9Playground.exe】,再點選【確定】即將WPF程式執行起來了;
  4. 主程式介面氣球個數輸入超過9個,比如80?
  5. 點選“開始遊戲”按鈕;
  6. 進入斷點了,除錯看看,真的進入PlayBrokenHeartAnimation()方法

4. 使用Lib.Harmony攔截

明白了原因,我們使用Lib.Harmony攔截StartGame()方法。

4.1. 安裝Lib.Harmony包

我們安裝最低版本1.2.0.1

為啥是安裝最低版本?

為了後面引入一庫多版本相容需求,低版本的Lib.Harmony有Bug,我們繼續,哈哈。

4.2. 編寫攔截類

新增攔截類“/Hooks/HookBallGameStartGame.cs”:

using Dotnet9Games.Views;
using Harmony;
using System.Reflection;

namespace Dotnet9Playground.Hooks;

internal class HookBallGameStartGame
{
    /// <summary>
    /// 攔截遊戲的開始方法StartGame
    /// </summary>
    public static void StartHook()
    {
        var harmony = HarmonyInstance.Create("https://dotnet9.com/HookBallGameStartGame");
        var hookClassType = typeof(BallGame);
        var hookMethod =
            hookClassType!.GetMethod(nameof(BallGame.StartGame), BindingFlags.Public | BindingFlags.Instance);
        var replaceMethod = typeof(HookBallGameStartGame).GetMethod(nameof(HookStartGame));
        var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
        harmony.Patch(hookMethod, replaceHarmonyMethod);
    }

    /// <summary>
    /// StartGame替換方法
    /// </summary>
    /// <param name="__instance">BallGame例項</param>
    /// <returns></returns>
    public static bool HookStartGame(ref object __instance)
    {
        #region 原方法原始碼

        //if (BallCount > 9)
        //{
        //    // 播放爆炸動畫效果
        //    PlayExplosionAnimation();
        //}
        //else
        //{
        //    // 生成彩色氣球
        //    GenerateBalloons();
        //}

        #endregion

        #region 攔截替換方法邏輯

        // 1、刪除氣球個數限制邏輯
        // 2、生成氣球方法為private修飾,我們透過反射呼叫
        var instanceType = __instance.GetType();
        var hookGenerateBalloonsMethod =
            instanceType.GetMethod("GenerateBalloons", BindingFlags.Instance | BindingFlags.NonPublic);

        // 生成彩色氣球
        hookGenerateBalloonsMethod!.Invoke(__instance, null);

        #endregion

        return false;
    }
}

上面的程式碼加了相關的註釋,這裡再提一提:

  • StartHook()方法用於關聯被攔截方法StartGame與攔截替換方法HookStartGame
  • HookStartGame是攔截替換方法,方法中註釋的程式碼為原方法邏輯程式碼;
  • 替換程式碼你可以將氣球個數改大一點,或者像站長一樣直接不要if (BallCount > 9)判斷,改為直接呼叫氣球生成方法GenerateBalloons

App.xaml.cs註冊上面的攔截類:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // 攔截氣球動畫播放方法
        HookBallGameStartGame.StartHook();
    }
}

現在再執行WPF程式,我們把氣球個數改為80個,正常生成了:

4.3. 就這樣?No,再來一陷阱

看著氣球在動,我們縮放下窗體大小(這裡建議Debug下嘗試,因為程式會崩潰,導致作業系統會卡那麼一小會兒):

程式異常了,再截圖看看:

貼上異常程式碼:

/// <summary>
/// 重寫MeasureOverride方法,引出Size引數為負數異常
/// </summary>
/// <param name="constraint"></param>
/// <returns></returns>
protected override Size MeasureOverride(Size constraint)
{
    // 計算最後一個元素寬度,不需要關注為什麼這樣寫,只是為了引出Size異常使得

    var lastChild = _balloons.LastOrDefault();
    if (lastChild != null)
    {
        var remainWidth = ActualWidth;
        foreach (var balloon in _balloons)
        {
            remainWidth -= balloon.Shape.Width;
        }

        lastChild.Shape.Measure(new Size(remainWidth, lastChild.Shape.Height));
    }

    return base.MeasureOverride(constraint);
}

分析

  • 在拖動窗體大小時,遊戲使用者控制元件BallGameMeasureOverride方法會觸發,對佈局進行重新計算;
  • 方法內邏輯:
    1. 如果存在一個運動的氣球,那麼計算BallGame的實際寬度減去所有子氣球的寬度之間的差,得到remainWidth;
    2. 使用remainWidth重新計算最後一個氣球的大小;
    3. remainWidth在做減法操作,那麼氣球個數足夠多,以致於遊戲控制元件寬度小於這些氣球寬之和時,就會為負數;
    4. 我們再看看Size建構函式程式碼(如果你用的VS,這裡推薦大家安裝ReSharper,十分方便的檢視引用庫方法 ),如下截圖:

程式碼複製過來看:

  /// <summary>Implements a structure that is used to describe the <see cref="T:System.Windows.Size" /> of an object. </summary>
  [TypeConverter(typeof (SizeConverter))]
  [ValueSerializer(typeof (SizeValueSerializer))]
  [Serializable]
  public struct Size : IFormattable
  {
    // 這裡省略N多程式碼
    /// <summary>Initializes a new instance of the <see cref="T:System.Windows.Size" /> structure and assigns it an initial <paramref name="width" /> and <paramref name="height" />.</summary>
    /// <param name="width">The initial width of the instance of <see cref="T:System.Windows.Size" />.</param>
    /// <param name="height">The initial height of the instance of <see cref="T:System.Windows.Size" />.</param>
    public Size(double width, double height)
    {
      this._width = width >= 0.0 && height >= 0.0 ? width : throw new ArgumentException(MS.Internal.WindowsBase.SR.Get("Size_WidthAndHeightCannotBeNegative"));
      this._height = height;
    }
    // 這裡省略N多程式碼
  }

當寬高為負數時會丟擲異常,這就能理解了,我們再使用Lib.Harmony攔截BallGameMeasureOverride方法,如法炮製。

新增/Hooks/HookBallgameMeasureOverride.cs類攔截:

using Dotnet9Games.Views;
using Harmony;
using System.Reflection;

namespace Dotnet9Playground.Hooks;

/// <summary>
/// 攔截BallGame的MeasureOverride方法
/// </summary>
internal class HookBallgameMeasureOverride
{
    /// <summary>
    /// 攔截遊戲的MeasureOverride方法
    /// </summary>
    public static void StartHook()
    {
        var harmony = HarmonyInstance.Create("https://dotnet9.com/HookBallgameMeasureOverride");
        var hookClassType = typeof(BallGame);
        var hookMethod = hookClassType!.GetMethod("MeasureOverride", BindingFlags.NonPublic | BindingFlags.Instance);
        var replaceMethod = typeof(HookBallgameMeasureOverride).GetMethod(nameof(HookMeasureOverride));
        var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
        harmony.Patch(hookMethod, replaceHarmonyMethod);
    }

    /// <summary>
    /// MeasureOverride替換方法
    /// </summary>
    /// <param name="__instance">BallGame例項</param>
    /// <returns></returns>
    public static bool HookMeasureOverride(ref object __instance)
    {
        // 暫時不做任何處理,返回false表示
        return false;
    }
}

再在App.xaml.cs新增攔截註冊:

using Dotnet9Playground.Hooks;
using System.Windows;

namespace Dotnet9Playground
{
    /// <summary>
    /// App.xaml 的互動邏輯
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            // 攔截氣球動畫播放方法
            HookBallGameStartGame.StartHook();

            // 這是第二個攔截方法:攔截氣球MeasureOverride方法
            HookBallgameMeasureOverride.StartHook();
        }
    }
}

再執行程式:

攔截方法進入了斷點,但無法獲取BallGame的例項,提示無法讀取記憶體,攔截方法返回False(不執行原方法)有下面的異常:

這時程式異常退出,我們將攔截方法返回True(繼續執行原方法),又有提示:

因為繼續執行原方法,取最後一個氣球方法又報錯var lastChild = _balloons.LastOrDefault();,好無奈呀,心酸。

經過公司專家指點:

因為Size是個結構體指標,0Harmony 1.2.0.1版本把指標當成4位,但“我們的程式”是64位,指標是8位,所有記憶體錯了。

好,那我們使用高版本Lib.Harmony

5. 引入高版本Lib.Harmony:支援多個版本的庫的相容性使用

5.1. 新建立工程引入高版本Lib.Harmony

理由

有可能程式中使用低版本的Lib.Harmony庫做了不少攔截操作,貿然全部升級,測試不到位,容易出現程式大崩潰(當前本程式只加了一個HookBallGameStartGame攔截類),而工程Dotnet9Playground直接引入同一個庫多版本無法實現(網友如果有建議歡迎留言)。

新增新類庫“Dotnet9HookHigh”,並使用NuGet安裝2.2.2穩定最新版Lib.Harmony庫:

同時也新增Dotnet9GamesNuGet包,將前面新增的HookBallgameMeasureOverride類剪下到該庫,Lib.Harmony高版本用法與低版本有所區別,在程式碼中有註釋,注意對比,升級後的HookBallgameMeasureOverride類定義:

using Dotnet9Games.Views;
using HarmonyLib;
using System.Reflection;

namespace Dotnet9HookHigh;

/// <summary>
/// 攔截BallGame的MeasureOverride方法
/// </summary>
public class HookBallgameMeasureOverride
{
    /// <summary>
    /// 攔截遊戲的MeasureOverride方法
    /// </summary>
    public static void StartHook()
    {
        //var harmony =  HarmonyInstance.Create("https://dotnet9.com/HookBallgameMeasureOverride");
        // 上面是低版本Harmony例項獲取程式碼,下面是高版本
        var harmony =  new Harmony("https://dotnet9.com/HookBallgameMeasureOverride");
        var hookClassType = typeof(BallGame);
        var hookMethod = hookClassType!.GetMethod("MeasureOverride", BindingFlags.NonPublic | BindingFlags.Instance);
        var replaceMethod = typeof(HookBallgameMeasureOverride).GetMethod(nameof(HookMeasureOverride));
        var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
        harmony.Patch(hookMethod, replaceHarmonyMethod);
    }

    /// <summary>
    /// MeasureOverride替換方法
    /// </summary>
    /// <param name="__instance">BallGame例項</param>
    /// <returns></returns>
    public static bool HookMeasureOverride(ref object __instance)
    {
        return false;
    }
}

區別如下圖,Harmony例項獲取程式碼有變化,其它不變:

主工程Dotnet9Playground新增Dotnet9HookHigh工程的引用,App.xaml.cs中新增引用HookBallgameMeasureOverride名稱空間:using Dotnet9HookHigh;,程式碼如下:

using Dotnet9HookHigh;
using Dotnet9Playground.Hooks;
using System.Windows;

namespace Dotnet9Playground
{
    /// <summary>
    /// App.xaml 的互動邏輯
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            // 攔截氣球動畫播放方法
            HookBallGameStartGame.StartHook();

            // 這是第二個攔截方法:攔截氣球MeasureOverride方法
            HookBallgameMeasureOverride.StartHook();
        }
    }
}

這就完了?執行試試:

這提示是指我的新工程Dotnet9HookHigh未成功應用高版本Lib.Harmony(2.2.2),亦指主工程Dotnet9Playground未成功識別載入高版本Lib.Harmony,怎麼辦?看我接下來的表演!

5.2. 高低版本的庫分目錄存放

5.2.1. 分析程式輸出目錄

程式輸出目錄只有一個0Harmony.dll,高低2個版本應該是兩個庫才對,怎麼辦?

5.2.2. 新建立目錄

低版本不變(存在位置依然放輸出目錄的根目錄),為了相容,我們把高版本改目錄存放,比如:Lib/Lib.Harmony/2.2.2/0Harmony.dll,將庫按目錄結構存放在工程Dotnet9HookHigh中:

  • 並將0Harmony.dll的屬性【複製到輸出目錄】設定為【如果較新則複製】
  • 刪除Dotnet9HookHighLib.Harmony庫的NuGet引用,改為本地引用(原來的配方,瀏覽本地路徑的方式);

這就完了嗎?咋還是報那個錯?

5.3. 同庫多版本配置

5.3.1. App.config配置多版本

修改Dotnet9PalygroundApp.config檔案,新增0Harmony.dll兩個版本及讀取位置:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
    </startup>
	<runtime>
		<assemblyBinding>
			<dependentAssembly>
				<assemblyIdentity name="0Harmony"
				  publicKeyToken="null"/>
				<codeBase version="1.2.0.1" href="0Harmony.dll" />
			</dependentAssembly>
			<dependentAssembly>
				<assemblyIdentity name="0Harmony"
				  publicKeyToken="null"/>
				<codeBase version="2.2.2.0" href="Lib\Lib.Harmony\2.2.2\0Harmony.dll" />
			</dependentAssembly>
		</assemblyBinding>
	</runtime>
</configuration>

再執行,還是報上面的錯?啊,我要暈了。。。。

5.3.2. 重點:庫的強簽名

上面分目錄、配置檔案版本配置目錄也還不夠,主工程還是無法區分兩個版本的Lib.Harmony庫,這裡涉及.NET 庫強簽名,就是上面App.config配置中的publicKeyToken特性,加上這個主程式就認識了,關於強簽名網上找到個說明[《.Net程式集強簽名詳解》](.Net程式集強簽名詳解_51CTO部落格_.net 簽名):

  1. 可以將強簽名的dll註冊到GAC,不同的應用程式可以共享同一dll。

  2. 強簽名的庫,或者應用程式只能引用強簽名的dll,不能引用未強簽名的dll,但是未強簽名的dll可以引用強簽名的dll。

  3. 強簽名無法保護原始碼,強簽名的dll是可以被反編譯的。

  4. 強簽名的dll可以防止第三方惡意篡改。

這裡,對於1.2.0.1版本的0Harmony.dll庫我們依然不動,只對2.2.2高版本做強簽名處理,簽名步驟參考[VS2008版本引入第三方dll無強簽名],我們來一起做一遍,這裡會藉助Everything軟體搜尋使用到的命令程式,建議提前下載。

注意:暫時不要用最新預覽版2.3.0-prerelease.2,站長做這個示例簽名用這個版本花了2個晚上沒成功,換成2.2.2就可以,下面的圖也重新錄了,可能該版本有其他依賴的緣故,只是猜測:

  1. 建立一個新的隨機金鑰對0Harmony.snk

使用Everything查詢一個sn.exe程式,隨便使用一個,比如:"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe",在高版本目錄下生成一個金鑰對檔案0Harmony.snk,命令如下:

"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe" -k "F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.snk"

  1. 反編譯0Harmony.dll

查詢ildasm.exe,比如C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\ildasm.exe,執行以下命令生成0Harmony.dll的il中間檔案:

"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\ildasm.exe" "F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.dll" /out="F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.il"

  1. 重新編譯,附帶強命名引數

查詢ilasm.exe,比如C:\Windows\Microsoft.NET\Framework64\v2.0.50727\ilasm.exe,執行以下命令做簽名:

"C:\Windows\Microsoft.NET\Framework64\v2.0.50727\ilasm.exe" "F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.il" /dll /resource="F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.res" /key="F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.snk" /optimize

  1. 驗證簽名資訊
"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe" -v "F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.dll" 

也可將生成的dll拖入dnSpy檢視:

做為對比,檢視NuGet下載的Lib.Harmony是沒做簽名的:

我們將簽名補充進App.Config檔案。

注意:因為我們使用的隨機金鑰對,所以您生成的簽名和我的肯定不一樣:

再除錯,能正常攔截MeasureOverride方法了,傳入的例項也能正常顯示BallGame(就這?對,我搞了2個晚上。。。。):

5.4. 一切就緒,完善最後一個攔截

程式碼如下:

using Dotnet9Games.Views;
using HarmonyLib;
using System.Collections;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;

namespace Dotnet9HookHigh;

/// <summary>
/// 攔截BallGame的MeasureOverride方法
/// </summary>
public class HookBallgameMeasureOverride
{
    /// <summary>
    /// 攔截遊戲的MeasureOverride方法
    /// </summary>
    public static void StartHook()
    {
        //var harmony =  HarmonyInstance.Create("https://dotnet9.com/HookBallgameMeasureOverride");
        // 上面是低版本Harmony例項獲取程式碼,下面是高版本
        var harmony = new Harmony("https://dotnet9.com/HookBallgameMeasureOverride");
        var hookClassType = typeof(BallGame);
        var hookMethod = hookClassType!.GetMethod("MeasureOverride", BindingFlags.NonPublic | BindingFlags.Instance);
        var replaceMethod = typeof(HookBallgameMeasureOverride).GetMethod(nameof(HookMeasureOverride));
        var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
        harmony.Patch(hookMethod, replaceHarmonyMethod);
    }

    /// <summary>
    /// MeasureOverride替換方法
    /// </summary>
    /// <param name="__instance">BallGame例項</param>
    /// <returns></returns>
    public static bool HookMeasureOverride(ref object __instance)
    {
        #region 原方法程式碼邏輯

        //// 計算最後一個元素寬度,不需要關注為什麼這樣寫,只是為了引出Size異常使得

        //var lastChild = _balloons.LastOrDefault();
        //if (lastChild != null)
        //{
        //    var remainWidth = ActualWidth;
        //    foreach (var balloon in _balloons)
        //    {
        //        remainWidth -= balloon.Shape.Width;
        //    }

        //    lastChild.Shape.Measure(new Size(remainWidth, lastChild.Shape.Height));
        //}

        //return base.MeasureOverride(constraint);

        #endregion

        #region 攔截替換程式碼

        var instanceType = __instance.GetType();
        var balloonsField = instanceType.GetField("_balloons", BindingFlags.NonPublic | BindingFlags.Instance);
        var balloons = (IEnumerable)balloonsField!.GetValue(__instance);

        var lastChild = balloons.Cast<object>().LastOrDefault();
        if (lastChild == null)
        {
            return false;
        }

        var remainWidth = ((UserControl)__instance).ActualWidth;
        foreach (object balloon in balloons)
        {
            remainWidth -= GetBalloonSize(balloon).Width;
        }

        // 注意:關鍵程式碼在這,如果剩餘寬度大於0才重新計算最後一個子項大小
		// 這段程式碼可能沒什麼意義,可按實際開發修改
        if (remainWidth > 0)
        {
            var lashShape = GetBalloonShape(lastChild);
            lashShape.Measure(new Size(remainWidth, lashShape.Height));
        }

        #endregion

        return false;
    }

    private static Ellipse GetBalloonShape(object balloon)
    {
        var shapeProperty = balloon.GetType().GetProperty("Shape");
        var shape = (Ellipse)shapeProperty!.GetValue(balloon);
        return shape;
    }

    private static Size GetBalloonSize(object balloon)
    {
        var shape = GetBalloonShape(balloon);
        return new Size(shape.Width, shape.Height);
    }
}

其中關鍵程式碼是:

// 注意:關鍵程式碼在這,如果剩餘寬度大於0才重新計算最後一個子項大小
// 這段程式碼可能沒什麼意義,可按實際開發修改
if (remainWidth > 0)
{
    var lashShape = GetBalloonShape(lastChild);
    lashShape.Measure(new Size(remainWidth, lashShape.Height));
}

其他程式碼就是反射的使用,不再細說,我們執行程式,現在隨便縮放窗體了:

當剩餘寬度小於0時跳過計算最後一個子項大小

5.4. 小最佳化

上面部分截圖中可能您也看到了0Harmony.ref檔案,我們簡單說說。

Git一般是配置成不能上傳可執行程式或dll檔案的,但多版本dll特殊,部分庫不能直接從NuGet引用,所以本文中的高版本Lib.Harmony庫只能使用自己強簽名版本,我們將dll副檔名改為“.ref"以允許上傳,他人能正常使用,程式如果需要正常編譯、生成,則給Dotnet9HookHigh工程新增生成前命令列,即生成時將.ref複製一份為.dll

copy "$(ProjectDir)Lib\Lib.Harmony\2.2.2\0Harmony.ref" "$(ProjectDir)Lib\Lib.Harmony\2.2.2\0Harmony.dll"

6. 總結

  • 技術交流加群請新增站長微訊號:dotnet9com
  • 文中示例程式碼:MultiVersionLibrary

文中案例寫的一般,特別是第二個陷阱,有興趣可以閱讀遊戲相關程式碼,提PR大家一起切磋,把這個案例寫的更合理、更有趣、更好玩一點,能讓第二個陷阱寫一些好玩的特效,攔截後實現不同的效果,這才是攔截的樂趣。

本文透過一個模擬實際案例,幫助大家應用前兩篇文章中涉及的技能(dnSpy除錯第三方庫和Lib.Harmony攔截第三方庫),並且介紹一種支援多個版本的庫的相容性解決方案。

透過本文介紹支援多個版本的庫的相容性解決方案,讀者可以簡單瞭解如何反編譯第三方庫,以及如何使用強簽名技術來保證庫的相容性(和安全性,本文未展開說,可以閱讀此文[淺談.NET程式集安全簽名](淺談.NET程式集安全簽名 - 知乎 (zhihu.com)))。希望本文提供的案例能幫助讀者更好地理解和應用這些技能。

謝謝您閱讀到這,可以關注【Dotnet9】微信公眾號,大家技術交流、保持進步:

相關文章