Onveilige code, aanwijzertypen en functie-aanwijzers
De meeste C#-code die u schrijft, is 'verifieerbaar veilige code'. Verifieerbare veilige code betekent dat .NET-hulpprogramma's kunnen controleren of de code veilig is. Over het algemeen heeft veilige code geen rechtstreeks toegang tot geheugen met behulp van aanwijzers. Er wordt ook geen onbewerkt geheugen toegewezen. In plaats daarvan worden beheerde objecten gemaakt.
C# ondersteunt een unsafe
context, waarin u niet-verifieerbare code kunt schrijven. In een unsafe
context kan code pointers gebruiken, geheugenblokken toewijzen en vrijmaken en methoden aanroepen met behulp van functieaanwijzers. Onveilige code in C# is niet noodzakelijkerwijs gevaarlijk; het is gewoon code waarvan de veiligheid niet kan worden geverifieerd.
Onveilige code heeft de volgende eigenschappen:
- Methoden, typen en codeblokken kunnen worden gedefinieerd als onveilig.
- In sommige gevallen kan onveilige code de prestaties van een toepassing verhogen door matrixgrenzencontroles te verwijderen.
- Onveilige code is vereist wanneer u systeemeigen functies aanroept waarvoor aanwijzers zijn vereist.
- Het gebruik van onveilige code introduceert beveiligings- en stabiliteitsrisico's.
- De code die onveilige blokken bevat, moet worden gecompileerd met de optie AllowUnsafeBlocks compiler.
Aanwijzertypen
In een onveilige context kan een type een aanwijzer zijn, naast een waardetype of een verwijzingstype. Een declaratie van het type aanwijzer heeft een van de volgende vormen:
type* identifier;
void* identifier; //allowed but not recommended
Het type dat is opgegeven vóór de *
in een aanwijzertype, wordt het verwijzingstypegenoemd.
Aanwijzertypen nemen niet over van object en er bestaan geen conversies tussen aanwijzertypen en object
. Bovendien bieden boxing en unboxing geen ondersteuning voor pointers. U kunt echter converteren tussen verschillende typen aanwijzers en tussen aanwijzertypen en integrale typen.
Wanneer u meerdere aanwijzers in dezelfde declaratie declareert, schrijft u het sterretje (*
) samen met het onderliggende type. Het wordt niet gebruikt als voorvoegsel voor elke aanwijzernaam. Bijvoorbeeld:
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
De garbagecollector houdt niet bij of een object wordt verwezen door eventuele aanwijzertypen. Als het referrant een object in de beheerde heap is (inclusief lokale variabelen die zijn vastgelegd door lambda-expressies of anonieme gemachtigden), moet het object worden vastgemaakt zolang de aanwijzer wordt gebruikt.
De waarde van de aanwijzervariabele van het type MyType*
is het adres van een variabele van het type MyType
. Hier volgen voorbeelden van declaraties van aanwijzertypen:
-
int* p
:p
is een aanwijzer naar een geheel getal. -
int** p
:p
is een aanwijzer naar een geheel getal. -
int*[] p
:p
is een eendimensionale matrix met aanwijzers naar gehele getallen. -
char* p
:p
is een aanwijzer naar een teken. -
void* p
:p
is een aanwijzer naar een onbekend type.
De operator voor aanwijzer indirectie *
kan worden gebruikt om toegang te krijgen tot de inhoud op de locatie waar de aanwijzervariabele naar wijst. Denk bijvoorbeeld aan de volgende declaratie:
int* myVariable;
De expressie *myVariable
geeft de int
variabele aan die is gevonden op het adres in myVariable
.
Er zijn verschillende voorbeelden van verwijzingen in de artikelen over de fixed
verklaring. In het volgende voorbeeld worden het unsafe
trefwoord en de fixed
-instructie gebruikt en wordt uitgelegd hoe u een binnenaanwijzer kunt verhogen. U kunt deze code in de hoofdfunctie van een consoletoepassing plakken om deze uit te voeren. Deze voorbeelden moeten worden gecompileerd met de AllowUnsafeBlocks compileroptie.
// 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
*/
U kunt de indirectieoperator niet toepassen op een aanwijzer van het type void*
. U kunt echter een cast gebruiken om een void-pointer te converteren naar elk ander type aanwijzer en omgekeerd.
Een aanwijzer kan null
zijn. Het toepassen van de indirectieoperator op een null-aanwijzer veroorzaakt een door de implementatie gedefinieerd gedrag.
Het doorgeven van aanwijzers tussen methoden kan niet-gedefinieerd gedrag veroorzaken. Overweeg een methode die een aanwijzer naar een lokale variabele retourneert via een in
, out
of ref
parameter of als het functieresultaat. Als de aanwijzer is ingesteld in een vast blok, kan de variabele waarop deze wijst mogelijk niet meer worden opgelost.
De volgende tabel bevat de operators en instructies die kunnen worden uitgevoerd op aanwijzers in een onveilige context:
Operator/instructie | Gebruik |
---|---|
* |
Voert aanwijzer indirectie uit. |
-> |
Verwijst naar een lid van een struct via een aanwijzer. |
[] |
Indexeert een aanwijzer. |
& |
Hiermee haalt u het adres van een variabele op. |
++ en -- |
Aanwijzers verhogen en verlagen. |
+ en - |
Voert pointer-arithmetic uit. |
== , != , < , > , <= en >= |
Vergelijkt aanwijzers. |
stackalloc |
Wijst geheugen toe aan de stack. |
fixed verklaring |
Corrigeert tijdelijk een variabele, zodat het adres ervan kan worden gevonden. |
Voor meer informatie over aanwijzer-gerelateerde operatoren, zie .
Elk type aanwijzer kan impliciet worden geconverteerd naar een void*
type. Elke aanwijzertype kan de waarde null
toegewezen krijgen. Elk type aanwijzer kan expliciet worden geconverteerd naar elk ander type aanwijzer met behulp van een cast-expressie. U kunt ook elk integraal type converteren naar een pointertype, of een pointertype naar een integraal type. Voor deze conversies is een expliciete cast vereist.
In het volgende voorbeeld wordt een int*
geconverteerd naar een byte*
. U ziet dat de aanwijzer verwijst naar de laagst geadresseerde byte van de variabele. Wanneer u het resultaat achtereenvolgens vergroot tot de grootte van int
(4 bytes), kunt u de resterende bytes van de variabele weergeven.
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
*/
}
Buffers met vaste grootte
U kunt het trefwoord fixed
gebruiken om een buffer te maken met een matrix met een vaste grootte in een gegevensstructuur. Buffers met een vaste grootte zijn handig wanneer u methoden schrijft die samenwerken met gegevensbronnen uit andere talen of platforms. De buffer met vaste grootte kan alle kenmerken of modifiers aannemen die zijn toegestaan voor gewone struct-leden. De enige beperking is dat het matrixtype moet worden bool
, byte
, char
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, float
of double
.
private fixed char name[30];
In veilige code bevat een C#-struct die een matrix bevat niet de matrixelementen. De struct bevat in plaats daarvan een verwijzing naar de elementen. U kunt een matrix met vaste grootte insluiten in een struct wanneer deze wordt gebruikt in een onveilig codeblok.
De grootte van de volgende struct
is niet afhankelijk van het aantal elementen in de matrix, omdat pathName
een verwijzing is:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
Een struct kan een ingesloten matrix bevatten in onveilige code. In het volgende voorbeeld heeft de fixedBuffer
matrix een vaste grootte. U gebruikt een fixed
instructie om een aanwijzer naar het eerste element te krijgen. U opent de elementen van de matrix via deze aanwijzer. Met de instructie fixed
wordt het fixedBuffer
-exemplaarveld vastgemaakt aan een specifieke locatie in het geheugen.
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]);
}
}
De grootte van de 128-element char
array is 256 bytes. Vaste grootte teken buffers nemen altijd 2 bytes per teken in beslag, ongeacht de codering. Deze arraygrootte is hetzelfde, zelfs wanneer char-buffers naar API-methoden of structs met CharSet = CharSet.Auto
of CharSet = CharSet.Ansi
worden gemarshald. Zie CharSetvoor meer informatie.
In het voorgaande voorbeeld wordt getoond hoe je toegang krijgt tot fixed
velden zonder vastzetten. Een andere veelgebruikte matrix met vaste grootte is de bool matrix. De elementen in een bool
matrix zijn altijd 1 byte groot.
bool
matrices zijn niet geschikt voor het maken van bitmatrices of buffers.
Buffers met vaste grootte worden gecompileerd met het System.Runtime.CompilerServices.UnsafeValueTypeAttribute, waarmee de Common Language Runtime (CLR) wordt geïnstrueerd dat een type een onbeheerde array bevat die mogelijk kan overstromen. Geheugen toegewezen met behulp van stackalloc schakelt ook automatisch detectiefuncties voor bufferoverschrijding in de CLR in. In het vorige voorbeeld ziet u hoe een buffer met een vaste grootte in een unsafe struct
kan bestaan.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
De door de compiler gegenereerde C# voor Buffer
wordt als volgt toegeschreven:
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;
}
Buffers met vaste grootte verschillen van reguliere matrices op de volgende manieren:
- Mag alleen worden gebruikt in een
unsafe
context. - Dit kunnen alleen instantievelden van structs zijn.
- Ze zijn altijd vectoren of eendimensionale matrices.
- De declaratie moet de lengte bevatten, zoals
fixed char id[8]
. U kuntfixed char id[]
niet gebruiken.
Aanwijzers gebruiken om een matrix van bytes te kopiëren
In het volgende voorbeeld worden aanwijzers gebruikt om bytes van de ene matrix naar de andere te kopiëren.
In dit voorbeeld wordt het onveilige trefwoord gebruikt, waarmee u aanwijzers kunt gebruiken in de methode Copy
. De fixed statement wordt gebruikt om pointers naar de bron- en doelarrays te declareren. De fixed
-instructie pint de locatie van de bron- en doellijsten in het geheugen, zodat garbage collection de lijsten niet verplaatst. De geheugenblokken voor de matrices worden losgemaakt wanneer het fixed
blok is voltooid. Omdat de methode Copy
in dit voorbeeld gebruikmaakt van het trefwoord unsafe
, moet deze worden gecompileerd met de optie AllowUnsafeBlocks compiler.
In dit voorbeeld worden de elementen van beide matrices geopend met behulp van indexen in plaats van een tweede onbeheerde aanwijzer. Met de declaratie van de pSource
en pTarget
aanwijzers worden de matrices vastgemaakt.
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
*/
}
Functiewijzers
C# biedt delegate
typen voor het definiëren van veilige functiepointerobjecten. Het aanroepen van een gemachtigde omvat het instantiëren van een type dat is afgeleid van System.Delegate en het aanroepen van een virtuele methode naar de Invoke
methode. Deze virtuele aanroep maakt gebruik van de callvirt
IL-instructie. In prestatiekritieke codepaden is het gebruik van de calli
IL-instructie efficiënter.
U kunt een functiepointer definiëren met behulp van de delegate*
syntaxis. De compiler roept de functie aan met behulp van de instructie calli
in plaats van een delegate
-object te instantiëren en Invoke
aan te roepen. De volgende code declareert twee methoden die gebruikmaken van een delegate
of een delegate*
om twee objecten van hetzelfde type te combineren. De eerste methode maakt gebruik van een System.Func<T1,T2,TResult> type gemachtigde. De tweede methode maakt gebruik van een delegate*
-declaratie met dezelfde parameters en retourtype:
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);
De volgende code laat zien hoe u een statische lokale functie declareert en de methode UnsafeCombine
aanroept met behulp van een aanwijzer naar die lokale functie:
int product = 0;
unsafe
{
static int localMultiply(int x, int y) => x * y;
product = UnsafeCombine(&localMultiply, 3, 4);
}
De voorgaande code illustreert een aantal van de regels voor de functie die als functiepointer wordt geopend:
- Functieaanwijzers kunnen alleen worden gedeclareerd in een
unsafe
context. - Methoden die een
delegate*
(of eendelegate*
retourneren) kunnen alleen worden aangeroepen in eenunsafe
context. - De operator
&
voor het verkrijgen van het adres van een functie is alleen toegestaan voorstatic
functies. (Deze regel is van toepassing op zowel lidfuncties als lokale functies).
De syntaxis bevat parallellen met het declareren van delegate
typen en het gebruik van aanwijzers. Het achtervoegsel *
op delegate
geeft aan dat de declaratie een functieaanwijzer is. De &
bij het toewijzen van een methodegroep aan een functiepointer geeft aan dat de bewerking het adres van de methode gebruikt.
U kunt de oproepconventie voor een delegate*
opgeven met behulp van de trefwoorden managed
en unmanaged
. Daarnaast kunt u voor unmanaged
functie-aanwijzers de aanroepconventie opgeven. In de volgende declaraties ziet u voorbeelden van elk van deze declaraties. De eerste declaratie maakt gebruik van de managed
oproepconventie. Dit is de standaardinstelling. De volgende vier gebruiken een unmanaged
-aanroepconventie. Elk geeft een van de ECMA 335-aanroepconventies op: Cdecl
, Stdcall
, Fastcall
of Thiscall
. De laatste declaratie maakt gebruik van de unmanaged
oproepconventie, waarbij de CLR wordt geïnstrueerd om de standaardaanroepconventie voor het platform te kiezen. De CLR kiest de oproepconventie tijdens runtime.
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);
Meer informatie over functiepointers vindt u in de functiespecificatie Functie-aanwijzer.
C#-taalspecificatie
Zie het hoofdstuk Onveilige code van de C#-taalspecificatievoor meer informatie.