什么情况会引发 .net 2.0 String 构造函数抛出异常?

What situations provoke a .net 2.0 String constructor to throw an exception?

我有一些代码有时(但不总是)抛出 String 构造函数的 a Microsoft kb article when using a particular form 中描述的异常。

本质上,我的代码如下所示(除了输入字符串数组的长度因输入而异):

int arraySize = 8;
char* charArray3 = new char[arraySize];
memset(charArray3, 0x61, arraySize);
char * pstr3 = &charArray3[0];
String^ szAsciiUpper = gcnew String(pstr3, 0, arraySize);

kb 文章建议此 'may' 导致抛出异常,但我的单元测试和 大多数 时间在野外,它从未出现。

我想知道什么会引发异常,以便我可以在我的单元测试中复制它并验证它在我们的代码库中永久修复。

他们在 4.0 中修复了它,在 2.0 中仍然损坏:

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication13
{
    class Program
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr VirtualAlloc(IntPtr lpAddress, IntPtr dwSize, uint flAllocationType, uint flProtect);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);

        // For .NET 4.0
        //[System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
        static unsafe void Main(string[] args)
        {
            IntPtr ptr = VirtualAlloc(
                IntPtr.Zero, 
                (IntPtr)(4096 * 2), 
                0x1000 /* MEM_COMMIT */ | 0x2000 /* MEM_RESERVE */, 
                0x04 /* PAGE_READWRITE */);

            IntPtr page1 = ptr;
            IntPtr page2 = (IntPtr)((long)ptr + 4096);

            uint oldAccess;
            bool res = VirtualProtect(page2, 4096, 0x01 /* PAGE_NOACCESS */, out oldAccess);

            try
            {
                Marshal.WriteByte(page1, 1);
                Console.WriteLine("OK");
            }
            catch (AccessViolationException)
            {
                Console.WriteLine("KO");
            }

            try
            {
                Marshal.WriteByte(page2, 1);
                Console.WriteLine("KO");
            }
            catch (AccessViolationException)
            {
                Console.WriteLine("OK");
            }

            try
            {
                byte b1 = Marshal.ReadByte(page1);
                Console.WriteLine("OK");
            }
            catch (AccessViolationException)
            {
                Console.WriteLine("KO");
            }

            try
            {
                byte b2 = Marshal.ReadByte(page2);
                Console.WriteLine("KO");
            }
            catch (AccessViolationException)
            {
                Console.WriteLine("OK");
            }

            for (int i = 0; i < 4096; i++)
            {
                Marshal.WriteByte(page1, i, (byte)'A');
            }

            sbyte* ptr2 = (sbyte*)page1;

            try
            {
                var st1 = new string(ptr2, 0, 4096);
                Console.WriteLine("OK");
            }
            catch (ArgumentOutOfRangeException)
            {
                Console.WriteLine("KO");
            }
        }
    }
}

您必须在 .NET 4.0 中取消注释一行。请注意,此代码不会释放它分配的内存,但这不是一个大问题,因为当进程结束时,内存会被 OS.

回收。

这个程序是做什么的?它使用 VirtualAlloc 分配了 8192 字节(2 页)。通过使用 VirtualAlloc 两个页面是页面对齐的。它禁止访问第二页(VirtualProtect)。然后用 'A' 填充第一页。然后它尝试从第一页创建一个 string。在 .NET 2.0 上,string 构造函数尝试读取第二页的第一个字节(即使您告诉它该字符串只有 4096 字节长)。

中间有一些检查页面是否可以read/written的测试。

通常很难检查这种情况,因为很难有一块内存恰好位于分配的可读内存的末尾space。

此错误出现在 src/vm/comstring.cpp、COMString::StringInitCharHelper() 函数中。这就是作恶者:

   if( IsBadReadPtr(pszSource, (UINT_PTR)length + 1)) {
       COMPlusThrowArgumentOutOfRange(L"ptr", L"ArgumentOutOfRange_PartialWCHAR");
   }

或者换句话说,当 IsBadReadPtr() returns 为 false 时,它​​将窥视 length+1 并俯冲。是的,你必须是不幸的,你的 charArray3 将不得不恰好在内存页面的末尾分配并且下一页必须不可访问。这种情况并不经常发生。

不太确定重现错误是否有任何意义,它太随机了。简单地让你的数组 1 元素更大来避免它。或者转移到 .NET 4,他们确实通过完全删除检查来修复它。

如果有人感兴趣,这是在 C++/CLI 中复制它的方法(完全基于 xanatos 的回答):

LPVOID ptr = VirtualAlloc(0, 4096 * 2, 0x1000, 0x04); // ReadWrite

LPVOID page1 = ptr;
LPVOID page2 = (LPVOID)((long)ptr + 4096);

DWORD oldAccess;
bool res = VirtualProtect(page2, 4096, 0x01, &oldAccess);

char* ptr2 = (char*)page1;

String^ st1 = gcnew String(ptr2, 0, 4096); // <-- This will cause the exception.

Console::WriteLine(st1);