Guia de rastreamento dinâmico Solaris

Capítulo 5 Ponteiros e matrizes

Ponteiros são endereços de memória de objetos de dados no kernel do sistema operacional ou no espaço de endereço de um processo do usuário. D ajuda você a criar e manipular ponteiros, além de armazená-los em variáveis e matrizes de associação. Este capítulo descreve a sintaxe D dos ponteiros, os operadores que podem ser aplicados para criar ou acessar ponteiros e a relação entre ponteiros e matrizes escalares de tamanho fixo. Também são discutidos problemas relacionados ao uso dos ponteiros em espaços de endereço diferentes.


Observação –

Se você for um programador experiente em C ou C++, poderá ler rapidamente este capítulo, pois a sintaxe de ponteiro de D é a mesma sintaxe ANSI-C correspondente. Você deve ler o Ponteiros para objetos do DTrace e o Ponteiros e espaços de endereço, pois eles descrevem os recursos e os problemas específicos do DTrace.


Ponteiros e endereços

O sistema operacional Solaris usa uma técnica chamada memória virtual para fornecer a cada usuário os processos com sua própria visão virtual dos recursos de memória do sistema. Uma visão virtual dos recursos de memória é chamada de espaço de endereço, que associa um intervalo de valores de endereço ([0 ... 0xffffffff] para um espaço de endereço de 32 bits ou [0 ... 0xffffffffffffffff] para um espaço de endereço de 64 bits) a um conjunto de translações que o sistema operacional e o hardware usam para converter cada endereço virtual em um local de memória física correspondente. Os ponteiros em D são objetos de dados que armazenam um valor de endereço virtual de inteiro e associa-o a um tipo de D que descreve o formato dos dados armazenados no local correspondente da memória.

Você pode declarar uma variável de D como o tipo de ponteiro, especificando primeiro o tipo dos dados referenciados e, em seguida, acrescentando um asterisco ( *) ao nome do tipo para indicar que deseja declarar um tipo de ponteiro. Por exemplo, a declaração:

int *p;

declara uma variável de D global chamada p que é um ponteiro para um inteiro. Esta declaração significa que a própria p é um inteiro de 32 ou 64 bits, cujo valor é o endereço de outro inteiro localizado em algum lugar da memória. Como o formato compilado do seu código de D é executado na hora em que o teste é acionado dentro do próprio kernel do sistema operacional, os ponteiros de D são geralmente associados ao espaço de endereço do kernel. Você pode usar o comando isainfo(1) -b para determinar o número de bits usado para ponteiros pelo kernel ativo do sistema operacional.

Se você quiser criar um ponteiro para um objeto de dados dentro do kernel, calcule seu endereço usando o operador &. Por exemplo, o código-fonte do kernel do sistema operacional declara um int kmem_flags ajustável. Você poderia rastrear o endereço deste int rastreando o resultado da aplicação do operador & ao nome desse objeto em D:

trace(&`kmem_flags);

O operador * pode ser usado para fazer referência ao objeto endereçado pelo ponteiro, e age inversamente ao operador &. Por exemplo, os dois fragmentos de código de D seguintes têm significados equivalentes:

p = &`kmem_flags;				trace(`kmem_flags);
trace(*p);

O fragmento esquerdo cria um ponteiro de variável de D global p. Como o objeto kmem_flags é do tipo int, o tipo do resultado de &`kmem_flags é int * (ou seja, o ponteiro para int). O fragmento esquerdo rastreia o valor de *p, que segue o ponteiro de volta para o objeto de dados kmem_flags. Portanto, esse fragmento é o mesmo que o fragmento direito, que simplesmente rastreia o valor do objeto de dados diretamente através do seu nome.

Segurança de ponteiro

Se você for um programador em C ou C++, talvez esteja um pouco assustado depois de ler a seção anterior porque sabe que o uso incorreto de ponteiros ponde fazer com que os seus programas travem. O DTrace é um ambiente seguro e robusto para executar seus programas em D, onde esses erros não podem fazer com que o seu programa trave. Você pode até escrever um programa em D com erros, mas acessos inválidos ao ponteiro de D não farão com que o DTrace ou o kernel do sistema operacional falhe ou trave. Em vez disso, o software do DTrace detectará quaisquer acessos inválidos ao ponteiro, desativará sua instrumentação e informará o problema para que você faça a depuração.

Se você programou na linguagem de programação Java, provavelmente sabe que a linguagem Java não aceita ponteiros exatamente pelos mesmos motivos de segurança. Os ponteiros são necessários em D porque eles são uma parte intrínseca da implementação do sistema operacional em C, mas o DTrace implementa os mesmos tipos de mecanismo de segurança encontrados na linguagem de programação Java que evitam que programas com erro causem danos neles mesmos ou em outros programas. O relatório de erros do DTrace é semelhante ao ambiente de tempo de execução da linguagem de programação Java, que detecta um erro de programação e informa uma exceção para você.

Para ver o relatório e a manipulação de erros do DTrace, escreva deliberadamente um programa D ruim usando ponteiros. Em um editor, digite o seguinte programa em D e salve-o em um arquivo chamado badptr.d:


Exemplo 5–1 badptr.d: demonstração de manipulação de erros do DTrace

BEGIN
{
	x = (int *)NULL;
	y = *x;
	trace(y);
}

O programa badptr.d cria um ponteiro de D chamado x que é um ponteiro para int. O programa atribui a esse ponteiro o valor de ponteiro inválido especial NULL, que é um alias interno para o endereço 0. Por convenção, o endereço 0 é sempre definido como inválido, para que NULL possa ser usado como um valor de sentinela em programas em C e em D. O programa usa uma expressão de conversão para converter NULL a fim de que seja usado como um ponteiro para um inteiro. O programa cancela a referência ao ponteiro usando a expressão *x, atribui o resultado a outra variável y e tenta rastrear y. Quando o programa em D é executado, o DTrace detecta um acesso inválido ao ponteiro quando a declaração y = *x é executada e informa o erro:


# dtrace -s badptr.d
dtrace: script '/dev/stdin' matched 1 probe
CPU     ID                    FUNCTION:NAME
dtrace: error on enabled probe ID 1 (ID 1: dtrace:::BEGIN): invalid address
(0x0) in action #2 at DIF offset 4
dtrace: 1 error on CPU 0
^C
#

O outro problema que pode surgir dos programas que usam ponteiros inválidos é um erro de alinhamento. Por convenção arquitetural, os objetos de dados fundamentais, tais como inteiros, são alinhados na memória de acordo com o su tamanho. Por exemplo, inteiros de 2 bytes são alinhados em endereços que são múltiplos de 2, inteiros de 4 bytes em múltiplos de 4, e assim por diante. Se você cancelar a referência de um ponteiro a um inteiro de 4 bytes e o endereço do ponteiro for um valor inválido que não seja múltiplo de 4, o acesso falhará devido a um erro de alinhamento. Os erros de alinhamento em D quase sempre indicam que o ponteiro possui um valor inválido ou corrompido devido a um erro em seu programa em D. Você pode criar um erro de alinhamento de exemplo, alterando o código-fonte de badptr.d para usar o endereço (int *)2 em vez de NULL. Como int é de 4 bytes e 2 não é um múltiplo de 4, a expressão *x resulta em um erro de alinhamento do DTrace.

Para obter detalhes sobre o mecanismo de erro do DTrace, consulte Teste ERROR.

Declarações e armazenamento de matriz

D oferece suporte a matrizes escalares além das matrizes de associação dinâmicas descritas no Capítulo 3. As matrizes escalares são um grupo de tamanho fixo de locais de memória consecutivos que armazenam um valor do mesmo tipo. As matrizes escalares são acessadas quando se faz referência a cada local que tenha um inteiro começando a partir de zero. As matrizes escalares correspondem diretamente em conceito e sintaxe às matrizes em C e C++. As matrizes escalares não são usadas tão freqüentemente em D como as matrizes de associação e suas contrapartes mais avançadas, as agregações, mas elas às vezes são necessárias durante o acesso a estruturas de dados de matriz existentes do sistema operacional declaradas em C. As agregações são descritas no Capítulo 9Agregações.

Uma matriz escalar de D de 5 inteiros será declarada através do tipo int e da colocação de um sufixo na declaração com o número de elementos entre colchetes, da seguinte forma:

int a[5];

O diagrama seguinte mostra uma representação visual do armazenamento da matriz:

Figura 5–1 Representação de matriz escalar

O diagrama mostra uma imagem de uma matriz de cinco objetos.

A expressão de D a[0] é usada para fazer referência ao primeiro elemento da matriz, a[1] se refere ao segundo, e assim por diante. De uma perspectiva sintática, as matrizes escalares e as matrizes de associação são muito semelhantes. Você pode declarar uma matriz associativa de cinco inteiros referenciada por uma chave de inteiro, da seguinte forma:

int a[int];

e também pode referenciar essa matriz usando a expressão a[0]. Mas de uma perspectiva de armazenamento e de implementação, as duas matrizes são muito diferentes. A matriz estática a consiste em cinco locais de memória consecutivos numerados a partir de zero e o índice se refere a um desvio no armazenamento alocado da matriz. Por outro lado, uma matriz associativa não tem tamanho predefinido e não armazena elementos em locais de memória consecutivos. Além disso, as chaves de matriz de associação não têm relação com o local de armazenamento do valor correspondente. Você pode acessar os elementos de matriz associativa a[0] e a[-5], e apenas duas palavras de armazenamento, que podem ou não ser consecutivas, serão alocadas pelo DTrace. As chaves de matriz associativa são nomes abstratos do valor correspondente que não tem relação com os locais de armazenamento de valor.

Se você criar uma matriz usando uma atribuição inicial e utilizar uma única expressão de inteiro como o índice de matriz (por exemplo, a[0] = 2), o compilador de D sempre criará uma nova matriz associativa, embora nessa expressão a também pudesse ser interpretado como uma atribuição a uma matriz escalar. As matrizes escalares devem ser declaradas previamente nesta situação, para que o compilador de D possa ver a definição do tamanho da matriz e deduzir que a matriz é uma matriz escalar.

Relação entre ponteiro e matriz

Os ponteiros e as matrizes possuem uma relação especial em D, assim como em ANSI-C. Uma matriz é representada por uma variável que é associada ao endereço do seu primeiro local de armazenamento. Um ponteiro também é o endereço de um local de armazenamento com um tipo definido, sendo assim, D permite o uso da notação de índice [ ] de matriz com variáveis de ponteiro e variáveis de matriz. Por exemplo, os dois fragmentos de D seguintes têm significados semelhantes:

p = &a[0];				trace(a[2]);
trace(p[2]);

No fragmento esquerdo, o ponteiro p é atribuído ao endereço do primeiro elemento de matriz em a por meio da aplicação do operador & à expressão a[0]. A expressão p[2] rastreia o valor do terceiro elemento da matriz (índice 2). Como p agora contém o mesmo endereço associado a a, esta expressão possui o mesmo valor que a[2], mostrado no fragmento direito. Uma conseqüência dessa equivalência é que C e D permitem que você acesse qualquer índice de qualquer ponteiro ou matriz. O compilador ou o ambiente de tempo de execução do DTrace não realizam a verificação dos limites da matriz para você. Se você acessar a memória após um valor predefinido de uma matriz, obterá um resultado inesperado ou o DTrace reportará um erro de endereço inválido, como mostrado no exemplo anterior. Como sempre, você não pode danificar o próprio DTrace ou o sistema operacional, mas precisará depurar o seu programa em D.

A diferença entre ponteiros e matrizes é que uma variável de ponteiro se refere a um pedaço separado do armazenamento que contém o endereço do inteiro de algum outro armazenamento. Uma variável nomeia o próprio armazenamento da matriz, não o local de um inteiro que em compensação contém o local da matriz. Esta diferença é ilustrada no diagrama seguinte:

Figura 5–2 Armazenamento de ponteiro e de matriz

O diagrama mostra um ponteiro para uma matriz de cinco objetos.

Esta diferença será manifestada na sintaxe de D, se você tentar atribuir ponteiros e matrizes escalares. Se x e y forem variáveis de ponteiro, a expressão x = y será legal. Ela simplesmente copia o endereço do ponteiro em y para o local de armazenamento nomeado por x. Se x e y forem variáveis de matriz escalar, a expressão x = y não será legal. As matrizes não podem ser atribuídas como um todo em D. Entretanto, uma variável de matriz ou nome de símbolo pode ser usado em qualquer contexto em que um ponteiro seja permitido. Se p for um ponteiro e a for uma matriz, a declaração p = a será permitida; essa declaração equivale a p = &a[0].

Aritmética de ponteiro

Já que os ponteiros são apenas inteiros usados como endereços de outros objetos na memória, D fornece um conjunto de recursos para realizar aritmética em ponteiros. Entretanto, a aritmética de ponteiro não é idêntica à aritmética de inteiro. A aritmética de ponteiro ajusta implicitamente o endereço subjacente, multiplicando ou dividindo os operandos pelo tamanho do tipo referenciado pelo ponteiro. O fragmento de D seguinte ilustra esta propriedade:

int *x;

BEGIN
{
	trace(x);
	trace(x + 1);
	trace(x + 2);
}

Este fragmento cria um ponteiro de inteiro x e depois rastreia o seu valor, seu valor incrementado por um e seu valor incrementado por dois. Se você criar e executar este programa, o DTrace informará os valores inteiros 0, 4 e 8.

Como x é um ponteiro para um int (de 4 bytes), incrementar x adiciona 4 ao valor de ponteiro subjacente. Esta propriedade é útil quando os ponteiros são usados para fazer referência a locais de armazenamento consecutivos, tais como matrizes. Por exemplo, se x fosse atribuído ao endereço de uma matriz a conforme a mostrada na Figura 5–2, a expressão x + 1 seria equivalente à expressão &a[1]. Similarmente, a expressão *(x + 1) se referiria ao valor a[1]. A aritmética de ponteiro é implementada pelo compilador de D sempre que um valor de ponteiro é incrementado por meio dos operadores +=, + ou ++.

A aritmética de ponteiro também é aplicada quando um inteiro é subtraído de um ponteiro no lado esquerdo, quando um ponteiro é subtraído de outro ponteiro, ou quando o operador -- é aplicado a um ponteiro. Por exemplo, o programa em D seguinte rastrearia o resultado 2:

int *x, *y;
int a[5];

BEGIN
{
	x = &a[0];
	y = &a[2];
	trace(y - x);
}

Ponteiros genéricos

Às vezes, é útil representar ou manipular um endereço de ponteiro genérico em um programa em D sem especificar o tipo de dados referenciado pelo ponteiro. Os ponteiros genéricos podem ser especificados por meio do tipo void *, onde a palavra-chave void representa a ausência de informações de tipo específico, ou por meio do alias de tipo interno uintptr_t , que é o alias de um tipo de inteiro não assinado do tamanho apropriado para um ponteiro no modelo de dados atual. Você não pode aplicar a aritmética de ponteiro a um objeto do tipo void *, e esses ponteiros não podem ter a referência cancelada sem serem convertidos primeiro em outro tipo. Você pode converter um ponteiro para o tipo uintptr_t quando precisar realizar uma aritmética de inteiro no valor do ponteiro.

Os ponteiros para void podem ser usados em qualquer contexto em que é necessário um ponteiro para outro tipo de dados, tal como uma expressão de tupla de matriz associativa ou o lado direito de uma declaração de atribuição. Similarmente, um ponteiro para qualquer tipo de dados pode ser usado em um contexto em que é necessário um ponteiro para void . Para usar um ponteiro para um tipo não-void em vez de outro tipo de ponteiro não-void, é necessária uma conversão explícita. Você deve sempre usar conversões explícitas para converter ponteiros em tipos de inteiros, tal como uintptr_t, ou para reconverter esses inteiros para o tipo de ponteiro apropriado.

Matrizes multidimensionais

As matrizes escalares multidimensionais são usadas freqüentemente em D, mas são fornecidas para compatibilidade com ANSI-C e para observar e acessar estruturas de dados do sistema operacional, criadas por meio desse recurso em C. Uma matriz multidimensional é declarada como uma série consecutiva de tamanhos de matriz escalar entre colchetes [ ] seguindo o tipo base. Por exemplo, para declarar uma matriz retangular bidimensional de tamanho fixo de inteiros com dimensões de 12 linhas por 34 colunas, você escreveria a declaração:

int a[12][34];

Uma matriz escalar multidimensional é acessada por meio de uma notação semelhante. Por exemplo, para acessar o valor armazenado na linha 0 coluna 1, você escreveria a expressão de D:

a[0][1]

Os locais de armazenamento de valores de matriz escalar multidimensional são calculados pela multiplicação do número de linhas pelo número total de colunas declaradas e, em seguida, pela adição do número de colunas.

Você deve ter cuidado para não confundir a sintaxe de matriz multidimensional com a sintaxe de D de acessos de matriz associativa (ou seja, a[0][1] não é o mesmo que a[0, 1]). Se você usar uma tupla incompatível com uma matriz associativa ou tentar acessar uma matriz associativa de uma matriz escalar, o compilador de D informará uma mensagem de erro apropriada e recusará a compilação do seu programa.

Ponteiros para objetos do DTrace

O compilador de D proíbe o uso do operador & para obter ponteiros para objetos do DTrace, tais como matrizes de associação, funções internas e variáveis. Você não pode obter o endereço dessas variáveis, para que o ambiente de tempo de execução do DTrace esteja livre para realocá-las, conforme necessário, entre os acionamentos de teste, de forma a gerenciar de maneira mais eficaz a memória necessária para os seus programas. Se você cria estruturas compostas, é possível construir expressões que recuperem o endereço do kernel do armazenamento do objeto do DTrace. Você deve evitar a criação de tais expressões em seus programas em D. Se você precisar usar uma expressão como essa, certifique-se de não armazenar em cache o endereço entre os acionamentos de teste.

Em ANSI-C, os ponteiros também podem ser usados para realizar chamadas de função indireta ou atribuições, tal como a criação de uma expressão usando o operador unário de cancelamento de referência * no lado esquerdo de um operador de atribuição. Em D, esses tipos de expressões usando ponteiros não são permitidos. Você só pode atribuir valores diretamente para variáveis de D usando o nome delas ou aplicando o operador de índice de matriz [] a uma matriz escalar ou de associação de D. Você só pode chamar funções definidas pelo ambiente do DTrace por nome, conforme especificado no Capítulo 10Ações e sub-rotinas. As chamadas de função indiretas usando ponteiros não são permitidas em D.

Ponteiros e espaços de endereço

Um ponteiro é um endereço que fornece uma translação em algum espaço de endereço virtual para um pedaço de memória física. O DTrace executa os seus programas em D no espaço de endereço do próprio kernel do sistema operacional. O sistema Solaris inteiro gerencia muitos espaços de endereço: um para o kernel do sistema operacional e outro para cada processo do usuário. Já que cada espaço de endereço dá a ilusão de que pode acessar toda a memória do sistema, o mesmo valor do ponteiro do endereço virtual pode ser reutilizado entre espaços de endereço, mas é transladado para uma memória física diferente. Portanto, ao escrever programas em D que usam ponteiros, você deve estar ciente do espaço de endereço correspondente para os ponteiros que deseja usar.

Por exemplo, se você usar o provedor syscall para instrumentar a entrada para uma chamada ao sistema que usa um ponteiro para um inteiro ou uma matriz de inteiros como um argumento (por exemplo, pipe(2)), não seria válido cancelar a referência desse ponteiro ou matriz usando os operadores * ou [] porque o endereço em questão é de um espaço de endereço do processo do usuário que realizou a chamada ao sistema. Aplicar os operadores * ou [] a esse endereço em D resultaria no acesso ao espaço de endereço do kernel, que resultaria em um erro de endereço inválido ou no retorno de dados inesperados para o seu programa em D, dependendo se o endereço coincidiu com um endereço de kernel válido.

Para acessar a memória do processo do usuário de um teste do DTrace, aplique uma das funções copyin(), copyinstr() ou copyinto() descrita no Capítulo 10Ações e sub-rotinas para o ponteiro do espaço de endereço do usuário. Tome cuidado ao escrever seus programas em D para nomear e comentar as variáveis que armazenam endereços do usuário de modo a evitar confusão. Você também pode armazenar endereços do usuário como uintptr_t, para que não compile acidentalmente o código de D que cancela a referência deles. As técnicas para usar o DTrace em processos do usuário são descritas no Capítulo 33Rastreio de processo do usuário.