Osäker kod, pekartyper och funktionspekare
Merparten av C#-koden som du skriver är "verifierbart säker kod" verifierbart säker kod innebär att .NET-verktyg kan verifiera att koden är säker. I allmänhet kommer säker kod inte direkt åt minnet med hjälp av pekare. Den allokerar inte heller råminne. Det skapar hanterade objekt i stället.
C# stöder en unsafe
kontext där du kan skriva icke-verifierad kod. I en unsafe
kontext kan kod använda pekare, allokera och frigöra minnesblock och anropa metoder med hjälp av funktionspekare. Osäker kod i C# är inte nödvändigtvis farlig. Det är bara kod vars säkerhet inte kan verifieras.
Osäker kod har följande egenskaper:
- Metoder, typer och kodblock kan definieras som osäkra.
- I vissa fall kan osäker kod öka ett programs prestanda genom att ta bort matrisgränskontroller.
- Osäker kod krävs när du anropar inbyggda funktioner som kräver pekare.
- Att använda osäker kod medför säkerhets- och stabilitetsrisker.
- Koden som innehåller osäkra block måste kompileras med kompilatoralternativet AllowUnsafeBlocks.
Pekartyper
I ett osäkert sammanhang kan en typ vara en pekartyp, utöver en värdetyp eller en referenstyp. En deklaration av pekartyp har något av följande formulär:
type* identifier;
void* identifier; //allowed but not recommended
Den typ som angavs innan *
i en pekartyp kallas referenstyp.
Pekartyper ärver inte från objekt och det finns inga konverteringar mellan pekartyper och object
. Boxning och avboxning stöder inte heller pekare. Du kan dock konvertera mellan olika pekartyper och mellan pekartyper och integraltyper.
När du deklarerar flera pekare i samma deklaration skriver du asterisken (*
) tillsammans med endast den underliggande typen. Den används inte som ett prefix för varje pekarnamn. Till exempel:
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
Skräpinsamlaren håller inte reda på om ett objekt pekas på av några pekartyper. Om referenten är ett objekt i det hanterade heapminnet (inklusive lokala variabler som fångats av lambda-uttryck eller anonyma delegeringar) måste objektet vara låst så länge pekaren används.
Värdet för pekarvariabeln av typen MyType*
är adressen till en variabel av typen MyType
. Följande är exempel på deklarationer av pekartyp:
-
int* p
:p
är en pekare till ett heltal. -
int** p
:p
är en pekare till en pekare till ett heltal. -
int*[] p
:p
är en endimensionell matris med pekare till heltal. -
char* p
:p
är en pekare till ett tecken. -
void* p
:p
är en pekare till en okänd typ.
Pekarens indirekta operator *
kan användas för att komma åt innehållet på den plats som pekarvariabeln pekar på. Tänk till exempel på följande deklaration:
int* myVariable;
Uttrycket *myVariable
anger variabeln int
som finns på adressen i myVariable
.
Det finns flera exempel på pekare i artiklarna om fixed
-instruktionen. I följande exempel används nyckelordet unsafe
och instruktionen fixed
och visar hur du ökar en inre pekare. Du kan klistra in den här koden i huvudfunktionen i ett konsolprogram för att köra den. Dessa exempel måste kompileras med kompilatoralternativet AllowUnsafeBlocks inställt.
// 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
*/
Du kan inte använda indirektionsoperatorn på en pekare av typen void*
. Du kan dock använda en cast för att konvertera en void-pekare till vilken annan pekartyp som helst, och vice versa.
En pekare kan vara null
. Om du tillämpar indirektionsoperatorn på en nullpekare uppstår ett implementeringsdefinierat beteende.
Att skicka pekare mellan metoder kan orsaka odefinierat beteende. Överväg en metod som returnerar en pekare till en lokal variabel via en in
, out
eller ref
parameter eller som funktionsresultat. Om pekaren har angetts i ett fast block kan variabeln som den pekar på inte längre vara fast.
I följande tabell visas de operatorer och instruktioner som kan användas på pekare i en osäker kontext:
Operator/instruktion | Använd |
---|---|
* |
Utför pekarindirektion. |
-> |
Öppnar en medlem i en struct via en pekare. |
[] |
Indexerar en pekare. |
& |
Hämtar adressen för en variabel. |
++ och -- |
Inkrements- och minskningspekare. |
+ och - |
Utför pekararitmetik. |
== , != , < , > , <= och >= |
Jämför pekare. |
stackalloc |
Allokerar minne på stacken. |
fixed -instruktion |
Korrigerar tillfälligt en variabel så att dess adress kan hittas. |
Mer information om pekarrelaterade operatorer finns i Pointer-relaterade operatorer.
Alla pekartyper kan implicit konverteras till en void*
typ. Alla pekartyper kan tilldelas värdet null
. Alla pekartyper kan uttryckligen konverteras till andra pekartyper med hjälp av ett gjutet uttryck. Du kan också konvertera valfri integraltyp till en pekartyp eller valfri pekartyp till en integrerad typ. Dessa konverteringar kräver en explicit gjutning.
I följande exempel konverteras en int*
till en byte*
. Observera att pekaren pekar på variabelns lägsta adresserade byte. När du stegvis ökar resultatet, upp till storleken på int
(4 byte), kan du visa de återstående byteen i variabeln.
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
*/
}
Buffertar med fast storlek
Du kan använda nyckelordet fixed
för att skapa en buffert med en matris med fast storlek i en datastruktur. Buffertar med fast storlek är användbara när du skriver metoder som samverkar med datakällor från andra språk eller plattformar. Bufferten med fast storlek kan ta alla attribut eller modifierare som tillåts för vanliga struct-medlemmar. Den enda begränsningen är att matristypen måste vara bool
, byte
, char
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, float
eller double
.
private fixed char name[30];
I säker kod innehåller en C#-struct som innehåller en matris inte matriselementen. Structen innehåller en referens till elementen i stället. Du kan bädda in en matris med fast storlek i en struct när den används i ett osäkert kodblock.
Storleken på följande struct
beror inte på antalet element i matrisen, eftersom pathName
är en referens:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
En struct kan innehålla en inbäddad matris i osäker kod. I följande exempel har matrisen fixedBuffer
en fast storlek. Du använder en fixed
-instruktion för att få en pekare till det första elementet. Du kommer åt elementen i matrisen via den här pekaren. Instruktionen fixed
fäster fältet fixedBuffer
instans på en specifik plats i minnet.
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]);
}
}
Storleken på 128-elementet char
matris är 256 byte. Fast storleks tecken buffert tar alltid 2 byte per tecken, oavsett kodning. Den här matrisstorleken är densamma även när teckenbuffertar konverteras till API-metoder eller structs med CharSet = CharSet.Auto
eller CharSet = CharSet.Ansi
. Mer information finns i CharSet.
Föregående exempel visar åtkomst till fixed
-fält utan att låsa. En annan vanlig matris med fast storlek är matrisen bool. Elementen i en bool
matris är alltid 1 byte i storlek.
bool
matriser är inte lämpliga för att skapa bitmatriser eller buffertar.
Buffertar med fast storlek kompileras med System.Runtime.CompilerServices.UnsafeValueTypeAttribute, som instruerar CLR (Common Language Runtime) att en typ innehåller en ohanterad matris som potentiellt kan flöda över. Minne som allokeras med stackalloc aktiverar också automatiskt funktioner för buffertöverkörningsidentifiering i CLR. I föregående exempel visas hur en buffert med fast storlek kan finnas i en unsafe struct
.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
Den kompilatorgenererade C#-filen för Buffer
tillskrivs på följande sätt:
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;
}
Buffertar med fast storlek skiljer sig från vanliga matriser på följande sätt:
- Får endast användas i en
unsafe
kontext. - Kan bara vara instansfält för structs.
- De är alltid vektorer eller endimensionella matriser.
- Deklarationen bör innehålla längden, till exempel
fixed char id[8]
. Du kan inte användafixed char id[]
.
Så här använder du pekare för att kopiera en matris med byte
I följande exempel används pekare för att kopiera byte från en matris till en annan.
I det här exemplet används nyckelordet unsafe , vilket gör att du kan använda pekare i metoden Copy
. Instruktionen fast används för att deklarera pekare till käll- och målmatriserna. Instruktionen fixed
fäster platsen för käll- och målmatriserna i minnet så att skräpinsamlingen inte flyttar matriserna. Minnesblocken för matriserna frigörs när fixed
-blocket har slutförts. Eftersom metoden Copy
i det här exemplet använder nyckelordet unsafe
måste den kompileras med kompilatorn AllowUnsafeBlocks.
Det här exemplet kommer åt elementen i båda matriserna med hjälp av index i stället för en andra ohanterad pekare. Deklarationen av pSource
och pTarget
pekare fäster matriserna.
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
*/
}
Funktionspekare
C# innehåller delegate
typer för att definiera säkra funktionspekarobjekt. Att anropa en delegat innebär att instansiera en typ som härleds från System.Delegate och göra ett anrop till dess virtuella Invoke
-metod. Det här virtuella anropet använder instruktionen callvirt
IL. I prestandakritiska kodsökvägar är det mer effektivt att använda calli
IL-instruktionen.
Du kan definiera en funktionspekare med hjälp av syntaxen för delegate*
. Kompilatorn anropar funktionen med hjälp av instruktionen calli
i stället för att instansiera ett delegate
-objekt och anropa Invoke
. Följande kod deklarerar två metoder som använder en delegate
eller en delegate*
för att kombinera två objekt av samma typ. Den första metoden använder en System.Func<T1,T2,TResult> ombudstyp. Den andra metoden använder en delegate*
-deklaration med samma parametrar och returtyp:
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);
Följande kod visar hur du deklarerar en statisk lokal funktion och anropar metoden UnsafeCombine
med hjälp av en pekare till den lokala funktionen:
int product = 0;
unsafe
{
static int localMultiply(int x, int y) => x * y;
product = UnsafeCombine(&localMultiply, 3, 4);
}
Föregående kod illustrerar flera av reglerna för funktionen som används som en funktionspekare:
- Funktionspekare kan bara deklareras i
unsafe
-kontexten. - Metoder som tar en
delegate*
(eller returnerar endelegate*
) kan bara anropas i enunsafe
kontext. - Operatorn
&
för att hämta adressen till en funktion tillåts endast förstatic
funktioner. (Den här regeln gäller både medlemsfunktioner och lokala funktioner).
Syntaxen har paralleller med att deklarera delegate
typer och använda pekare. Suffixet *
på delegate
anger att deklarationen är en funktionspekare. Värdet &
när du tilldelar en metodgrupp till en funktionspekare indikerar att operationen tar metodens adress.
Du kan ange anropskonventionen för en delegate*
med hjälp av nyckelorden managed
och unmanaged
. För unmanaged
funktionspekare kan du dessutom ange anropskonventionen. Följande deklarationer visar exempel på var och en. Den första deklarationen använder managed
-anropskonventionen, som är standard. De följande fyra använder en unmanaged
anropskonvention. Var och en anger någon av ECMA 335-anropskonventionerna: Cdecl
, Stdcall
, Fastcall
eller Thiscall
. Den senaste deklarationen använder unmanaged
anropande konvention och instruerar CLR att välja standardanropskonventionen för plattformen. CLR väljer anropskonventionen vid körningstid.
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);
Du kan lära dig mer om funktionspekare i Funktionspekare funktionsspecifikation.
Språkspecifikation för C#
Mer information finns i Osäker kod kapitel i C#-språkspecifikationen.