免責事項
本公众号が提供する情報の伝播および利用によって生じた直接的または間接的な結果および損失については、利用者自身が全責任を負うものとします。公众号および筆者はこれらの結果に対して一切の責任を負いません。結果が生じた場合は、自己責任で対応してください。ありがとうございます。
こんにちは、沙漠の果ての狼です。
本記事はDotnet9で初公開されました。前の2つの記事(サードパーティの.NETライブラリのソースコードがなくてもデバッグする方法 と .NETクラスライブラリ内のpublicに限らないクラスやメソッドのインターセプト、改ざん、偽装)と組み合わせて、本記事では実際のケースを設計し、これらの記事で扱ったスキルを実際に適用する方法を手順を追って紹介します。また、複数バージョンのライブラリをサポートする互換性ソリューション(サードパーティライブラリの逆コンパイルと強力な署名を含む)についても説明します。
本記事の目次は以下の通りです:
- はじめに
- ケース設計
- dnSpyを使用したデバッグ
- Lib.Harmonyを使用したインターセプト
- 高バージョンのLib.Harmonyの導入:複数バージョンのライブラリの互換性をサポート
- まとめ
1. はじめに
技術は存在するからには理由があり、重要なのはどのように使用するかです。以前の記事で、読者から次のようなコメントをいただきました:
Lib.Harmonyはあまり正統派ではないライブラリのように思えますが、どのような正当なシナリオで使用する必要がありますか?
サイト管理者の回答:非常に正統派です。サードパーティのライブラリを使用していて、バージョンを確定してすでに本番環境にある場合、潜在的なリスクがあるため、サードパーティライブラリを安易にアップグレードできないことがあります。その場合、サードパーティライブラリには手を加えず、自分のコードのみを変更するしかありません。
また、別の読者のコメントももっともです:
このツールは非常に強力ですが、時には非常に恐ろしいものです。
読者から疑問があったため、この記事を書きました。できるだけ実際のアプリケーションシナリオを模擬したケースを紹介します。実際に試してみて、このツールが本当に正統派かどうかを確認してください。本記事では詳細な手順を提供します。
2. ケース設計
これは小さなアニメーションゲームであり、NuGetに公開しています:Dotnet9Games。この小さなアニメーションゲームには2つのトラップが仕掛けられています。私の手順に従って、一つ一つ問題を解決していきます。まず、.NET Framework 4.6.1 の空のWPFプロジェクト【Dotnet9Playground】を作成します。多くの方がこのバージョンのデスクトップアプリケーションを使用していると思いますが、そうでない場合はコメントでお知らせください。
2.1. Dotnet9Gamesパッケージの導入
作成した(架空の)ゲームをNuGetにサードパーティパッケージとして公開しています。できるだけ実際のシナリオを模擬するため、最新バージョン(本記事では1.0.3ベースで作成)を直接インストールします:

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();
}
}
準備が完了したら、プログラムを実行します:

このゲームは比較的シンプルで、主に以下の手順で構成されています:
- メイン画面にテキスト入力ボックスを表示し、生成する風船の数を入力します。テキストボックスの値をゲームの
BallCountプロパティにデータバインディングでバインドできます。 ゲーム開始ボタンを提供し、クリックするとMyBallGame.StartGame()メソッドが呼び出され、風船を生成してアニメーションを再生します。
2.3. 最初のトラップの導入
風船が8個では少なすぎるので、80個の風船を生成してみましょう:

大きな赤い円がポップアップ表示され、風船がすべて消えました!これがトラップです!
3. dnSpyを使用したデバッグ
3.1. 分析
80個の風船を入力した後、ゲーム開始をクリックするとゲームのメソッドStartGame()が呼び出されます。dnSpy(このリンクには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. デバッグによる検証
大まかな手順は次の通りです:
StartGame()メソッドの最初の行にブレークポイントを設定します。- dnSpyの【起動】ボタンをクリックします。
- 表示された【デバッグプログラム】画面で、「デバッグエンジン」はデフォルトの
.NET Framework、「実行可能プログラム」はWPFメインプログラムのExe【Dotnet9Playground.exe】を選択し、【確定】をクリックしてWPFプログラムを起動します。 - メインプログラム画面で風船の数を9個以上、例えば80個入力します。
- 「ゲーム開始」ボタンをクリックします。
- ブレークポイントにヒットしました。デバッグして確認すると、
PlayBrokenHeartAnimation()メソッドに入っていることがわかります。

4. Lib.Harmonyを使用したインターセプト
原因がわかったので、Lib.Harmonyを使用してStartGame()メソッドをインターセプトします。
4.1. Lib.Harmonyパッケージのインストール
最低バージョン1.2.0.1をインストールします:

なぜ最低バージョンをインストールするのですか?
後ほど一ライブラリ複数バージョンの互換性要件を導入するためです。低バージョンのLib.Harmonyにはバグがあります。そのまま進めましょう。ハハ。
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. これで終わり?いいえ、もう一つのトラップ
風船が動いているのを見ながら、ウィンドウのサイズを縮小/拡大してみます(ここではDebugでの実行を推奨します。プログラムがクラッシュしてOSが少しの間フリーズする可能性があるためです):

プログラムが異常終了しました。スクリーンショットを再度確認します:

異常コードを貼り付けます:
/// <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);
}
分析
- ウィンドウのサイズをドラッグすると、ゲームユーザーコントロール
BallGameのMeasureOverrideメソッドがトリガーされ、レイアウトが再計算されます。 - メソッド内のロジック:
- 動いている風船が存在する場合、
BallGameの実際の幅からすべての子風船の幅の合計を引いた差を計算し、remainWidthを取得します。 remainWidthを使用して最後の風船のサイズを再計算します。remainWidthは減算操作を行うため、風船の数が十分に多く、ゲームコントロールの幅がこれらの風船の幅の合計より小さくなると、負数になります。- 次に
Sizeコンストラクターのコードを確認します(VSを使用している場合、ReSharperをインストールすることをお勧めします。参照ライブラリのメソッドを簡単に確認できます)。以下のスクリーンショットの通りです:
- 動いている風船が存在する場合、

コードをコピーして確認します:
/// <summary>オブジェクトの <see cref="T:System.Windows.Size" /> を記述するために使用される構造体を実装します。 </summary>
[TypeConverter(typeof (SizeConverter))]
[ValueSerializer(typeof (SizeValueSerializer))]
[Serializable]
public struct Size : IFormattable
{
// ここでは多くのコードを省略
/// <summary><see cref="T:System.Windows.Size" /> 構造体の新しいインスタンスを初期化し、初期の <paramref name="width" /> と <paramref name="height" /> を割り当てます。</summary>
/// <param name="width"><see cref="T:System.Windows.Size" /> のインスタンスの初期の幅。</param>
/// <param name="height"><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;
}
// ここでは多くのコードを省略
}
幅と高さが負数になると例外がスローされることが理解できました。再度Lib.Harmonyを使用してBallGameのMeasureOverrideメソッドをインターセプトします。同様の手順で進めます。
/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();
// これは2つ目のインターセプトメソッド:風船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ライブラリをインストールします:

同時にDotnet9GamesのNuGetパッケージも追加します。以前追加した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();
// これは2つ目のインターセプトメソッド:風船MeasureOverrideメソッドをインターセプト
HookBallgameMeasureOverride.StartHook();
}
}
}
これで終わり?実行してみます:

このエラーは、新しいプロジェクトDotnet9HookHighが高バージョンのLib.Harmony(2.2.2)を正しく適用できていないことを示しています。また、メインプロジェクトDotnet9Playgroundが高バージョンのLib.Harmonyを認識して読み込むことができていないことも示しています。どうすればよいでしょうか?私の次のパフォーマンスをご覧ください!
5.2. 高低バージョンのライブラリを別々のディレクトリに配置
5.2.1. プログラム出力ディレクトリの分析

プログラムの出力ディレクトリには0Harmony.dllが1つしかありません。高低2つのバージョンは2つのDLLであるべきです。どうすればよいでしょうか?
5.2.2. 新しいディレクトリの作成
低バージョンはそのまま(出力ディレクトリのルートに配置)、互換性を保つために、高バージョンは別のディレクトリに配置します。例:Lib/Lib.Harmony/2.2.2/0Harmony.dll。このディレクトリ構造でライブラリをDotnet9HookHighプロジェクト内に配置します:

0Harmony.dllのプロパティ【出力ディレクトリにコピー】を【新しい場合はコピーする】に設定します。Dotnet9HookHighのLib.HarmonyライブラリへのNuGet参照を削除し、ローカル参照(元のレシピ、ローカルパスを参照)に変更します。

これで終わり?まだ同じエラーが出ますが?
5.3. 同一ライブラリの複数バージョン設定
5.3.1. App.configで複数バージョンを設定
Dotnet9PalygroundのApp.configファイルを変更し、0Harmony.dllの2つのバージョンと読み込み位置を追加します:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<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. 重点:ライブラリの強力な署名
上記のディレクトリ分割や設定ファイルのバージョン設定だけでは不十分です。メインプロジェクトは依然として2つのバージョンのLib.Harmonyライブラリを区別できません。ここで.NETライブラリの強署名が関係します。上記のApp.config設定のpublicKeyToken属性です。これを追加すればメインプログラムが認識できるようになります。強署名については、ネット上で説明を見つけました《.Netアセンブリ強署名詳細解説》:
- 強署名されたdllをGACに登録すると、異なるアプリケーションで同じdllを共有できます。
- 強署名されたライブラリ、またはアプリケーションは強署名されたdllのみを参照でき、強署名されていないdllは参照できません。ただし、強署名されていないdllは強署名されたdllを参照できます。
- 強署名はソースコードを保護できません。強署名されたdllは逆コンパイル可能です。
- 強署名されたdllは、第三者による悪意のある改ざんを防ぐことができます。
ここでは、バージョン1.2.0.1の0Harmony.dllライブラリはそのままにして、2.2.2の高バージョンに対してのみ強署名処理を行います。署名手順は、[VS2008バージョンでサードパーティdllに強署名がない場合の対応]を参考にします。一緒に行ってみましょう。ここではEverythingソフトウェアを使用して使用するコマンドプログラムを検索します。事前にダウンロードしておくことをお勧めします。
注意:最新プレビュー版2.3.0-prerelease.2は一時的に使用しないでください。サイト管理者がこのバージョンを使用して署名を試みたところ、2晩かかっても成功しませんでした。2.2.2に変更したところ成功しました。下の図も再録画しました。このバージョンには他の依存関係があるためかもしれません。推測の域を出ません:

- 新しいランダムキーペア
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"

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"

- 強命名パラメータを付けて再コンパイルします。
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

- 署名情報を検証します。
"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. まとめ
文中のサンプルコード:MultiVersionLibrary
文中のケースは一般的なもので、特に2つ目のトラップはもっと良くする余地があります。ゲーム関連のコードを読んでみたい方は、PRを出して一緒に切磋琢磨し、このケースをより合理的で面白く、楽しいものにしてください。2つ目のトラップに面白いエフェクトを書いて、インターセプト後に異なる効果を実現する、それがインターセプトの楽しみです。
本記事では、シミュレートされた実際のケースを通じて、前の2つの記事で扱ったスキル(dnSpyを使用したサードパーティライブラリのデバッグ、Lib.Harmonyを使用したサードパーティライブラリのインターセプト)を応用し、複数バージョンのライブラリをサポートする互換性ソリューションを紹介しました。
本記事で紹介した複数バージョンのライブラリをサポートする互換性ソリューションを通じて、読者はサードパーティライブラリの逆コンパイル方法や、強署名技術を使用してライブラリの互換性(およびセキュリティ、本記事では詳しく説明していません。.NETアセンブリのセキュリティ署名についての浅い解説を参照してください)を確保する方法を簡単に理解できます。本記事のケースが、読者のこれらのスキルの理解と応用に役立つことを願っています。