안전하지 않은 코드, 포인터 형식 및 함수 포인터
작성하는 C# 코드 대부분은 “확인할 수 있는 안전한 코드”입니다. 확인할 수 있는 안전한 코드란 .NET 도구에서 코드가 안전한지 확인할 수 있음을 의미합니다. 일반적으로 안전한 코드는 포인터를 사용하여 메모리에 직접 액세스하지 않습니다. 또한 원시 메모리를 할당하지 않습니다. 대신 관리형 개체를 만듭니다.
C#은 unsafe
컨텍스트를 지원하는데, 이 컨텍스트에서는 확인할 수없는 코드를 작성할 수 있습니다. unsafe
컨텍스트에서 코드는 포인터를 사용하고, 메모리 블록을 할당 및 해제하고, 함수 포인터를 사용하여 메서드를 호출할 수 있습니다. C#의 안전하지 않은 코드가 반드시 위험한 것은 아닙니다. 단지 CLR에서 안전을 확인할 수 없을 뿐입니다.
안전하지 않은 코드에는 다음과 같은 속성이 있습니다.
- 메서드, 형식 및 코드 블록은 안전하지 않은 것으로 정의할 수 있습니다.
- 경우에 따라 안전하지 않은 코드는 배열 범위 검사를 제거하여 애플리케이션의 성능을 향상할 수 있습니다.
- 포인터가 필요한 네이티브 함수를 호출하는 경우 안전하지 않은 코드가 필요합니다.
- 안전하지 않은 코드를 사용하면 보안 및 안정성 위험이 발생합니다.
- 안전하지 않은 블록을 포함하는 코드는 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;
여기서 *myVariable
식은 int
에 포함된 주소에 있는 myVariable
변수를 가리킵니다.
fixed
문에 대한 문서에는 몇 가지 포인터 예제가 있습니다. 다음 예제는 unsafe
키워드 및 fixed
문을 사용하고 정수 포인터를 증분하는 방법을 보여줍니다. 이 코드를 실행하려면 콘솔 애플리케이션의 주 함수에 붙여 넣습니다. 이러한 예제는 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
매개 변수를 통해, 또는 함수 결과로 지역 변수에 포인터를 반환하는 메서드를 고려합니다. fixed 블록에서 포인터가 설정되면 이 포인터가 가리키는 변수의 고정 상태가 해제될 수 있습니다.
다음 표에서는 안전하지 않은 컨텍스트에서 포인터에 대해 수행할 수 있는 연산자와 문을 보여 줍니다.
연산자/문 | 사용할 용어 |
---|---|
* |
포인터 간접 참조를 수행합니다. |
-> |
포인터를 통해 구조체 멤버에 액세스합니다. |
[] |
포인터를 인덱싱합니다. |
& |
변수 주소를 가져옵니다. |
++ 및 -- |
포인터를 증가 및 감소시킵니다. |
+ 및 - |
포인터 연산을 수행합니다. |
== , != , < , > , <= 및 >= |
포인터를 비교합니다. |
stackalloc |
스택에 메모리를 할당합니다. |
fixed statement |
해당 주소를 찾을 수 있도록 임시로 변수를 고정합니다. |
포인터에 관련 연산자에 대한 자세한 내용은 포인터 관련 연산자를 참조하세요.
모든 포인터 형식을 암시적으로 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# 구조체는 배열 요소를 포함하지 않습니다. 대신 구조체에 요소에 대한 참조가 포함되어 있습니다. unsafe 코드 블록에서 사용될 경우 struct에 고정 크기 배열을 포함할 수 있습니다.
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]);
}
}
128개 요소 char
배열의 크기는 256바이트입니다. 고정 크기 char 버퍼는 인코딩에 관계없이 문자당 항상 2바이트를 사용합니다. 이 배열 크기는 char 버퍼가 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
메서드에서 포인터를 사용할 수 있도록 하는 unsafe 키워드를 사용합니다. fixed 문은 소스 및 대상 배열에 대한 포인터를 선언하는 데 사용됩니다. fixed
명령문은 소스 및 대상 배열의 위치가 메모리에 고정되므로 가비지 수집에 의해 이동되지 않습니다. fixed
블록이 완료되면 배열에 대한 메모리 블록이 고정 해제됩니다. 이 예제의 Copy
메서드는 unsafe
키워드를 사용하므로 AllowUnsafeBlocks 컴파일러 옵션으로 컴파일해야 합니다.
이 예제에서는 두 번째 관리되지 않는 포인터보다는 인덱스를 사용하여 두 배열의 요소에 액세스합니다. pSource
및 pTarget
포인터의 선언은 배열을 고정합니다.
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 T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) =>
combinator(left, right);
다음 코드에서는 정적 로컬 함수를 선언하고 해당 로컬 함수에 대한 포인터를 사용하여 UnsafeCombine
메서드를 호출하는 방법을 보여 줍니다.
static int localMultiply(int x, int y) => x * y;
int product = UnsafeCombine(&localMultiply, 3, 4);
위의 코드는 함수 포인터로 액세스되는 함수에 대한 몇 가지 규칙을 보여 줍니다.
- 함수 포인터는
unsafe
컨텍스트에서만 선언할 수 있습니다. delegate*
를 사용하거나delegate*
를 반환하는 메서드는unsafe
컨텍스트에서만 호출할 수 있습니다.- 함수의 주소를 가져오는
&
연산자는static
함수에서만 사용할 수 있습니다. 이 규칙은 멤버 함수와 로컬 함수에 모두 적용됩니다.
구문은 delegate
형식 선언 및 포인터 사용과 유사합니다. delegate
의 *
접미사는 선언이 함수 포인터임을 나타냅니다. 메서드 그룹을 함수 포인터에 할당할 때 &
는 연산에 메서드의 주소가 사용됨을 나타냅니다.
managed
및 unmanaged
키워드를 사용하여 delegate*
에 대한 호출 규칙을 지정할 수 있습니다. 또한 unmanaged
함수 포인터의 경우 호출 규칙을 지정할 수 있습니다. 다음 선언에서는 각각의 예제를 보여 줍니다. 첫 번째 선언은 managed
호출 규칙(기본값)을 사용합니다. 다음 4개에서는 unmanaged
호출 규칙을 사용합니다. 각각은 ECMA 335 호출 규칙(Cdecl
, Stdcall
, Fastcall
또는 Thiscall
) 중 하나를 지정합니다. 마지막 선언은 unmanaged
호출 규칙을 사용하여 CLR에 플랫폼에 대한 기본 호출 규칙을 선택하도록 지시합니다. CLR은 런타임에 호출 규칙을 선택합니다.
public static T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
combinator(left, right);
함수 포인터 기능 사양에서 함수 포인터에 대해 자세히 알아볼 수 있습니다.
C# 언어 사양
자세한 내용은 C# 언어 사양의 안전하지 않은 코드 챕터를 참조하세요.
.NET