2011-06-12 2 views
3

У меня есть небезопасный код C#, который выполняет арифметику указателей на больших блоках памяти по типу byte*, работающий на 64-битной машине. Он работает правильно большую часть времени, но когда вещи становятся большими, я часто получаю какое-то повреждение, когда указатель получает неправильную информацию.64-разрядная арифметика указателя в C#, проверка поведения арифметических переполнений

Странно то, что если я включу «Проверить арифметическое переполнение/нижнее течение», все будет работать правильно. У меня нет исключений переполнения. Но из-за большого повышения производительности мне нужно запустить код без этой опции.

Что может быть причиной этой разницы в поведении?

+6

Вы можете показать нам некоторые из вашего кода? – aL3891

+0

Смещение подписано и указатель без знака? Вам редко нужно отрицательное смещение. –

+0

Соответствующие части кода выглядят следующим образом: this.currentLocation + = sizeof (byte), this.currentLocation + = numberOfChildren * sizeof (ushort), this.currentLocation + = subTree.Length и т. Д. Никаких отрицательных смещений. –

ответ

3

Это ошибка компилятора C# (filed on Connect). @Grant has shown, что MSIL, сгенерированный компилятором C#, интерпретирует операнд uint как подписанный. Это неправильно по # спецификации C, вот соответствующий раздел (18.5.6):

18.5.6 Указатель арифметика

В небезопасном контексте + и - операторы (§7.8.4 и §7.8.5) могут применяться к значениям всех типов указателей, кроме void*. Таким образом, для каждого типа указателя T*, неявно определены следующие операторы:

T* operator +(T* x, int y); 
T* operator +(T* x, uint y); 
T* operator +(T* x, long y); 
T* operator +(T* x, ulong y); 
T* operator +(int x, T* y); 
T* operator +(uint x, T* y); 
T* operator +(long x, T* y); 
T* operator +(ulong x, T* y); 
T* operator –(T* x, int y); 
T* operator –(T* x, uint y); 
T* operator –(T* x, long y); 
T* operator –(T* x, ulong y); 
long operator –(T* x, T* y); 

Учитывая выражение P от типа указателя T* и выражения N типа int, uint, long или ulong, выражения P + N и N + P вычислить значение указателя типа T*, которое получается при добавлении N * sizeof(T) по адресу, указанному P. Аналогично, выражение P - N вычисляет значение указателя типа T*, которое является результатом вычитания N * sizeof(T) по адресу, указанному P.

Даны два выражения, P и Q, типа указателя T*, выражение P – Q вычисляет разность между адресами заданных P и Q, а затем делит эту разность на sizeof(T). Тип результата всегда long. Фактически P - Q вычисляется как ((long)(P) - (long)(Q))/sizeof(T).

Если арифметическая операция указателя переполняет домен типа указателя, результат усекается в соответствии с реализацией, но никаких исключений не производится.


Вы позволили добавить uint к указателю, не неявное преобразование не происходит. И операция не переполняет домен типа указателя. Таким образом, усечение не допускается.

+0

Очень интересно! Прошло некоторое время с тех пор, как моя первая мысль, когда мой код не работал, «это, должно быть, ошибка компилятора»;) –

3

Пожалуйста, проверьте ваш небезопасный код. Чтение или запись памяти за пределами выделенного блока памяти вызывает «коррупцию».

+0

Да, конечно. Я писал код как это в течение многих лет (в C++ тоже); новая вещь работает с 64-битными указателями и обнаруживает, что проверка поведения изменений переполнения, без каких-либо исключений переполнения ... –

+0

Нет разницы в x86 и x64 с точки зрения этой проблемы. Один единственный байт, случайно прочитанный или написанный недействительным указателем, - и проблема появляется в другом месте. Проверьте скорее другой небезопасный код, чем основной алгоритм, где происходит ошибка. – HandMadeOX

+0

Согласен. Когда алгоритм работает правильно, указатель никогда не будет недопустимым. Это (было) просто симптомом проблем с (64-разрядной) арифметикой указателя. Во всяком случае, я решил проблему и написал короткую часть кода, которая ее демонстрирует. Выложите его как ответ в течение нескольких часов. –

1

Я отвечаю на мой собственный вопрос, как я решил эту проблему, но все равно будет интересно читать комментарии о почему изменения поведения с checked против unchecked.

Этот код демонстрирует проблему, а также решение (всегда литье смещение к long перед добавлением):

public static unsafe void Main(string[] args) 
{ 
    // Dummy pointer, never dereferenced 
    byte* testPtr = (byte*)0x00000008000000L; 

    uint offset = uint.MaxValue; 

    unchecked 
    { 
     Console.WriteLine("{0:x}", (long)(testPtr + offset)); 
    } 

    checked 
    { 
     Console.WriteLine("{0:x}", (long)(testPtr + offset)); 
    } 

    unchecked 
    { 
     Console.WriteLine("{0:x}", (long)(testPtr + (long)offset)); 
    } 

    checked 
    { 
     Console.WriteLine("{0:x}", (long)(testPtr + (long)offset)); 
    } 
} 

Это будет возвращать (при запуске на 64-битной машине):

7ffffff 
107ffffff 
107ffffff 
107ffffff 

(BTW, в моем проекте я сначала написал весь код как управляемый код без всякой небезопасной арифметической гадости указателя, но обнаружил, что он использует слишком много памяти. Это всего лишь хобби-проект, единственный, который получает больно, если он Взорвал меня.)

+0

Я не уверен, что это связано, но я недавно обнаружил [ошибку в x64 JIT] (https://connect.microsoft.com/VisualStudio/feedback/details/674232/jit-optimizer-error-when -loop-control-variable-approach-int-maxvalue), что приводит к тому, что константы, находящиеся за пределами диапазона 'Int32', теряют свое значение из-за целочисленного wraparound, когда проверенная арифметика отключена. Я отправлю комментарий об этой ошибке, упоминающей этот тестовый пример. –

+1

Хорошо, см. Ответ @ Гранта. Он инженер MS, ответственный за JIT, поэтому он определенно знает, что случилось. По-видимому, MSIL, созданный компилятором C#, уже имеет «неправильное» поведение, поэтому это не ошибка JIT. Я рассмотрю спецификацию C# и попытаюсь выяснить, разрешено ли компилятору C# создавать этот MSIL из вашего кода. –

+0

Спасибо за ваши проницательные комментарии! –

6

Разница между отмеченными и не отмеченными здесь на самом деле является немного ошибкой в ​​IL или просто некорректным исходным кодом (я не эксперт по языку, поэтому я не буду комментировать, если компилятор C# генерирует правильные IL для двусмысленного исходного кода). Я скомпилировал этот тестовый код, используя версию компилятора C# 4.0.30319.1 (хотя версия 2.0, похоже, делала то же самое). Параметры командной строки, которые я использовал, были:/o +/unsafe/debug: pdbonly.

Для незарегистрированного блока, мы имеем этот IL-код:

//000008:  unchecked 
//000009:  { 
//000010:   Console.WriteLine("{0:x}", (long)(testPtr + offset)); 
    IL_000a: ldstr  "{0:x}" 
    IL_000f: ldloc.0 
    IL_0010: ldloc.1 
    IL_0011: add 
    IL_0012: conv.u8 
    IL_0013: box  [mscorlib]System.Int64 
    IL_0018: call  void [mscorlib]System.Console::WriteLine(string, 
                   object) 

В IL компенсировано 11, адд получает 2 операндами, один из типа байт *, а другой тип uint32. По спецификации CLI они действительно нормализуются в собственные int и int32 соответственно. Согласно спецификации CLI (раздел III, если быть точным), результатом будет собственный int. Таким образом, операнд secodn должен быть повышен до типа int int. Согласно спецификации, это достигается посредством расширения знака. Таким образом, uint.MaxValue (который равен 0xFFFFFFFF или -1 в подписанной нотации), является расширением знака до 0xFFFFFFFFFFFFFFFF. Затем добавляются 2 операнда (0x0000000008000000L + (-1L) = 0x0000000007FFFFFFL). Командный код conv необходим только для целей проверки, чтобы преобразовать собственный int в int64, который в сгенерированном коде является nop.

Теперь для проверяемого блока, мы имеем этот IL:

//000012:  checked 
//000013:  { 
//000014:   Console.WriteLine("{0:x}", (long)(testPtr + offset)); 
    IL_001d: ldstr  "{0:x}" 
    IL_0022: ldloc.0 
    IL_0023: ldloc.1 
    IL_0024: add.ovf.un 
    IL_0025: conv.ovf.i8.un 
    IL_0026: box  [mscorlib]System.Int64 
    IL_002b: call  void [mscorlib]System.Console::WriteLine(string, 
                   object) 

Это практически идентично, за исключением добавления и ко опкода кроме. Для добавления кода операции мы добавили 2 'суффикса'. Первый - суффикс «.ovf», который имеет очевидное значение: проверьте переполнение, но также необходимо «включить второй суффикс:» .un ». (т. е. нет «add.un», только «add.ovf.un»). «.un» имеет 2 эффекта.Наиболее очевидным является то, что дополнительная проверка переполнения выполняется так, как если бы операнды были целыми без знака. Из наших классов CS обратный путь назад, когда, надеюсь, мы все помним, что благодаря двоичному кодированию дополнений двух, добавление дополнений и без знака одно и то же, поэтому «.un» действительно влияет только на проверку переполнения, правильно?

Неправильно.

Помните, что в стеке IL у нас нет 2 64-битных чисел, у нас есть int32 и собственный int (после нормализации). Ну, «.un» означает, что преобразование из int32 в native рассматривается как «conv.u», а не по умолчанию «conv.i», как указано выше. Таким образом, uint.MaxValue равен нулю, равному 0x00000000FFFFFFFFL. Затем добавление правильно создает 0x0000000107FFFFFFL. Код conv convec гарантирует, что неподписанный операнд может быть представлен как подписанный int64 (который он может).

Ваши исправления работают только для 64-битного. На уровне IL более правильным решением было бы явно преобразовать операнд uint32 в собственный int или неподписанный собственный int, а затем и check, и unchecked будут одинаковыми для 32-разрядных и 64-разрядных.

+0

Спасибо! Думаю, я должен был потратить время на изучение ИЛ, когда возникла проблема, но в этот момент я просто хотел, чтобы код работал. Я считаю, что этот проект будет «только 64-битным» BTW, поскольку для реалистичного использования ему требуется несколько ГБ памяти. (Хотел бы я позволить себе сервер с 144 ГБ оперативной памяти.) –

Смежные вопросы