如何通过IO方式读取DLL中带索引的字符串资源?
问题描述
修改Windows 10右键菜单时发现,Visual Studio的菜单项与其他程序不同:
- Git的右键菜单项
HKEY_CLASSES_ROOT\Directory\Background\shell\git_shell显示名称是静态字符串(如"Git Ba&sh Here") - Visual Studio的AnyCode项
HKEY_CLASSES_ROOT\Directory\Background\shell\AnyCode显示名称采用@<DLL路径>,<索引编号>格式,示例为@C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\2052\VSLauncherUI.dll,-1002
尝试用C#程序通过IO方式读取DLL中的对应资源时,报错E: Unable to read beyond the end of the stream,无法实现Windows读取此类资源的逻辑,求正确的实现方法。
附尝试的代码:
using System; using System.IO; namespace ResourceLister { class Program { static void Main(string[] args) { try { string dllPath = @"C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\2052\VSLauncherUI.dll"; if (File.Exists(dllPath)) { string[] resourceNames = GetResourceNamesFromDll(dllPath); foreach (var resourceName in resourceNames) { Console.WriteLine("ResourceName: {0}", resourceName); } } else { Console.WriteLine("Not exist"); } } catch (Exception e) { Console.WriteLine("E: {0}", e.Message); } } static string[] GetResourceNamesFromDll(string dllPath) { using (var dllStream = new FileStream(dllPath, FileMode.Open, FileAccess.Read)) { using (var dllReader = new BinaryReader(dllStream)) { dllStream.Position = 0x3C; int peHeaderOffset = dllReader.ReadInt32(); dllStream.Position = peHeaderOffset + 0x78; int resourceTableOffset = peHeaderOffset + 0x88; dllStream.Position = resourceTableOffset; int numberOfResources = dllReader.ReadInt16(); string[] resourceNames = new string[numberOfResources]; for (int i = 0; i < numberOfResources; i++) { dllStream.Position = resourceTableOffset + 2 + (i * 8) + 4; int nameOffset = dllReader.ReadInt32(); dllStream.Position = resourceTableOffset + nameOffset; int nameLength = dllReader.ReadByte(); byte[] nameBytes = dllReader.ReadBytes(nameLength * 2); resourceNames[i] = System.Text.Encoding.Unicode.GetString(nameBytes); } return resourceNames; } } } } }
解决方案
不要手动解析PE文件结构,这种方式容易因文件格式细节(如对齐规则、资源表嵌套结构)出错,维护成本极高。Windows中@<DLL路径>,<索引编号>格式的字符串是通过LoadString API加载的字符串资源,推荐两种可靠的实现方式:
方法1:使用P/Invoke调用LoadString API
这是系统解析这类注册表项的原生方式,直接调用Windows API读取指定索引的字符串资源:
using System; using System.Runtime.InteropServices; using System.Text; using System.ComponentModel; class ResourceReader { [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern int LoadString(IntPtr hInstance, int uID, StringBuilder lpBuffer, int nBufferMax); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool FreeLibrary(IntPtr hModule); private const uint LOAD_LIBRARY_AS_DATAFILE = 0x00000002; public static string GetStringFromDll(string dllPath, int resourceId) { IntPtr hModule = LoadLibraryEx(dllPath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); if (hModule == IntPtr.Zero) { throw new Win32Exception(); } try { // 先获取字符串长度,传入0作为缓冲区大小 int length = LoadString(hModule, resourceId, null, 0); if (length == 0) { throw new Win32Exception(); } StringBuilder sb = new StringBuilder(length); LoadString(hModule, resourceId, sb, length + 1); return sb.ToString(); } finally { FreeLibrary(hModule); } } static void Main() { try { string dllPath = @"C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\2052\VSLauncherUI.dll"; // 注册表中的-1002对应这里的正整数ID 1002 string result = GetStringFromDll(dllPath, 1002); Console.WriteLine("读取到的字符串:" + result); } catch (Exception ex) { Console.WriteLine("错误:" + ex.Message); } } }
方法2:使用.NET的ResourceManager(仅适合托管资源)
如果目标DLL包含.NET托管资源,可以用ResourceManager读取,但系统原生DLL的字符串资源通常是非托管的,因此方法1更通用:
using System; using System.Resources; using System.Reflection; class ManagedResourceReader { static void Main() { try { Assembly dllAssembly = Assembly.LoadFile(@"C:\Path\To\Your\Managed.dll"); // 替换为实际的资源命名空间和资源文件名 ResourceManager rm = new ResourceManager("ResourceNamespace.ResourceFileName", dllAssembly); string value = rm.GetString("ResourceName"); Console.WriteLine(value); } catch (Exception ex) { Console.WriteLine("错误:" + ex.Message); } } }
关键说明
- 注册表中的负号索引(如-1002)对应LoadString API中的正整数ID,直接传入正数即可。
- 系统API已封装所有PE文件解析细节,是最稳定可靠的实现方式。
内容的提问来源于stack exchange,提问作者ycMia
相关产品推荐
相关产品推荐

