最近又被 ODP.NET 版本問題搞得很煩。

故事是我有顆公用程式元件最近改版(假設名稱叫 MyLibrary.dll 好了),專案參照的 ODP.NET 已升級到 2.122.1.0。移交給同事使用時,同事的 ASP.NET 為配合其他元件仍在使用 ODP.NET 2.112.2.0,這種狀況下,即使加 bindingRedirect 統一導向 2.112.2.0,仍會噴出 CS1705: Assembly 'MyLibrary, Version=3.5.0.0, Culture=neutral, PublicKeyToken=null' uses 'Oracle.DataAccess, Version=2.122.1.0, Culture=neutral, PublicKeyToken=89b483f429c47342' which has a higher version than referenced assembly 'Oracle.DataAccess, Version=2.112.2.0, Culture=neutral, PublicKeyToken=89b483f429c47342' 錯誤。

對我來說,這不是什麼新鮮問題。為了省去為此修改參照版本重新編譯的工夫,我之前就研究過修改參數版號再 ilasm 編譯回 DLL 的奧步妙招(前題是程式未使用新舊版本不相容的 API),還寫了懶人工具簡化操作,最後想出直接修改 .DLL 二進位資料的大絕,簡單、粗暴但絕對有效,呵。

參考資料:

這回再遇上老問題,一不做二不休,決定寫成通用工具,DLL 名稱、新舊版號可任意指定,未來管它 2.112.1、2.112.2、2.122.1 或 2.122.2,愛怎麼改就怎麼改。另外,這次再加上顯示現有參照版本的功能,用起來會像這樣:

第一參數是 DLL 路徑,必填。若不輸入版號參數,工具會透過 System.Reflection 列舉該組件參照的其他組件及版號。若要修改版號,則需同時輸入舊版號及新版號,程式會將舊版號轉成 byte[8] 比對 DLL 的二進位資料。若比對吻合且只有一處符合時,將其置換成新版號的 byte[8],並在原 DLL 路徑檔名後方加上 .patched 另存新檔。比對如超過一處符合則有錯換疑慮,即放棄修改。理論上它也可以用來置換 ODP.NET 以外的程式庫參照版號,但實務上舊版號必須夠特殊才可行。ODP.NET 慣用 2.112.1.0 這種細到三節的版號,轉成 byte[] 會是 0x02 0x00 0x70 0x00 0x01 0x00 0x00 0x00 不容易跟 DLL 其他內容重複;像是 Json.NET 的 12.0.0.0 轉成 byte[] 0x0C 0x00 0x00 0x00 0x00 0x00 0x00 0x00,在 DLL 裡很常出現,除非更進一步去解讀 DLL 檔案結構否則無法確認是哪一段才是 Json.NET 版號,就無法適用。現階段這樣子已可解決 ODP.NET 版號問題,不打算多花時間深入,否則有違「省事」的初衷。

程式其實很短,100 行打死,附上完整程式碼提供大家參考:

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

namespace SetOdpNetRefVer
{
    class Program
    {
        static byte[] ConvVerStrToBytes(string verString)
        {
            var m = Regex.Match(verString, @"(?<n0>\d+)\.(?<n1>\d+)\.(?<n2>\d+)\.(?<n3>\d+)");
            if (!m.Success) throw new ApplicationException($"Invalid version format - {verString}");
            byte[] buff = new byte[8];
            for (var i = 0; i < 4; i++)
                BitConverter.GetBytes(short.Parse(m.Groups["n" + i].Value)).CopyTo(buff, i * 2);
            return buff;
        }

        static void Main(string[] args)
        {
            if (args.Length != 1 && args.Length != 3)
            {
                Console.WriteLine("Syntax: SetAsmRefDllVer.exe AssemblyName.dll");
                Console.WriteLine("Syntax: SetAsmRefDllVer.exe AssemblyName.dll 2.122.1.0 2.112.2.0");
                return;
            }
            var dllPath = args[0];
            if (!File.Exists(dllPath))
            {
                Console.WriteLine("File not found - " + dllPath);
                return;
            }

            if (args.Length == 3)
                ChangeRefVersion(dllPath, args[1], args[2]);
            else
                ShowRefVersions(dllPath);
        }

        private static void ShowRefVersions(string dllPath)
        {
            var asm = Assembly.LoadFrom(dllPath);
            asm.GetReferencedAssemblies().ToList().ForEach(o =>
            {
                Console.WriteLine($"{o.Name}, {o.Version}");
            });
        }

        private static void ChangeRefVersion(string dllPath, string oldVerStr, string newVerStr)
        {
            try
            {
                var oldVerBytes = ConvVerStrToBytes(oldVerStr);
                var newVerBytes = ConvVerStrToBytes(newVerStr);
                byte[] buff = File.ReadAllBytes(dllPath);
                var idx = 0;
                var foundCount = 0;
                while (idx < buff.Length - 8)
                {
                    var match = true;
                    for (var offset = 0; offset < 8; offset++)
                    {
                        if (buff[idx + offset] != oldVerBytes[offset])
                        {
                            match = false;
                            break;
                        }
                    }

                    if (match)
                    {
                        for (var offset = 0; offset < 8; offset++)
                            buff[idx + offset] = newVerBytes[offset];
                        foundCount++;
                    }
                    idx++;
                }
                if (foundCount == 0)
                    Console.WriteLine("Not found");
                else if (foundCount > 1)
                    Console.WriteLine("More than one matched result");
                else if (foundCount == 1)
                {
                    var newFileName = dllPath + ".patched";
                    File.WriteAllBytes(newFileName, buff);
                    Console.WriteLine("Done. Saved as " + newFileName);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("*** ERROR ***");
                Console.WriteLine(ex.ToString());
            }
        }
    }
}

另外,專案我會放上 Github,有需要的同學請自取。

A hacking tool to modify .NET assembly binary directly to change the version number of referenced ODP.NET.


Comments

# by Hung Yang

之前因為IIS主機問題,將ODP版本升級,指定路徑,結果發生因為與Oracle Instance版本不一致,導致CLOB型別資料存取出現異常,不知道黑大這方式會不會產生相似的問題,謝謝。

# by Jeffrey

to Hung Yang,若問題出在 ODP.NET 跟 Oracle Client 版本不相容,不適用這種偷吃步的方法。

Post a comment