使用Mono.Cecil替换对类型/命名空间的引用

6

背景(非必要,容易混淆,只针对好奇者)

我正在使用免费的Unity3D移动版,它不允许我在移动设备上使用System.Net.Sockets命名空间。问题是我正在使用一个已编译的.dll库(即IKVM),它引用了System.Net.Sockets。实际上我并没有使用IKVM中引用System.Net.Sockets的类,所以我制作了一个叫做dudeprgm.Net.SocketsSockets命名空间的存根库,它只是用存根替换了所有的类和方法(我使用了Mono源代码来完成这个工作)。


我的问题

我需要将我的dlls中的所有System.Net.Sockets.*引用替换为dudeprgm.Net.Sockets.*我知道其他人已经做到了这样的事情(请参见本页底部的编辑。我想知道如何自己完成这个操作。

我使用Mono.Cecil编写了以下代码。
它遍历所有IL指令,检查操作数是否为InlineType,然后检查内联类型是否属于System.Net.Sockets,然后将其重命名为dudeprgm.Net.Sockets并写入。**我不确定这是否是在Mono.Cecil中进行“查找和替换”的正确方法。问题是,这不能捕获所有Sockets用法(请参见下文)。
private static AssemblyDefinition stubsAssembly;

static void Main(string[] args) {
    AssemblyDefinition asm = AssemblyDefinition.ReadAssembly(args[0]);
    stubsAssembly = AssemblyDefinition.ReadAssembly("Socket Stubs.dll");
    // ...
    // Call ProcessSockets on everything
    // ...
    asm.Write(args[1]);
}

/*
 * This will be run on every property, constructor and method in the entire dll given
 */
private static void ProcessSockets(MethodDefinition method) {
    if (method.HasBody) {
        Mono.Collections.Generic.Collection<Instruction> instructions = method.Body.Instructions;
        for (int i = 0; i < instructions.Count; i++) {
            Instruction instruction = instructions[i];
            if (instruction.OpCode.OperandType == OperandType.InlineType) {
                string operand = instruction.Operand.ToString();
                if (operand.StartsWith("System.Net.Sockets")) {
                    Console.WriteLine(method.DeclaringType + "." + method.Name + "(...) uses type " + operand);
                    Console.WriteLine("\t(Instruction: " + instruction.OpCode.ToString() + " " + instruction.Operand.ToString() + ")");
                    instruction.Operand = method.Module.Import(stubsAssembly.MainModule.GetType("dudeprgm.Net.Sockets", operand.Substring(19)));
                    Console.WriteLine("\tReplaced with type " + "dudeprgm.Net.Sockets" + operand.Substring(18));
                }
            }
        }
    }
}

它可以正常工作,但只能捕获“简单”的指令。使用 ildasm 反编译后,我可以看到它替换了类型,就像这里一样:
box        ['Socket Stubs'/*23000009*/]dudeprgm.Net.Sockets.SocketOptionLevel/*01000058*/

但它没有捕捉到这些“复杂”的指令:

callvirt   instance void [System/*23000003*/]System.Net.Sockets.Socket/*0100003F*/::SetSocketOption(valuetype [System/*23000003*/]System.Net.Sockets.SocketOptionLevel/*01000055*/,
                                                                                                                                                valuetype [System/*23000003*/]System.Net.Sockets.SocketOptionName/*01000056*/,
                                                                                                                                                int32) /* 0A000094 */

现在,.dll 中混杂着 dudeprgm.Net.SocketsSystem.Net.Sockets 引用。我相信这是因为我只改变了 OperandType.InlineType,但我不确定还有其他方法。我尝试到处查找,但似乎 Mono.Cecil 没有办法将操作数设置为 string,似乎必须仅使用 Cecil API 完成所有操作(https://dev59.com/gFrUa4cB1Zd3GeqPmLDK#7215711)。对不起,如果我使用的术语不正确,我对 IL 一般都很陌生。
问题:我如何替换 Mono.Cecil 中所有出现 System.Net.Sockets 的地方,而不仅仅是操作数为 InlineType 的地方?我不想逐个检查 Cecil 中的每个 OperandType,我只是想在 Cecil 中找到一些“查找和替换”方法,而不必自己处理纯 IL。

编辑:(同样是不必要的、令人困惑的,只是为了好奇心)
这个人能够做类似的事情,价格为25美元:http://www.reddit.com/r/Unity3D/comments/1xq516/good_ol_sockets_net_sockets_for_mobile_without/

自动修补工具,检测和修复脚本和.dll中的套接字使用。

    ...

“DLL使用Mono.Cecil进行修补。...

您可以查看第二张截图https://www.assetstore.unity3d.com/en/#!/content/13166,看到它说它可以替换命名空间。

那个库不符合我的需求,因为 1)它不能重命名到我想要的命名空间(dudeprgm.Net.Sockets),2)它正在重命名的库不支持 IKVM 需要的所有 System.Net.Sockets 类,因为 IKVM 使用几乎每一个 Sockets 类,以及 3)它需要花费 $25,而我并不真正想购买我不会使用的东西。我只是想展示在 Mono.Cecil 中替换命名空间/类型引用是可能的。

有人吗?请帮忙。 - user837703
如果你能够获取Unity的DLL文件,你可以对它们进行反汇编以查看其内部运行情况。 - Philip Pittle
@PhilipPittle 好的,但我不确定那会有帮助。我认为我无法编辑它们以允许Unity Free编译引用System.Net.Sockets的代码(而且,我认为Unity的这部分只在一个大的.exe文件中)。我更多地是在寻找一种方法来编辑我的IKVM dlls,以便不包括System.Net.Sockets:基本上,只需在dll中用另一个命名空间替换一个命名空间。 :) - user837703
3个回答

11

[01] 相似的问题

您要替换dll(及其中的类型)中的引用以使用另一个dll(及其中的类型),这个问题在技术上类似于已知问题:

Google:“c#为第三方程序集添加强名称”

在这个问题中,您希望使用强名称签名并可能将应用程序安装到GAC或Ngen-ed中,但是您的应用程序依赖于一个旧的第三方库,该库在编译时没有添加强名称,这违反了强命名程序集只能使用强命名程序集的要求。您没有第三方库的源代码,只有二进制文件,因此无法重新编译它(==“简化描述”)

有几种解决方案可行,其中最典型的3种是:

[02] 相似问题的解决方案#1

您可以使用 ildasm/ilasm 进行 "往返旅程",将所有二进制文件转换为文本形式,将所有引用递归地更改为它们的强名称等效项,然后将文本转换回代码。 示例:http://buffered.io/posts/net-fu-signing-an-unsigned-assembly-without-delay-signing/https://dev59.com/nmw15IYBdhLWcg3wisW-#6546134

[03] 类似问题的解决方案 #2

您可以使用已经编写好的工具来解决此问题,例如:http://brutaldev.com/post/2013/10/18/NET-Assembly-Strong-Name-Signer

[04] 类似问题的解决方案 #3

您可以创建一个定制的工具,以满足您的确切需求。这是可能的,我已经做到了,花费了几周时间,代码有数千行。对于最脏的工作,我主要重用(稍加修改)了以下来源的源代码(未排序):
  • ApiChange.Api.Introspection.CorFlagsReader.cs
  • GACManagerApi.Fusion
  • brutaldev/StrongNameSigner
  • icsharpcode/ILSpy
  • Mono.Cecil.Binary
  • Mono.Cecil.Metadata
  • Mono.ResGen
  • Ricciolo.StylesExplorer.MarkupReflection
  • 并阅读http://referencesource.microsoft.com/

[05] 您的问题

虽然您描述的问题看起来只是我上面描述的一部分,但如果您想使用需要强名称签名的GAC安装,则可能会变成相同的问题。

我为您的建议是

[06] 您的问题的解决方案#1

请尝试最简单的解决方案[02],并且为了避免麻烦,请使用Mono包中的ilasm/ildasm工具而不是Microsoft .NET Framework提供的工具(.NET Framework 4.5中的Microsoft Resgen存在问题,无法往返resx格式,Ildasm输出不能正确处理非ASCII字符等)。虽然您无法修复Microsoft的损坏闭源代码,但可以修复Mono的开源代码(但我不需要)。

[07] 您问题的解决方案#2

如果[06]对您不起作用,则研究(调试)→ ILSpy ←和研究Mono文档以获取执行所需操作的各种命令行工具及其源代码 - 您将看到它们如何使用Mono.Cecil库。

如果您需要验证强名称甚至已签名的程序集(篡改它们将使签名无效)或删除签名等,则需要深入了解代码,这比简单的Stack Overflow答案描述更长。

[08] 您问题的解决方案#3

潜伏在 ILMerge 背后的原理和解决方案可能会指向更简单的解决方案。

[09] 问题解决方案 #4

另一个更简单的解决方案可能是(如果 IKVM 支持),挂钩 AssemblyResolve 事件,您可以将 dll 名称重新映射到物理 dll,例如从完全不同的文件或资源流中加载。如旧版 Stack Overflow 问题 Embedding DLLs in a compiled executable 的多个答案所示。

(编辑 #1:经评论)

[10] 问题解决方案 #5

如果您的一般问题实际上归结为“如何使 IKVM.dll 使用我的套接字类而不是命名空间 System.Net.Sockets 中的套接字类”,那么相当直接的解决方案可能是:

使用http://www.ikvm.net/download.html提供的源代码编译和部署您自己定制的IKVM.dll版本 - 无需二进制Mono.Cecil魔法。

由于所有代码都是开放的,因此应该可以找到并重定向所有指向命名空间System.Net的引用到dudeprgm.Net中。

  • [10.1] 获取 IKVM 源代码和所有其他前置条件,并确保您可以编译工作的 IKVM.dll
  • [10.2] 将 dudeprgm.Net.cs 项目添加到解决方案中
  • [10.3] 在所有源文件中查找并删除所有类似于 using System.Net 的内容
  • [10.4] 在所有源文件中查找并替换所有类似于 System.Net 的完整文本为 dudeprgm.Net
  • [10.5] 编译。当编译器抱怨缺少一个符号(之前在 System.Net 命名空间中)时,请将其添加到您的存根文件中。转到 [10.5]
  • [10.6] 如果以上步骤在 2 小时后仍未解决“构建成功”的问题,则考虑另一种解决方案(或睡觉)
  • [10.7] 检查 IKVM 许可证(http://sourceforge.net/p/ikvm/wiki/License/),如果有必须更改/声明/确认的内容,因为原始源代码已被修改

[11] 你的问题解决方案 #6

如果你选择使用轨迹 [04] 并且使用文本文件以及 ilasm/ildasm 工具 (样式 [02]) 看起来不够高效,那么下面是我自动强名称签名工具中关键的相关部分,它使用 Mono.Cecil 将程序集引用转换为其他引用。该代码已按原样粘贴(没有前后和周围的代码行),以适合我的工作方式。阅读密钥:a 是 Mono.Cecil.AssemblyDefinitionb 实现了 Mono.Cecil.IAssemblyResolverb 实例中的关键方法是方法 AssemblyDefinition Resolve(AssemblyNameReference name),该方法将所需的 DLL 名称转换为对 AssemblyDefinition.ReadAssembly(..) 的调用。我不需要解析指令流,重新映射程序集引用就足够了(如果需要,我可以在这里粘贴我的代码的几个其他部分)。

/// <summary>
/// Fixes references in assembly pointing to other assemblies to make their PublicKeyToken-s compatible. Returns true if some changes were made.
/// <para>Inspiration comes from https://github.com/brutaldev/StrongNameSigner/blob/master/src/Brutal.Dev.StrongNameSigner.UI/MainForm.cs
/// see call to SigningHelper.FixAssemblyReference
/// </para>
/// </summary>
public static bool FixStrongNameReferences(IEngine engine, string assemblyFile, string keyFile, string password)
{
    var modified = false;

    assemblyFile = Path.GetFullPath(assemblyFile);

    var assemblyHasStrongName = GetAssemblyInfo(assemblyFile, AssemblyInfoFlags.Read_StrongNameStatus)
        .StrongNameStatus == StrongNameStatus.Present;

    using (var handle = new AssemblyHandle(engine, assemblyFile))
    {
        AssemblyDefinition a;

        var resolver = handle.GetAssemblyResolver();

        a = handle.AssemblyDefinition;

        foreach (var reference in a.MainModule.AssemblyReferences)
        {
            var b = resolver.Resolve(reference);

            if (b != null)
            {
                // Found a matching reference, let's set the public key token.
                if (BitConverter.ToString(reference.PublicKeyToken) != BitConverter.ToString(b.Name.PublicKeyToken))
                {
                    reference.PublicKeyToken = b.Name.PublicKeyToken ?? new byte[0];
                    modified = true;
                }
            }
        }

        foreach (var resource in a.MainModule.Resources.ToList())
        {
            var er = resource as EmbeddedResource;
            if (er != null && er.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase))
            {
                using (var targetStream = new MemoryStream())
                {
                    bool resourceModified = false;

                    using (var sourceStream = er.GetResourceStream())
                    {
                        using (System.Resources.IResourceReader reader = new System.Resources.ResourceReader(sourceStream))
                        {
                            using (var writer = new System.Resources.ResourceWriter(targetStream))
                            {
                                foreach (DictionaryEntry entry in reader)
                                {
                                    var key = (string)entry.Key;
                                    if (entry.Value is string)
                                    {
                                        writer.AddResource(key, (string)entry.Value);
                                    }
                                    else
                                    {
                                        if (key.EndsWith(".baml", StringComparison.OrdinalIgnoreCase) && entry.Value is Stream)
                                        {
                                            Stream newBamlStream = null;
                                            if (FixStrongNameReferences(handle, (Stream)entry.Value, ref newBamlStream))
                                            {
                                                writer.AddResource(key, newBamlStream, closeAfterWrite: true);
                                                resourceModified = true;
                                            }
                                            else
                                            {
                                                writer.AddResource(key, entry.Value);
                                            }
                                        }
                                        else
                                        {
                                            writer.AddResource(key, entry.Value);
                                        }
                                    }
                                }
                            }
                        }

                        if (resourceModified)
                        {
                            targetStream.Flush();
                            // I'll swap new resource instead of the old one
                            a.MainModule.Resources.Remove(resource);
                            a.MainModule.Resources.Add(new EmbeddedResource(er.Name, resource.Attributes, targetStream.ToArray()));
                            modified = true;
                        }
                    }
                }
            }
        }

        if (modified)
        {
            string backupFile = SigningHelper.GetTemporaryFile(assemblyFile, 1);

            // Make a backup before overwriting.
            File.Copy(assemblyFile, backupFile, true);
            try
            {
                try
                {
                    AssemblyResolver.RunDefaultAssemblyResolver(Path.GetDirectoryName(assemblyFile), () => {
                        // remove previous strong name https://groups.google.com/forum/#!topic/mono-cecil/5If6OnZCpWo
                        a.Name.HasPublicKey = false;
                        a.Name.PublicKey = new byte[0];
                        a.MainModule.Attributes &= ~ModuleAttributes.StrongNameSigned;

                        a.Write(assemblyFile);
                    });

                    if (assemblyHasStrongName)
                    {
                        SigningHelper.SignAssembly(assemblyFile, keyFile, null, password);
                    }
                }
                catch (Exception)
                {
                    // Restore the backup if something goes wrong.
                    File.Copy(backupFile, assemblyFile, true);

                    throw;
                }
            }
            finally
            {
                File.Delete(backupFile);
            }
        }
    }

    return modified;
}

[12] 轮到你了


+1 感谢提供详细的选项列表!我发现这比我想象的要难。可惜。我对IL相当新,所以希望有一个相对较快的方法来完成这个过程而不需要反汇编/重新组装代码。 - user837703
好的,我正在尝试使用Mono的monodisilasm来完成**[02]**,但是我一直收到这个晦涩的错误提示:[ERROR] FATAL UNHANDLED EXCEPTION: System.ArgumentException: Key duplication when adding: System.Collections.DictionaryEntry - user837703
1
我使用Visual Studio命令提示符中的Microsoft ilasmildasm解决了错误。我正在尝试编写一个脚本,以便能够对我所有的dll执行此操作。(另外,**[10]** 对我来说并不完全适用,因为我有一些闭源的dll引用了System.Data,而System.Data又引用了System.Net.Sockets,我也必须对它们进行类似的操作。) - user837703
@dudeprgm,感谢你的赏金积分。我还没有看到你的“答案”。实际上,在这里发布适用于你的脚本作为→ → 自我回答 ← ←可能是你19天长旅的非常有价值的结果 - 对于未来针对Unity3D移动平台的读者(它看起来被高估但非常受欢迎)。 - xmojmr
3
太棒了!StackOverflow应该有更多这样类型的回答……这个特别的回答对于考虑“以上任何一个”的人来说非常好,可以帮助他们理解自己要涉足的领域。 - Jonno
显示剩余5条评论

2
这实际上是对@xmojmer答案的扩展。
我编写了一个小型的bash脚本来自动化xmojmer的选项[02]:
# vsilscript.sh

# Usage:
# Open Cygwin
# . vsilscript.sh <original .dll to patch>
# output in "Sockets_Patched" subdirectory
nodosfilewarning=true # just in case cygwin complains
ILASM_PATH="/cygdrive/c/Windows/Microsoft.NET/Framework64/" # modify for your needs
ILASM_PATH="$ILASM_PATH"$(ls "$ILASM_PATH" -v | tail -n 1)
ILDASM_PATH="/cygdrive/c/Program Files (x86)/Microsoft SDKs/Windows/v8.1A/bin/NETFX 4.5.1 Tools" # modify for your needs
PATH="$ILDASM_PATH:$ILASM_PATH:$PATH"
base=$(echo $1 | sed "s/\.dll//g")
ildasm /out=$base".il" /all /typelist $base".dll"
cp $base".il" "tempdisassembled.il"
cat "tempdisassembled.il" | awk '
BEGIN { print ".assembly extern socketstubs { }"; }
{ gsub(/\[System[^\]]*\]System\.Net\.Sockets/, "[socketstubs]dudeprgm.Net.Sockets", $0); print $0; }
END { print "\n"; }
' 1> $base".il" #change the awk program to swap out different types
rm "tempdisassembled.il"
mkdir "Sockets_Patched"
# read -p "Press Enter to assemble..."
ilasm /output:"Sockets_Patched/"$base".dll" /dll /resource:$base".res" $base".il"

如果您的计算机是32位或者ilasmildasm在不同的位置,请修改ILASM_PATHILDASM_PATH变量。此脚本假定您正在Windows上使用Cygwin
修改发生在awk命令中。它通过添加对我的存根库(称为socketstubs.dll,存储在相同目录中)的引用来实现。
.assembly extern sockectstubs { }

在反汇编的IL代码开头。然后,对于每一行,它会查找 [System]System.Net.Sockets 并将其替换为 [socketstubs]dudeprgm.Net.Sockets。它在IL代码末尾添加一个换行符,否则 ilasm 将无法重新组装它,根据 docs

如果 .il 源文件中的最后一行代码没有尾随空格或换行符,则可能编译失败。

最终修补的 .dll 将放置在一个名为 "Sockets_Patched" 的新目录中。
您可以修改此脚本以交换任何 dll 中的任意两个命名空间。
将以下步骤翻译成中文:
  1. [System[^\]] 中的 System 替换为包含旧命名空间的程序集名称。
  2. System\.Net\.Sockets 替换为旧命名空间,并在每个句点前加上反斜杠。
  3. 将脚本中的所有 socketstubs 替换为包含新命名空间的库。
  4. dudeprgm.Net.Sockets 替换为新命名空间。

1
应用您的脚本后,您的应用程序是否能够运行?我的意思是,如果您没有遇到程序集强名称问题。当 DLL 二进制文件被修改时,DLL 的校验和会发生变化,这可能会使数字签名和强名称哈希无效。因此,.NET Framework 可能会拒绝加载此类 DLL。那么它真的有效吗? - xmojmr
1
我认为Unity没有进行任何此类检查(或者可能是抑制了它),而且我也没有将其放入GAC中,所以至少对我来说它是有效的。也许这是因为Unity使用Mono。但我不确定它能帮助其他人多少。 - user837703

0
你可以将套接字对象包装在一个接口中,然后进行依赖注入,以获取你真正想要的实现。这段代码可能有点繁琐,但是之后你可以将套接字替换为任何你想要的东西,而不必直接引用 System.Net.Sockets。

抱歉,我觉得我可能误读了你的问题。替换第三方DLL中的引用会更加困难。 - Owen Johnson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接