2016-07-07 8 views
1

네이티브 C 코드, C++/CLI 및 C#을 사용하는 .NET 응용 프로그램에서 힙 손상 문제가 발생합니다. 이것은 처음으로 잡초에 실제로 들어가는 처음입니다.C++/CLI에서 관리되지 않는 포인터를 래핑 할 때 힙 손상이 발생했습니다.

응용 프로그램의 구조는 GUI 및 전반적인 제어 흐름을위한 C#, 네이티브 C 함수 래핑을위한 C++/CLI 및 데이터 처리를위한 네이티브 C 함수입니다. 이러한 네이티브 C 함수는 일반적으로 배열에 대한 네이티브 포인터 (예 : int *) 및 차원을 입력으로 받아들입니다. C++/CLI는 이러한 저수준 함수를 상위 수준 결합 처리 함수로 랩핑하고 C#은 상위 수준 함수를 호출합니다.

가끔은 C# 수준에서 관리되지 않는 메모리를 할당 한 다음 동일한 소포를 다른 C++/CLI 함수에 전달해야합니다.

내 C# 및 C++/CLI 계층을 통해 이러한 배열을 자유롭게 전달하기 위해 관리되는 포인터 주위에 씬 (thin) 래퍼 클래스를 만들었습니다.

template <typename T> 
public ref class ContiguousArray 
{ 
public: 
    ContiguousArray<T>(int size) 
    { 
    _size = size; 
    p = (T*) calloc(_size,sizeof(T)); 
    } 

    T& operator[](int i) 
    { 
    return p[i]; 
    } 

    int GetLength() 
    { 
    return _size; 
    } 
    ~ContiguousArray<T>() 
    { 
    this->!ContiguousArray<T>(); 
    } 

    !ContiguousArray<T>() 
    { 
    if (p != nullptr) 
    { 
     free(p); 
     p = nullptr; 
    } 
    } 

    T* p; 
    int _size; 
}; 

// Some non-templated variants of ContiguousArray for passing out to other .NET languages 
public ref class ContiguousArrayInt16 : public ContiguousArray<Int16> 
{ 
    ContiguousArrayInt16(int size) : ContiguousArray<Int16>(size) {} 
}; 

내가 몇 가지 방법이 래퍼 클래스를 사용하십시오 C++/CLI 계층에서 정의 ContiguousArray라는이 래퍼는, 다음과 같이 보입니다.

사용 사례 1 (C++/CLI)

{ 
    // Create an array for the low level code 
    ContiguousArray<float> unmanagedArray(1024); 

    // Call some native functions 
    someNativeCFunction(unmanagedArray.p, unmanagedArray.GetLength()); 
    float* unmanagedArrayPointer = unmanagedArray.p; 
    anotherNativeCFunction(unmanagedArrayPointer, unmanagedArray.GetLength()); 
    int returnCode = theLastNativeCFunction(unmanagedArray.p, unmanagedArray.GetLength()); 

    return returnCode; 
} // unmanagedArray goes out of scope, freeing the memory 

사용 사례 2 (C++/CLI)

{ 
    // Create an array for the low level code 
    ContiguousArray<float>^ unmanagedArray = gcnew ContiguousArray<float>(1024); 
    cliFunction(unmanagedArray); 
    anotherCLIFunction(unmanagedArray); 
    float* unmanagedArrayPointer = unmanagedArray->p; 
    int returnCode = nativeFunction(unmanagedArrayPointer, unmanagedArray->GetLength()); 
    return returnCode; 
} // unmanagedArray goes out of scope, the garbage collector will take care of it at some point 

사용 사례 3 (C 번호)

{ 
    ContiguousArrayInt16 unmanagedArray = new UnmanagedArray(1024); 
    cliFunction(unmanagedArray); 
    unmanagedArray = anotherCLIFunctionThatReplacesUnmanagedArray(unmanagedArray); // Unmanaged array is possibly replaced, original gets collected at some point 
    returnCode = finalCLIFunction(unmanagedArray); 
    // Do something with return code like show the user 
} // Memory gets freed at some point 

나는이 래퍼 클래스를 사용하여 관리되지 않는 메모리를 처리하는 것에 매우주의를 기울이고 있다고 생각했지만 힙 손상 및 액세스 위반 문제가 계속 발생합니다. 내 신청서에. ContiguousArray 객체가 유효한 범위 외부의 관리되지 않는 메모리에 대한 기본 포인터를 보관하지 않습니다.

이론상 힙 손상을 일으킬 수있는 세 가지 유스 케이스에 문제가 있습니까? ContiguousArray 구현에서 뭔가 중요한 것이 누락 되었습니까? 나는 가비지 수집가가 지나치게 열중하고 관리 대상을 정리하기 전에 실제로 처리하기에 충분하지 않을까 걱정됩니다.

사례 1 : 닫는 중괄호까지 파이널 라이저를 호출하지 않을 것이라고 확신합니까? .NET에서 객체가 더 이상 사용되지 않으며 내부 메모리에 대한 포인터가있는 동안 정리가 가능하다고 결정할 수 있습니까? GC :: KeepAlive를 스택 객체에 사용해야합니까?

사례 2 : 세 번째 함수 호출 전에 개체가 삭제되지 않도록하려면 GC :: KeepAlive가 끝에 있어야합니까? 내가 대신 쓴다면 여전히 필요합니까? nativeFunction (unmanagedArray-> p, unmanagedArray-> GetLength());

사례 3 : 여기에 이상한 것을 볼 수는 없지만 뭔가 누락 된 것일 수 있습니까?

+0

http://www.pinvoke.net을보십시오. 모든 원시 dll의 샘플을 제공합니다. – jdweng

+0

내가 호출하는 C 함수는 Windows DLL을 빌드하지 않고 컴파일하는 코드입니다. 나는 네이티브 함수를 호출하기 위해 pinvoke를 사용하고 싶지는 않지만, 고성능은이 함수를 랩핑하는 것이다. –

+2

분명히 잘못된 코드입니다. 파이널 라이저는 네이티브 코드가 실행되는 동안 실행할 수 있습니다. 프로그램의 다른 스레드가 가비지 수집을 트리거 할 때 발생합니다. GC :: KeepAlive() 사용하여 해결 방법이 있지만 훨씬 더 나은 있습니다 : deterministically 배열을 파괴하십시오. C# 코드에서'use unmanagedArray;'를 사용하거나 스택 의미를 사용하십시오. –

답변

1

내 질문 (최고의 선생님)과 tsandy 및 Hans의 조언을 작성하는 마법 덕분에 관리되지 않는 리소스를 처리 할 때 가비지 수집기의 동작을 자세히 조사했습니다. 여기 내가 찾은 것이 있습니다 :

내가 사용하고있는 디자인 패턴에 결함이 있습니다. 가비지 수집기가 핸들이 여전히 범위에 있더라도 이 관리 대상 개체 핸들 (^)을 더 이상 사용하지 않는다고 판단하면 가비지 수집 될 수 있습니다. 적절한 (그러나 느린) 디자인 패턴은 관리되는 래퍼 클래스의 메서드를 통하는 경우를 제외하고는 관리되지 않는 리소스에 대한 액세스를 허용하지 않습니다. 관리되지 않는 리소스에 대한 포인터 또는 참조가 래퍼 외부로 누출 될 수있는 경우 해당 패키지를 소유하는 래퍼가 수집/최종화되지 않도록 신경 써야합니다. 따라서 ContiguousArray와 같은 디자인의 래퍼 클래스는 좋은 생각이 아닙니다.

즉,이 패턴은 빠릅니다! 그래서 여기에 사안을 구제하는 방법이 있습니다.

사례 1은 실제로 좋습니다! C++/CLI에서 스택 의미론을 사용하면 래퍼가 범위를 벗어날 때 결정 론적 마무리가 보장됩니다. 래퍼가 범위를 벗어난 후에도 포인터를 유지하는 것은 여전히 ​​오류이지만이 모든 작업은 안전합니다. C/++/CLI 코드에서만 호출되는 함수에 대한 인수로 가능할 때마다 핸들 참조 (%)를 사용하는 것을 포함하여 스택 의미론을 강력하게 선호하는 많은 C/++/CLI 코드를 변경했습니다.

사례 2는 위험하며 수정해야합니다. 가끔 핸들을 사용하는 것을 피할 수 없기 때문에 가비지 컬렉터가 KeepAlive 호출까지 오브젝트를 계속 유지하도록하려면 GC::KeepAlive(unmanagedArray)을 사용해야합니다.

{ 
    // Create an array for the low level code 
    ContiguousArray<float>^ unmanagedArray = gcnew ContiguousArray<float>(1024); 
    cliFunction(unmanagedArray); 
    anotherCLIFunction(unmanagedArray); 
    float* unmanagedArrayPointer = unmanagedArray->p; 
    int returnCode = nativeFunction(unmanagedArrayPointer, unmanagedArray->GetLength()); 
    GC::KeepAlive(unmanagedArray); // Force the wrapper to stay alive while native operations finish. 
    return returnCode; 
} 

사용 사례 3은 기술적으로 안전하지 않습니다. finalCLIFunction에 대한 호출 직후 .NET 가비지 컬렉터는 finalCLIFunction의 구현에 따라 더 이상 unmanagedArray가 필요하지 않다고 결정할 수 있습니다. 그러나 우리가 필요하지 않으면 KeepAlive와 같은 구현 세부 사항으로 C# 코드에 부담을주는 것은 이치에 맞지 않습니다. 대신 C# 코드에서 관리되지 않는 항목에 액세스하려고 시도하지 말고 모든 C++/CLI 함수의 구현이 해당 인수가 핸들 인 경우 자체 인수에 대해 KeepAlive를 호출하는지 확인하십시오.

int finalCLIFunction(ContiguousArrayInt16^ unmanagedArray) 
{ 
    // Do a bunch of work with the unmanaged array 
    Int16* ptr = unmanagedArray->p; 
    for(int i=0; i < unmanagedArray->GetLength(); i++) 
    { 
    ptr[i]++; 
    } 

    // Call KeepAlive on the calling arguments to ensure they stay alive 
    GC::KeepAlive(unmanagedArray); 

    return 0; 
} 

그렇습니다. 가능할 때마다 스택 의미론을 사용하십시오. 당신이 할 수없는 경우, GC :: KeepAlive()를 사용하십시오. C++/CLI 함수에 인수를 호출 할 때도이 작업을 수행해야합니다. 이 모든 가비지 수집을 C# 코드에서 벗어나지 않도록하십시오.이 코드는 이러한 구현 세부 사항을 알 필요가 없습니다.

이러한 모든 규칙을 따르고 힙 손상 및 액세스 위반이 사라졌습니다. 바라기를 이것은 누군가에게 도움이되기를 바랍니다.

+0

나쁜 조언은 아니지만 GC :: KeepAlive() 호출 하나를 잊어 버리면 다시 망했다. 접근법의 궁극적 인 결함을 이해하는 데 가장 도움이되는 GC는 관리되지 않는 메모리 할당도 처리 할 수 ​​있기를 바랍니다. 래퍼를 완전히 없애고 C 프로그램에서 평소와 같이 malloc/free를 호출하면 올바른 방법으로 강제 실행됩니다. –

+0

나는 이것이 중요한 요소라고 생각한다. 그러나 C++/CLI 라이브러리가 이러한 규칙에 따라 신중하게 작성되는 한 C# 코드에서 관리되지 않는 리소스를 만들 수 있고 .NET 가비지 수집기에서 이러한 리소스를 관리 할 수 ​​있으며 관리되지 않는 함수를 호출하여 모든 리소스를 처리 할 수 ​​있습니다. 메모리 관리로 C# 코드를 오염 시키거나 값 비싼 사본 벌칙을 초래합니다. 모든 복잡성은 C++/CLI 계층으로 밀려 내려와 한 번 올바르게 잊어 버린 다음 잊어 버릴 수 있습니다. –

+0

코드를 신중하게 작성하는 것이 아니라, 실패 할 때 디버깅하는 방법을 알고 있어야합니다. 원래의 버그는 디버깅 할 수 없었습니다. –

0

먼저 ContiguousArray<T>의 멤버가 size_size 대신 오타라고 가정하고 있습니다.

액세스 위반에 대해서는 사례 3에서 아무 것도 표시되지 않습니다. 사례 2에서 nativeFunction이 포인터를 사용하여 완료되기 전에 배열을 확실히 가비지 수집 할 수 있습니다. 케이스 1에 같은 문제가 있는지 확실하지 않습니다.GC::KeepAlive을 사용하는 경우 액세스 위반이 수정됩니까?

힙 손상은 메모리가 해제 된 시간 (이미 !ContiguousArray<T>())으로 이미 해제되었음을 의미합니다. 네이티브 메소드가 배열을 해제하거나 ContiguousArrays 소유 배열을 교체합니까?

추신 : callocnullptr을 반환하지 않는지 확인하는 것이 좋습니다.

+0

예, _size에 대해 유감스럽게 생각합니다. 문제의 핵심에 도달하기 위해 코드에서 많은 내용을 잘라내 었으며 제대로 리팩토링하지 못했습니다. 나는 그 질문을 편집 할 것이다. 나는 keepalives를 사용하기 위해 case 2와 같은 모든 예제를 편집하는 중이며 다시보고 할 것입니다. 네이티브 함수는 절대로 그것들에게 넘겨지는 포인터를 결코 자유롭게하려고 시도하지 않습니다. (그것은 악할 것입니다!) 그리고 ContiguousArrays는 관리가 진짜 골치 거리가되기 때문에 자신의 소유 어레이를 절대로 바꾸지 않습니다. 할당에 대한 팁 주셔서 감사합니다. –