다음을 통해 공유


안전하지 않은 코드, 포인터 형식 및 함수 포인터

작성하는 대부분의 C# 코드는 "확인 가능한 안전한 코드"입니다. 확인 가능한 안전한 코드 .NET 도구는 코드가 안전한지 확인할 수 있습니다. 일반적으로 안전한 코드는 포인터를 사용하여 메모리에 직접 액세스하지 않습니다. 또한 원시 메모리를 할당하지 않습니다. 대신 관리되는 개체를 만듭니다.

C#은 확인할 수 없는 코드를 작성할 수unsafe 컨텍스트를 지원합니다. unsafe 컨텍스트에서 코드는 포인터를 사용하고, 메모리 블록을 할당 및 해제하고, 함수 포인터를 사용하여 메서드를 호출할 수 있습니다. C#의 안전하지 않은 코드는 반드시 위험하지는 않습니다. 안전을 확인할 수 없는 코드일 뿐입니다.

안전하지 않은 코드에는 다음과 같은 속성이 있습니다.

  • 메서드, 형식 및 코드 블록은 안전하지 않은 것으로 정의할 수 있습니다.
  • 경우에 따라 안전하지 않은 코드는 배열 경계 검사를 제거하여 애플리케이션의 성능을 높일 수 있습니다.
  • 포인터가 필요한 네이티브 함수를 호출할 때 안전하지 않은 코드가 필요합니다.
  • 안전하지 않은 코드를 사용하면 보안 및 안정성 위험이 발생합니다.
  • 안전하지 않은 블록을 포함하는 코드는 AllowUnsafeBlocks 컴파일러 옵션으로 컴파일되어야 합니다.

포인터 형식

안전하지 않은 컨텍스트에서 형식은 값 형식 또는 참조 형식 외에 포인터 형식일 수 있습니다. 포인터 형식 선언은 다음 형식 중 하나를 사용합니다.

type* identifier;
void* identifier; //allowed but not recommended

포인터 형식의 * 앞에 지정된 형식을 참조 형식이라고 합니다.

포인터 형식은 개체 상속되지 않으며 포인터 형식과 object간에 변환이 없습니다. 또한 boxing과 unboxing은 포인터를 지원하지 않습니다. 그러나 다른 포인터 형식과 포인터 형식과 정수 형식 간에 변환할 수 있습니다.

동일한 선언에서 여러 포인터를 선언하는 경우 기본 형식과 함께 별표(*)를 작성합니다. 각 포인터 이름의 접두사로 사용되지 않습니다. 예를 들어:

int* p1, p2, p3;   // Ok
int *p1, *p2, *p3;   // Invalid in C#

가비지 수집기는 개체가 어떤 포인터 형식에 의해 가리켜지고 있는지 여부를 추적하지 않습니다. 참조 항목이 관리되는 힙의 개체(람다 식 또는 익명 대리자로 캡처된 지역 변수 포함)인 경우, 포인터가 사용되는 동안 개체는 고정되어야 합니다.

MyType* 형식의 포인터 변수 값은 MyType형식 변수의 주소입니다. 다음은 포인터 형식 선언의 예입니다.

  • int* p: p 정수에 대한 포인터입니다.
  • int** p: p 정수에 대한 포인터입니다.
  • int*[] p: p 정수에 대한 포인터의 1차원 배열입니다.
  • char* p: p 문자에 대한 포인터입니다.
  • void* p: p 알 수 없는 형식에 대한 포인터입니다.

포인터 간접 연산자 * 포인터 변수가 가리키는 위치의 내용에 액세스하는 데 사용할 수 있습니다. 예를 들어 다음 선언을 고려합니다.

int* myVariable;

*myVariablemyVariable포함된 주소에 있는 int 변수를 나타냅니다.

fixed 문서의문에 포인터의 여러 예가 있습니다. 다음 예제에서는 unsafe 키워드와 fixed 문을 사용하고 내부 포인터를 증가하는 방법을 보여 있습니다. 이 코드를 콘솔 애플리케이션의 Main 함수에 붙여넣어 실행할 수 있습니다. 이러한 예제는 AllowUnsafeBlocks 컴파일러 옵션 집합으로 컴파일되어야 합니다.

// Normal pointer to an object.
int[] a = [10, 20, 30, 40, 50];
// Must be in unsafe code to use interior pointers.
unsafe
{
    // Must pin object on heap so that it doesn't move while using interior pointers.
    fixed (int* p = &a[0])
    {
        // p is pinned as well as object, so create another pointer to show incrementing it.
        int* p2 = p;
        Console.WriteLine(*p2);
        // Incrementing p2 bumps the pointer by four bytes due to its type ...
        p2 += 1;
        Console.WriteLine(*p2);
        p2 += 1;
        Console.WriteLine(*p2);
        Console.WriteLine("--------");
        Console.WriteLine(*p);
        // Dereferencing p and incrementing changes the value of a[0] ...
        *p += 1;
        Console.WriteLine(*p);
        *p += 1;
        Console.WriteLine(*p);
    }
}

Console.WriteLine("--------");
Console.WriteLine(a[0]);

/*
Output:
10
20
30
--------
10
11
12
--------
12
*/

void*형식의 포인터에는 간접 참조 연산자를 적용할 수 없습니다. 그러나 캐스트를 사용하여 void 포인터를 다른 포인터 형식으로 변환하거나 그 반대로 변환할 수 있습니다.

포인터는 null수 있습니다. 간접 참조 연산자를 null 포인터에 적용하면 구현 정의 동작이 발생합니다.

메서드 간에 포인터를 전달하면 정의되지 않은 동작이 발생할 수 있습니다. in, out또는 ref 매개 변수 또는 함수 결과를 통해 지역 변수에 대한 포인터를 반환하는 메서드를 고려합니다. 포인터가 고정 블록에 설정된 경우 포인터가 가리키는 변수가 더 이상 고정되지 않을 수 있습니다.

다음 표에서는 안전하지 않은 컨텍스트의 포인터에서 작동할 수 있는 연산자와 문을 나열합니다.

연산자/명령문 사용하다
* 포인터 간접 참조를 수행합니다.
-> 포인터를 통해 구조체의 멤버에 액세스합니다.
[] 포인터를 인덱싱합니다.
& 변수의 주소를 가져옵니다.
++-- 포인터를 증가시키고 감소시킵니다.
+- 포인터 산술 연산을 수행합니다.
==, !=, <, >, <=>= 포인터를 비교합니다.
stackalloc 스택에 메모리를 할당합니다.
fixed 진술 주소를 찾을 수 있도록 변수를 일시적으로 수정합니다.

포인터 관련 연산자에 대한 자세한 내용은 포인터 관련 연산자참조하세요.

모든 포인터 형식은 암시적으로 void* 형식으로 변환할 수 있습니다. 모든 포인터 형식에 null값을 할당할 수 있습니다. 캐스트 식을 사용하여 모든 포인터 형식을 다른 포인터 형식으로 명시적으로 변환할 수 있습니다. 모든 정수 계열 형식을 포인터 형식으로 변환하거나 포인터 형식을 정수 형식으로 변환할 수도 있습니다. 이러한 변환에는 명시적 캐스트가 필요합니다.

다음 예제에서는 int*byte*변환합니다. 포인터는 변수의 주소가 가장 낮은 바이트를 가리킵니다. 결과를 연속적으로 증가시키는 경우 int 크기(4바이트)까지 변수의 나머지 바이트를 표시할 수 있습니다.

int number = 1024;

unsafe
{
    // Convert to byte:
    byte* p = (byte*)&number;

    System.Console.Write("The 4 bytes of the integer:");

    // Display the 4 bytes of the int variable:
    for (int i = 0 ; i < sizeof(int) ; ++i)
    {
        System.Console.Write(" {0:X2}", *p);
        // Increment the pointer:
        p++;
    }
    System.Console.WriteLine();
    System.Console.WriteLine("The value of the integer: {0}", number);

    /* Output:
        The 4 bytes of the integer: 00 04 00 00
        The value of the integer: 1024
    */
}

고정 크기 버퍼

fixed 키워드를 사용하여 데이터 구조에서 고정 크기 배열이 있는 버퍼를 만들 수 있습니다. 고정 크기 버퍼는 다른 언어 또는 플랫폼의 데이터 원본과 상호 운용되는 메서드를 작성할 때 유용합니다. 고정 크기 버퍼는 일반 구조체 멤버에 허용되는 모든 특성 또는 한정자를 사용할 수 있습니다. 유일한 제한 사항은 배열 형식이 bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float또는 double있어야 한다는 것입니다.

private fixed char name[30];

안전 코드에서 배열을 포함하는 C# 구조체에는 배열 요소가 포함되지 않습니다. 구조체에는 대신 요소에 대한 참조가 포함됩니다. 안전하지 않은 코드 블록에서 사용되는 경우 고정 크기의 배열을 구조체 포함할 수 있습니다.

pathName 참조이므로 다음 struct 크기는 배열의 요소 수에 따라 달라지지 않습니다.

public struct PathArray
{
    public char[] pathName;
    private int reserved;
}

구조체는 안전하지 않은 코드에 포함된 배열을 포함할 수 있습니다. 다음 예제에서는 fixedBuffer 배열의 크기가 고정되어 있습니다. fixed 구문을 사용하여 첫 번째 요소에 대한 포인터를 얻습니다. 이 포인터를 통해 배열의 요소에 액세스합니다. fixed 문은 fixedBuffer 인스턴스 필드를 메모리의 특정 위치에 고정합니다.

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

internal unsafe class Example
{
    public Buffer buffer = default;
}

private static void AccessEmbeddedArray()
{
    var example = new Example();

    unsafe
    {
        // Pin the buffer to a fixed location in memory.
        fixed (char* charPtr = example.buffer.fixedBuffer)
        {
            *charPtr = 'A';
        }
        // Access safely through the index:
        char c = example.buffer.fixedBuffer[0];
        Console.WriteLine(c);

        // Modify through the index:
        example.buffer.fixedBuffer[0] = 'B';
        Console.WriteLine(example.buffer.fixedBuffer[0]);
    }
}

배열 char 128개 요소의 크기는 256바이트입니다. 고정 크기 char 버퍼는 인코딩에 관계없이 문자당 항상 2바이트를 사용합니다. 이 배열 크기는 문자 버퍼가 CharSet = CharSet.Auto 또는 CharSet = CharSet.Ansi있는 API 메서드 또는 구조체로 마샬링되는 경우에도 동일합니다. 자세한 내용은 CharSet참조하세요.

앞의 예제에서는 고정하지 않고 fixed 필드에 액세스하는 방법을 보여 줍니다. 또 다른 일반적인 고정 크기 배열은 bool 배열입니다. bool 배열의 요소는 항상 1 바이트 크기입니다. bool 배열은 비트 배열 또는 버퍼를 만드는 데 적합하지 않습니다.

고정 크기 버퍼는 System.Runtime.CompilerServices.UnsafeValueTypeAttribute컴파일되며, CLR(공용 언어 런타임)에 잠재적으로 오버플로될 수 있는 관리되지 않는 배열이 형식에 포함되어 있음을 지시합니다. stackalloc 사용하여 할당된 메모리는 CLR에서 버퍼 오버런 감지 기능이 자동으로 활성화됩니다. 이전 예제는 unsafe struct내에서 고정 크기 버퍼가 어떻게 존재할 수 있는지를 보여줍니다.

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

Buffer에 대한 컴파일러 생성 C#은 다음과 같이 정의됩니다.

internal struct Buffer
{
    [StructLayout(LayoutKind.Sequential, Size = 256)]
    [CompilerGenerated]
    [UnsafeValueType]
    public struct <fixedBuffer>e__FixedBuffer
    {
        public char FixedElementField;
    }

    [FixedBuffer(typeof(char), 128)]
    public <fixedBuffer>e__FixedBuffer fixedBuffer;
}

고정 크기 버퍼는 다음과 같은 방법으로 일반 배열과 다릅니다.

  • unsafe 컨텍스트에서만 사용할 수 있습니다.
  • 구조체의 인스턴스 필드일 수 있습니다.
  • 항상 벡터 또는 1차원 배열입니다.
  • 선언에는 fixed char id[8]같은 길이가 포함되어야 합니다. fixed char id[]사용할 수 없습니다.

포인터를 사용하여 바이트 배열을 복사하는 방법

다음 예제에서는 포인터를 사용하여 한 배열에서 다른 배열로 바이트를 복사합니다.

이 예제에서는 안전하지 않은 키워드를 사용하여 Copy 메서드에서 포인터를 사용할 수 있습니다. 고정 문은 원본 및 대상 배열에 대한 포인터를 선언하는 데 사용됩니다. fixed 문은 원본 및 대상 배열이 가비지 수집으로 인해 메모리에서 이동하지 않도록 그 위치를 고정합니다. fixed 블록이 완료되면 배열의 메모리 블록이 고정 해제됩니다. 이 예제의 Copy 메서드는 unsafe 키워드를 사용하므로 AllowUnsafeBlocks 컴파일러 옵션을 사용하여 컴파일해야 합니다.

다음은 관리되지 않는 두 번째 포인터가 아닌 인덱스를 사용하여 두 배열의 요소에 액세스하는 예제입니다. pSourcepTarget 포인터 선언은 배열을 고정합니다.

static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
    int targetOffset, int count)
{
    // If either array is not instantiated, you cannot complete the copy.
    if ((source == null) || (target == null))
    {
        throw new System.ArgumentException("source or target is null");
    }

    // If either offset, or the number of bytes to copy, is negative, you
    // cannot complete the copy.
    if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
    {
        throw new System.ArgumentException("offset or bytes to copy is negative");
    }

    // If the number of bytes from the offset to the end of the array is
    // less than the number of bytes you want to copy, you cannot complete
    // the copy.
    if ((source.Length - sourceOffset < count) ||
        (target.Length - targetOffset < count))
    {
        throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
    }

    // The following fixed statement pins the location of the source and
    // target objects in memory so that they will not be moved by garbage
    // collection.
    fixed (byte* pSource = source, pTarget = target)
    {
        // Copy the specified number of bytes from source to target.
        for (int i = 0; i < count; i++)
        {
            pTarget[targetOffset + i] = pSource[sourceOffset + i];
        }
    }
}

static void UnsafeCopyArrays()
{
    // Create two arrays of the same length.
    int length = 100;
    byte[] byteArray1 = new byte[length];
    byte[] byteArray2 = new byte[length];

    // Fill byteArray1 with 0 - 99.
    for (int i = 0; i < length; ++i)
    {
        byteArray1[i] = (byte)i;
    }

    // Display the first 10 elements in byteArray1.
    System.Console.WriteLine("The first 10 elements of the original are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray1[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of byteArray1 to byteArray2.
    Copy(byteArray1, 0, byteArray2, 0, length);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of the last 10 elements of byteArray1 to the
    // beginning of byteArray2.
    // The offset specifies where the copying begins in the source array.
    int offset = length - 10;
    Copy(byteArray1, offset, byteArray2, 0, length - offset);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");
    /* Output:
        The first 10 elements of the original are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        90 91 92 93 94 95 96 97 98 99
    */
}

함수 포인터

C#은 안전한 함수 포인터 개체를 정의하는 delegate 형식을 제공합니다. 대리자를 호출하려면 System.Delegate 파생된 형식을 인스턴스화하고 해당 Invoke 메서드에 가상 메서드를 호출해야 합니다. 이 가상 호출은 callvirt IL 명령을 사용합니다. 성능에 중요한 코드 경로에서 calli IL 명령을 사용하는 것이 더 효율적입니다.

delegate* 구문을 사용하여 함수 포인터를 정의할 수 있습니다. 컴파일러는 delegate 개체를 인스턴스화하고 Invoke호출하는 대신 calli 명령을 사용하여 함수를 호출합니다. 다음 코드는 delegate 또는 delegate* 사용하여 동일한 형식의 두 개체를 결합하는 두 가지 메서드를 선언합니다. 첫 번째 메서드는 System.Func<T1,T2,TResult> 대리자 형식을 사용합니다. 두 번째 메서드는 매개 변수와 반환 형식이 동일한 delegate* 선언을 사용합니다.

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

public static unsafe T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

다음 코드는 정적 로컬 함수를 선언하고 해당 로컬 함수에 대한 포인터를 사용하여 UnsafeCombine 메서드를 호출하는 방법을 보여줍니다.

int product = 0;
unsafe
{
    static int localMultiply(int x, int y) => x * y;
    product = UnsafeCombine(&localMultiply, 3, 4);
}

앞의 코드는 함수 포인터로 액세스되는 함수에 대한 몇 가지 규칙을 보여 줍니다.

  • 함수 포인터는 unsafe 컨텍스트에서만 선언할 수 있습니다.
  • delegate* 사용하거나 delegate*반환하는 메서드는 unsafe 컨텍스트에서만 호출할 수 있습니다.
  • 함수의 주소를 가져오는 & 연산자는 static 함수에서만 허용됩니다. 이 규칙은 멤버 함수와 로컬 함수 모두에 적용됩니다.

구문에는 delegate 형식 선언 및 포인터 사용과 유사합니다. delegate* 접미사는 선언이 함수 포인터임을 나타냅니다. 함수 포인터에 메서드 그룹을 할당할 때 & 작업은 메서드의 주소를 사용했음을 나타냅니다.

키워드 managedunmanaged사용하여 delegate* 대한 호출 규칙을 지정할 수 있습니다. 또한 unmanaged 함수 포인터의 경우 호출 규칙을 지정할 수 있습니다. 다음 선언은 각각에 대한 예제를 보여 줍니다. 첫 번째 선언은 기본값인 managed 호출 규칙을 사용합니다. 다음 4개에서는 unmanaged 호출 규칙을 사용합니다. 각각은 ECMA 335 호출 규칙(Cdecl, Stdcall, Fastcall또는 Thiscall중 하나를 지정합니다. 마지막 선언은 unmanaged 호출 규칙을 사용하여 CLR이 플랫폼에 대한 기본 호출 규칙을 선택하도록 지시합니다. CLR은 런타임에 호출 규칙을 선택합니다.

public static unsafe T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
    combinator(left, right);

함수 포인터 기능 사양에서 함수 포인터에 대해 자세히 알아볼 수 있습니다.

C# 언어 사양

자세한 내용은 C# 언어 사양안전하지 않은 코드 장을 참조하세요.