2011-06-12 2 views
3

64 비트 시스템에서 실행중인 byte* 유형의 큰 메모리 블록에서 포인터 연산을 수행하는 안전하지 않은 C# 코드가 있습니다. 그것은 대부분의 경우 올바르게 작동하지만 일이 커지면 포인터가 올바르지 않게되는 일종의 손상이 발생합니다.C#에서 64 비트 포인터 연산, 산술 오버플로 변경 동작 확인

이상한 점은 "산술 오버플로/언더 플로우 검사"를 켜면 모든 것이 올바르게 작동한다는 것입니다. 오버플로 예외가 발생하지 않습니다. 그러나 큰 성능으로 인해이 옵션을 사용하지 않고 코드를 실행해야합니다.

이 동작의 차이를 유발할 수있는 원인은 무엇입니까?

+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은 C# 컴파일러에서 생성 한 MSIL이 uint 피연산자로 서명 된 것으로 해석합니다.

18.5.6 포인터 산술 안전하지 않은 상황에서

+- 사업자 (§7.8.4 : 그것은 여기에 관련 섹션 (18.5.6)가있어,는 C# 사양에 따라 잘못 및 §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); 

는 포인터 타입 T* 및 유형 int, uint, long의 표현 N, 또는 ulong, 식의 표현 P을 감안할 때 P + NN + PP에 의해 주어진 주소에 N * sizeof(T)을 더한 결과 T*의 포인터 값을 계산합니다. 마찬가지로, 식 P - NP에 의해 주어진 주소에서 N * sizeof(T)을 뺀 결과 T*의 포인터 값을 계산합니다.

주어 두 식, PQ가 포인터 타입 T*의 표현식은 P – QPQ 의해 주어진 어드레스 간의 차이를 계산하고 그 차이 sizeof(T) 의해 분할한다. 결과 유형은 항상 long입니다. 결과적으로 P - Q((long)(P) - (long)(Q))/sizeof(T)으로 계산됩니다.

포인터 산술 연산이 포인터 유형의 도메인을 오버플로하면 결과가 구현 정의 방식으로 잘리지 만 예외가 발생하지 않습니다.


당신은 포인터에 uint을 추가 할 수있어, 암시 적 변환이 수행되지 않습니다. 그리고 연산은 포인터 유형의 도메인을 오버플로하지 않습니다. 따라서 잘라내기를 허용하지 않습니다.

+0

매우 흥미 롭다! 내 코드가 작동하지 않을 때 처음 생각한 이후로 "컴파일러 버그"여야합니다.) –

3

안전하지 않은 코드를 다시 확인하십시오. 할당 된 메모리 블록 외부에서 메모리를 읽거나 쓰면 '손상'이 발생합니다.

+0

그래, 당연하지. 나는 이런 식으로 몇 년 동안 (C++에서도) 코드를 작성 해왔다. 새로운 기능은 64 비트 포인터로 작업하고 오버플로 예외를 발생시키지 않고 오버플로 변경 동작을 확인하는 것입니다. –

+0

이 문제점의 핵심은 x86 및 x64에서 차이가 없습니다. 우연히 한 바이트 만 유효하지 않은 포인터에 의해 읽히거나 쓰여지므로 문제가 다른 곳에서 나타납니다. 오류가 발생한 주 알고리즘보다 안전하지 않은 다른 코드를 확인하십시오. – HandMadeOX

+0

동의합니다. 알고리즘이 올바르게 작동하면 포인터가 결코 유효하지 않게됩니다. 이것은 (64 비트 관련) 포인터 산술의 문제 증상 일뿐입니다. 어쨌든, 나는 문제를 해결하고 그것을 보여주는 짧은 코드를 작성했다. 몇 시간 후에 응답으로 게시됩니다. –

1

내가 문제를 해결 한 것처럼 내 자신의 질문에 대답 해요,하지만 여전히 uncheckedchecked이유 행동 변화 주석을 읽기에 관심이있을 것입니다.

이 코드는 문제뿐만 아니라 (추가하기 전에 항상 long에 오프셋 캐스팅) 솔루션을 보여줍니다 (64 비트 시스템에서 실행할 때)

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)); 
    } 
} 

이 반환합니다

7ffffff 
107ffffff 
107ffffff 
107ffffff 

(BTW, 내 프로젝트에서 처음에는 모든 안전하지 않은 포인터 산술이 없으면 모든 코드를 관리 코드로 작성했지만 너무 많은 메모리를 사용하고 있음을 알았습니다. 이것은 취미 프로젝트 일 뿐이며, 나 불어이다.)

+0

이것이 관련이 있는지는 잘 모르겠지만 [x64 JIT의 버그] (https://connect.microsoft.com/VisualStudio/feedback/details/674232/jit-optimizer-error-when)를 최근에 발견했습니다. -loop-controlling-variable-approaches-int-maxvalue)는'Int32'의 범위를 벗어나는 상수가 결과 산술을 사용할 수 없을 때 정수로 감싸기 때문에 값을 잃게됩니다. 이 테스트 케이스를 언급하는 버그에 대한 의견을 게시 할 것입니다. –

+1

그래, @ 그랜트의 대답을 참조하십시오. 그는 JIT를 담당하는 MS 기술자이기 때문에 무엇이 일어나는지 확실히 알고 있습니다. 분명히 C# 컴파일러에서 생성 된 MSIL은 이미 "잘못된"동작을 가지고 있으므로 JIT 버그가 아닙니다. C# 스펙을 살펴보고 C# 컴파일러가 코드에서 MSIL을 생성 할 수 있는지 여부를 알아 보겠습니다. –

+0

귀하의 통찰력있는 의견을 보내 주셔서 감사합니다! –

6

여기에서 확인한 것과 체크하지 않은 것의 차이점은 실제로 일리노이 버그 나 일부 나쁜 소스 코드입니다. (저는 언어 전문가가 아니므로 C# 컴파일러가 올바른 코드를 생성하는지 논평하지 않겠습니다. 일리노이주의 모호한 소스 코드). 나는 4.0.30319.1 버전의 C# 컴파일러를 사용하여이 테스트 코드를 컴파일했다 (2.0 버전은 같은 일을하는 것처럼 보였지만). 필자가 사용하는 명령 줄 옵션은/o +/unsafe/debug : pdbonly입니다. 선택되지 않은 블록

, 우리는이 IL 코드를 가지고 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) 

11 오프셋을 추가 2 피연산자 입력 바이트 * 중 하나 형 UINT32 다른을 얻는다. CLI 스펙에 따라 이들은 각각 고유 한 int 및 int32로 실제로 표준화됩니다. CLI 스펙 (세분화 된 파티션 III)에 따르면 결과는 기본 int입니다. 따라서 secodn 피연산자는 native int 유형으로 승격되어야합니다. 명세에 따르면, 이것은 부호 연장을 통해 달성된다. 따라서 uint.MaxValue (부호있는 표기법에서 0xFFFFFFFF 또는 -1)는 부호가 0xFFFFFFFFFFFFFFFF까지 확장됩니다. 그런 다음 2 개의 피연산자가 추가됩니다 (0x0000000008000000L + (-1L) = 0x0000000007FFFFFFL). conv opcode는 네이티브 int를 생성 된 코드에서 nop 인 int64로 변환하기위한 검증 목적으로 만 필요합니다. 그것은 추가 및 전환 오피 코드를 제외하고 거의 동일하다

//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) 

:

지금 체크 블록, 우리는이 IL있다. 추가 연산 코드에 2 개의 '접미어'를 추가했습니다. 첫 번째 것은 ".ovf"접미사로 명백한 의미가 있습니다. 오버플로가 있는지 확인하지만 두 번째 접미사 인 ".un"을 활성화해야합니다. (즉 "add.un"이없고 "add.ovf.un"만 있습니다). ".un"에는 2 가지 효과가 있습니다.가장 명백한 것은 오퍼랜드가 부호없는 정수인 것처럼 추가 및 오버플로 검사가 수행된다는 것입니다. 우리 CS 클래스에서 돌아 왔을 때, 2의 보수 바이너리 인코딩 덕분에 서명 된 덧셈과 부호없는 덧셈은 동일하므로 ".un"은 실제로 오버플로 검사에만 영향을 미칩니다. 맞습니까?

틀린.

IL 스택에는 2 개의 64 비트 숫자가 없다는 것을 기억하십시오. 우리는 int32와 네이티브 int (정규화 이후)를 가지고 있습니다. ".un"은 int32에서 native 로의 변환이 위와 같이 기본 "conv.i"보다는 "conv.u"와 같이 처리된다는 것을 의미합니다. 따라서 uint.MaxValue는 0x00000000FFFFFFFFL까지 0으로 확장됩니다. 그런 다음 올바르게 추가하면 0x0000000107FFFFFFL이 생성됩니다. conv opcode는 부호없는 피연산자가 signed int64로 표현 될 수 있는지 확인합니다.

수정 된 내용은 64 비트 용입니다. IL 수준에서 uint32 피연산자를 네이티브 int 또는 부호없는 네이티브 int로 명시 적으로 변환 한 다음 32 비트 및 64 비트 모두 check 및 unchecked가 동일하게 수정됩니다.

+0

감사합니다. 문제가 발생했을 때 일리노이 즈를 공부할 시간을 가졌어야했는데, 그 시점에서 코드가 작동하기를 원했습니다. 나는이 프로젝트가 실제로 64 GB의 메모리 만 필요로하기 때문에 "64 비트 전용"이라고 생각한다. (나는 144GB RAM을 가진 서버를 구입할 수 있었으면 좋겠다.) –