Compartilhar via


Código não seguro, tipos de ponteiro e ponteiros de função

A maior parte do código C# que você escreve é "código verificávelmente seguro". código verificávelmente seguro significa que as ferramentas do .NET podem verificar se o código é seguro. Em geral, o código seguro não acessa diretamente a memória usando ponteiros. Ele também não aloca memória bruta. Em vez disso, ele cria objetos gerenciados.

O C# dá suporte a um contexto unsafe, no qual você pode escrever código não verificável. Em um contexto unsafe, o código pode usar ponteiros, alocar e liberar blocos de memória e chamar métodos usando ponteiros de função. Código não seguro em C# não é necessariamente perigoso; é apenas um código cuja segurança não pode ser verificada.

O código não seguro tem as seguintes propriedades:

  • Métodos, tipos e blocos de código podem ser definidos como não seguros.
  • Em alguns casos, o código não seguro pode aumentar o desempenho de um aplicativo removendo verificações de limites de matriz.
  • Código não seguro é necessário quando você chama funções nativas que exigem ponteiros.
  • O uso de código não seguro introduz riscos de segurança e estabilidade.
  • O código que contém blocos não seguros deve ser compilado com a opção do compilador AllowUnsafeBlocks.

Tipos de ponteiro

Em um contexto não seguro, os tipos podem ser de ponteiro, além de tipo de valor ou tipo de referência. Uma declaração de tipo de ponteiro usa uma das seguintes formas:

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

O tipo especificado antes do * em um tipo de ponteiro é chamado de tipo referent.

Os tipos de ponteiro não são herdados de object e não há conversão entre tipos de ponteiro e object. Além disso, as conversões boxing e unboxing não oferecem suporte a ponteiros. No entanto, você pode converter entre diferentes tipos de ponteiro e tipos de ponteiro e tipos integrais.

Ao declarar vários ponteiros na mesma declaração, você escreve o asterisco (*) junto apenas com o tipo subjacente. Ele não é usado como um prefixo para cada nome de ponteiro. Por exemplo:

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

O coletor de lixo não se dá conta de que um objeto está sendo apontado por qualquer tipo de ponteiro. Se o referenciante for um objeto no heap gerenciado (incluindo variáveis locais capturadas por expressões lambda ou delegados anônimos), o objeto deverá ser fixado enquanto o ponteiro for usado.

O valor da variável de ponteiro do tipo MyType* é o endereço de uma variável do tipo MyType. Estes são exemplos de declarações de tipos de ponteiro:

  • int* p: p é um ponteiro de um inteiro.
  • int** p: p é um ponteiro para um ponteiro de um inteiro.
  • int*[] p: p é uma matriz unidimensional de ponteiros para inteiros.
  • char* p: p é um ponteiro de um caractere.
  • void* p: p é um ponteiro de um tipo desconhecido.

O operador de indireção do ponteiro * pode ser usado para acessar o conteúdo no local apontado pela variável de ponteiro. Por exemplo, considere a seguinte declaração:

int* myVariable;

A expressão *myVariable indica a variável int encontrada no endereço contido no myVariable.

Há vários exemplos de ponteiros nos artigos sobre a fixed instrução. O exemplo a seguir usa a palavra-chave unsafe e a instrução fixed e mostra como incrementar um ponteiro interior. Você pode colar esse código na função Principal de um aplicativo de console para executá-lo. Esses exemplos devem ser compilados com o conjunto de opções do compilador 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
*/

Não é possível aplicar o operador de indireção a um ponteiro do tipo void*. No entanto, você pode usar uma conversão para converter um ponteiro nulo em qualquer outro tipo de ponteiro e vice-versa.

Um ponteiro pode ser null. Aplicar o operador de indireção a um ponteiro nulo causa um comportamento definido pela implementação.

Passar ponteiros entre métodos pode causar um comportamento indefinido. Considere um método que retorna um ponteiro para uma variável local por meio de um parâmetro in, outou ref ou como resultado da função. Se o ponteiro foi definido em um bloco fixo, a variável para a qual ele aponta pode não ser mais corrigida.

A tabela a seguir lista os operadores e instruções que podem operar em ponteiros em um contexto não seguro:

Operador/declaração Usar
* Executa indireção de ponteiro.
-> Acessa um membro de um struct através de um ponteiro.
[] Indexa um ponteiro.
& Obtém o endereço de uma variável.
++ e -- Incrementa e decrementa ponteiros.
+ e - Executa aritmética de ponteiros.
==, !=, <, >, <=e >= Compara ponteiros.
stackalloc Aloca memória na pilha.
fixed instrução Corrige temporariamente uma variável para que seu endereço possa ser encontrado.

Para obter mais informações sobre operadores relacionados a ponteiros, confira Operadores relacionados a ponteiros.

Qualquer tipo de ponteiro pode ser convertido em um tipo void* implicitamente. Qualquer tipo de ponteiro pode receber o valor null. Qualquer tipo de ponteiro pode ser convertido explicitamente em qualquer outro tipo de ponteiro usando uma expressão de conversão. Também é possível converter qualquer tipo integral em um tipo de ponteiro ou qualquer tipo de ponteiro em um tipo integral. Essas conversões exigem uma conversão explícita.

O exemplo a seguir converte um int* em um byte*. Observe que o ponteiro aponta para o byte endereçado mais baixo da variável. Quando você incrementa sucessivamente o resultado, até o tamanho de int (4 bytes), você pode exibir os bytes restantes da variável.

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 de tamanho fixo

Você pode usar a palavra-chave fixed para criar um buffer com uma matriz de tamanho fixo em uma estrutura de dados. Buffers de tamanho fixo são úteis quando você escreve métodos que interoperam com fontes de dados de outras linguagens ou plataformas. O buffer de tamanho fixo pode aceitar quaisquer atributos ou modificadores permitidos para membros de struct regulares. A única restrição é que o tipo de matriz deve ser bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, floatou double.

private fixed char name[30];

No código seguro, um struct C# que contém uma matriz não contém os elementos da matriz. Em vez disso, o struct contém uma referência aos elementos. Você pode inserir uma matriz de tamanho fixo em um struct quando ele é usado em um bloco de código não seguro.

O tamanho da seguinte struct não depende do número de elementos na matriz, pois pathName é uma referência:

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

Um struct pode conter uma matriz inserida em código não seguro. No exemplo a seguir, a matriz fixedBuffer tem um tamanho fixo. Você usa uma fixed instrução para estabelecer um ponteiro para o primeiro elemento. Você acessa os elementos da matriz por meio deste ponteiro. A instrução fixed fixa o campo de instância fixedBuffer em um local específico em memória.

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]);
    }
}

O tamanho da matriz char com 128 elementos é de 256 bytes. Os buffers de caracteres de tamanho fixo sempre utilizam 2 bytes por caractere, independentemente da codificação. Esse tamanho de matriz é igual mesmo quando se realiza marshaling nos buffers de char para structs ou métodos de API com CharSet = CharSet.Auto ou CharSet = CharSet.Ansi. Para obter mais informações, consulte CharSet.

O exemplo anterior demonstra o acesso a campos fixed sem fixação. Outra matriz de tamanho fixo comum é a matriz bool. Os elementos em uma matriz bool têm sempre 1 byte de tamanho. bool arrays não são adequados para criar arrays de bits ou buffers.

Os buffers de tamanho fixo são compilados com System.Runtime.CompilerServices.UnsafeValueTypeAttribute, o que instrui o CLR (Common Language Runtime) que um tipo contém uma matriz não gerenciada com o potencial de estourar. A memória alocada usando stackalloc também habilita automaticamente os recursos de detecção de estouro de buffer no CLR. O exemplo anterior mostra como um buffer de tamanho fixo pode existir em um unsafe struct.

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

O C# gerado pelo compilador para Buffer é atribuído da seguinte maneira:

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;
}

Os buffers de tamanho fixo diferem das matrizes regulares das seguintes maneiras:

  • Só pode ser usado em contextos unsafe.
  • Pode ser apenas campos de instância de structs.
  • Eles são sempre vetores ou matrizes unidimensionais.
  • A declaração deve incluir o comprimento, como fixed char id[8]. Você não pode usar fixed char id[].

Como usar ponteiros para copiar uma matriz de bytes

O exemplo a seguir usa ponteiros para copiar bytes de uma matriz para outra.

Este exemplo usa a palavra-chave não segura, que permite usar ponteiros no método Copy. A instrução fixa é usada para declarar ponteiros para as matrizes de origem e destino. A instrução fixed fixa o local das matrizes de origem e destino na memória para que a coleta de lixo não as mova. Os blocos de memória para as matrizes não serão fixado quando o bloco fixed for concluído. Como o método Copy neste exemplo usa a palavra-chave unsafe, ele deve ser compilado com a opção do compilador AllowUnsafeBlocks.

Este exemplo acessa os elementos de ambas as matrizes usando índices em vez de um segundo ponteiro não gerenciado. A declaração dos ponteiros pSource e pTarget fixa as matrizes.

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
    */
}

Ponteiros de função

O C# fornece tipos delegate para definir objetos de ponteiro de função seguros. Invocar um delegado envolve instanciar um tipo derivado de System.Delegate e fazer uma chamada de método virtual para seu método Invoke. Esta chamada virtual usa a instrução IL callvirt. Em caminhos de código críticos de desempenho, usar a instrução il calli é mais eficiente.

Você pode definir um ponteiro de função usando a sintaxe delegate*. O compilador chama a função usando a instrução calli em vez de instanciar um objeto delegate e chamar Invoke. O código a seguir declara dois métodos que usam um delegate ou um delegate* para combinar dois objetos do mesmo tipo. O primeiro método usa um tipo de delegado System.Func<T1,T2,TResult>. O segundo método usa uma declaração delegate* com os mesmos parâmetros e tipo de retorno:

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);

O código a seguir mostra como você declararia uma função local estática e invocaria o método UnsafeCombine usando um ponteiro para essa função local:

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

O código anterior ilustra várias das regras sobre a função quando acessada por meio de um ponteiro de função:

  • Ponteiros de função só podem ser declarados em contextos unsafe.
  • Métodos que tomam um delegate* (ou retornam um delegate*) só podem ser chamados em um contexto de unsafe.
  • O operador & para obter o endereço de uma função só é permitido em funções de static. (Essa regra se aplica a funções membro e funções locais).

A sintaxe tem paralelos com a declaração de tipos delegate e o uso de ponteiros. O sufixo * em delegate indica que a declaração é um ponteiro de função. O &, ao atribuir um grupo de métodos a um ponteiro de função, indica que a operação toma o endereço do método.

Você pode especificar a convenção de chamada para um delegate* usando as palavras-chave managed e unmanaged. Além disso, para ponteiros de função unmanaged, você pode especificar a convenção de chamada. As declarações a seguir mostram exemplos de cada um. A primeira declaração usa a convenção de chamada managed, que é o padrão. As quatro a seguir usam a convenção de chamada unmanaged. Cada uma especifica uma das convenções de chamada do ECMA 335: Cdecl, Stdcall, Fastcallou Thiscall. A última declaração usa a convenção de chamada unmanaged, instruindo o CLR a escolher a convenção de chamada padrão para a plataforma. O CLR escolhe a convenção de chamada em tempo de execução.

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);

Saiba mais sobre ponteiros de função na especificação de recurso Ponteiro de função.

Especificação da linguagem C#

Para obter mais informações, confira o capítulo Código não seguro da Especificação da linguagem C#.