NullReferenceException, como ele acontece?
Tags: dotnet, dotnetcore, performance
Published: 2023-03-15 17:23:24

Um dos erros mais comuns em aplicações .NET é a NullReferenceException. Esse erro ocorre quando tentamos acessar uma propriedade ou método de um objeto que ainda não foi instanciado e, portanto, tem um valor "null" na memória.

Mas como o .NET reconhece esse erro e gera a exceção? Vamos analisar um código simples e verificar a IL gerada.

```csharp
using System;

public class Program
{
    private static void Main(string[] args)
    {
        string nome = null;
        ImprimirTamanho(nome);
    }

    private static void ImprimirTamanho(string s)
    {
        Console.WriteLine(s.Length);
    }
}
```
IL do código (parte)

```il
  // Methods
    .method private hidebysig static 
        void Main (
            string[] args
        ) cil managed 
    {
        // Method begins at RVA 0x209d
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldnull
        IL_0001: call void Program::ImprimirTamanho(string)
        IL_0006: ret
    } // end of method Program::Main

    .method private hidebysig static 
        void ImprimirTamanho (
            string s
        ) cil managed 
    {
        // Method begins at RVA 0x20a5
        // Code size 12 (0xc)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: callvirt instance int32 [System.Runtime]System.String::get_Length()
        IL_0006: call void [System.Console]System.Console::WriteLine(int32)
        IL_000b: ret
    } // end of method Program::ImprimirTamanho
```

Para entender como o .NET reconhece o erro, vamos analisar a IL gerada pelo compilador. Na linha IL_0000, do método Program::Main, o comando ldnull coloca uma referência nula na pilha de avaliação.

Em seguida, o método Program::ImprimirTamanho é invocado e o comando ldarg.0 é responsável por carregar o valor do argumento na pilha de avaliação.

O erro ocorre na linha IL_0001, quando o comando callvirt tenta chamar o método get_Length() de uma referência nula que está na pilha. Isso gera a NullReferenceException.

Para entender melhor como o código está funcionando, vamos analisar o assembly gerado. Mais especificamente, vamos focar na linha:

` L0000: mov ecx, [rcx+8]:`

```asm
Program.ImprimirTamanho(System.String)
    L0000: mov ecx, [rcx+8]
    L0003: mov rax, 0x7ffed9667b70
    L000d: jmp qword ptr [rax]
```

Em termos simples, a instrução está dizendo: "pegue o valor na posição de memória que está localizada 8 bytes depois do endereço contido em rcx e coloque esse valor no registrador ecx".

É importante observar que a sintaxe [rcx+8] significa que estamos referenciando um endereço de memória que é calculado a partir do valor armazenado em rcx. O valor 8 representa o deslocamento (offset) em relação ao endereço apontado por rcx.

![Registradores](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3t0pllwtzn7l5m3ofekh.png)

Agora, vamos analisar o valor do registrador rcx no momento em que o método ImprimirTamanho é executado. Como pode ser visto na imagem acima, ele está zerado!

Levando todos os pontos apresentados em consideração, podemos concluir que a instrução assembly **"mov ecx, [rcx+8]"** tenta acessar o endereço de memória 0x0000000000000008 (64 Bits). No entanto, esse endereço não está alocado para o processo em questão e, portanto, essa tentativa de acesso resultará em uma violação de acesso à memória.

No Windows e no Linux, os primeiros 4KB de memória virtual de um processo são reservados e protegidos pelo sistema operacional. Essa região de memória é conhecida como "página zero" e é protegida para evitar que programas maliciosos explorem vulnerabilidades de segurança.

A página zero é uma área crítica da memória e contém informações importantes do sistema, como a tabela de vetores de interrupção e a tabela de páginas do kernel. A proteção da página zero é fundamental para garantir a integridade do sistema.

Além disso, essa região de memória é protegida para garantir que o sistema operacional possa detectar e tratar erros comuns em programas, como tentativas de acessar endereços de memória não alocados ou não inicializados. Se um programa tentar acessar um endereço de memória dentro dessa região, o sistema operacional geralmente responderá gerando uma exceção de violação de acesso à memória. Essa exceção pode ser traduzida em um NullReferenceException pela CLR. 

Agora que entendemos como ocorre o NullReferenceException, podemos olhar de forma mais aprofundada para a representação do valor NULL em um endereço de memória. A palavra NULL é utilizada para indicar que não existe uma instância alocada naquele endereço de memória, mas ela não é armazenada na memória com muitos pensam.

Até a próxima!