Guía de seguimiento dinámico de Solaris

Capítulo 5 Punteros y matrices

Los punteros son direcciones de memoria de objetos de datos en el núcleo del sistema operativo o en el espacio de direcciones de un proceso de usuario. D proporciona la posibilidad de crear y manipular punteros y almacenarlos en variables y matrices asociativas. Este capítulo describe la sintaxis en D para punteros, operadores que se pueden aplicar para crear o acceder a punteros, y la relación entre punteros y matrices escalares de tamaño fijo. También se tratarán temas relacionados con el uso de punteros en distintos espacios de direcciones.


Nota –

Si es un programador en C o C++ experimentado, podrá leer por encima la mayoría de estos capítulos, ya que la sintaxis de puntero en D es la misma que la sintaxis ANSI-C correspondiente. Consulte Punteros a objetos de DTrace y Punteros y espacios de direcciones, ya que describen características y problemas específicos de DTrace.


Punteros y direcciones

El sistema operativo Solaris utiliza una técnica denominada memoria virtual para proporcionar a cada proceso de usuario su propia visualización virtual de los recursos de memoria de su sistema. A la visualización virtual de los recursos de memoria se le denomina espacio de direcciones, que asocia un rango de valores de direcciones (bien [0 ... 0xffffffff] para un espacio de direcciones de 32 bits o [0 ... 0xffffffffffffffff] para un espacio de direcciones de 64 bits) a un conjunto de transacciones que el sistema operativo y el hardware utilizan para convertir cada dirección virtual en una ubicación de memoria física correspondiente. Los punteros en D son objetos de datos que almacenan un valor de dirección virtual entero y lo asocia a un tipo en D que describe el formato de los datos almacenados en la ubicación de memoria correspondiente.

Es posible declarar una variable en D para que sea un tipo de puntero especificando primero el tipo de los datos a los que se hace referencia y, a continuación, añadiendo un asterisco ( *) al nombre del tipo para indicar que desea declarar un tipo de puntero. Por ejemplo, la declaración:

int *p;

declara una variable global en D denominada p que es un puntero a un entero. Esta declaración significa que p es un entero con tamaño de 32 o 64 bits, cuyo valor es la dirección de otro entero ubicado en algún lugar de la memoria. Dado que la forma compilada del código en D se ejecuta en el tiempo de lanzamiento del sondeo dentro del propio núcleo del sistema operativo, los punteros en D normalmente son punteros asociados al espacio de direcciones del núcleo. Es posible utilizar el comando isainfo(1) -b para determinar el número de bits que el núcleo del sistema operativo activo utiliza para los punteros.

Si desea crear un puntero a un objeto de datos dentro del núcleo, debe calcular su dirección usando el operador &. Por ejemplo, el código fuente del núcleo del sistema operativo declara un int kmem_flags ajustable. Es posible rastrear la dirección de este int rastreando el resultado de aplicar el operador & al nombre de ese objeto en D:

trace(&`kmem_flags);

El operador * se puede utilizar para hacer referencia al objeto dirigido por el puntero, y actúa como lo contrario del operador &. Por ejemplo, los dos fragmentos de código en D siguientes tienen un significado equivalente:

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

El fragmento de la izquierda crea un puntero p de variable global en D. Dado que el objeto kmem_flags es del tipo int, el tipo del resultado de &`kmem_flags es int * (es decir, puntero a int). El fragmento de la izquierda rastrea el valor de *p, que sigue al puntero hasta el objeto de datos kmem_flags. Por tanto, este fragmento es igual que el fragmento de la derecha, que simplemente rastrea el valor del objeto de datos directamente utilizando su nombre.

Seguridad de punteros

Si es programador en C o C++, se puede asustar después de leer la sección anterior, ya que sabe que el uso inadecuado de los punteros en los programas puede dar lugar a que el programa falle. DTrace es un entorno sólido y seguro para ejecutar sus programas en D, donde estos fallos no pueden dar lugar a que el programa falle. De hecho, puede escribir un programa en D con errores, pero los accesos del puntero en D no válidos no darán lugar a que DTrace o el núcleo del sistema operativo falle o se bloquee de ninguna forma. En cambio, el software DTrace detectará cualquier acceso de puntero en D no válido, desactivará la instrumentación y le informará del problema para su depuración.

Si ha programado en el lenguaje de programación Java, probablemente sepa que el lenguaje Java no admite punteros precisamente por los mismos motivos de seguridad. Los punteros son necesarios en D, ya que son una parte intrínseca de la implementación del sistema operativo en C, pero DTrace implementa el mismo tipo de mecanismos de seguridad que el lenguaje de programación Java, los cuales evitan que los programas con errores se dañen a sí mismos o a otros. La información de errores de DTrace es similar al entorno de tiempo de ejecución del lenguaje de programación Java, que detecta un error de programación y le informa de una excepción.

Para ver la creación de informes y manejo de errores de DTrace, escriba un programa incorrecto a propósito en D que use punteros. En un editor, escriba el siguiente programa en D y guárdelo en un archivo denominado badptr.d:


Ejemplo 5–1 badptr.d: Demostración del manejo de errores de DTrace

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

El programa badptr.d crea un puntero en D denominado x, que es un puntero a int. El programa asigna a este puntero el valor de puntero especial no válido NULL, que es un alias incorporado para la dirección 0. Por convención, la dirección 0 siempre se define como no válida, de forma que NULL se puede utilizar como valor centinela en C y D. El programa utiliza una expresión de conversión para convertir NULL en un puntero a un entero. A continuación, el programa deja de hacer referencia al puntero usando la expresión *x y asigna el resultado a otra variable y; a continuación, intenta rastrear y. Cuando el programa en D se ejecuta, DTrace detecta un acceso del puntero no válido al ejecutar la instrucción y = *x e informa del error:


# 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
#

Otro problema que los programas que utilizan punteros no válidos pueden dar es un error de alineación. Por convención de arquitectura, los objetos de datos fundamentales, como enteros, se alinean en memoria según su tamaño. Por ejemplo, los enteros de 2 bytes se alinean en las direcciones que son múltiplos de 2, enteros de 4 bytes en múltiplos de 4, y así sucesivamente. Si deja de hacer referencia a un puntero a un entero de 4 bytes y la dirección del puntero es un valor no válido que no es múltiplo de 4, el acceso fallará con un error de alineación. Los errores de alineación en D casi siempre indican que el puntero tiene un valor no válido o corrupto debido a un defecto en el programa en D. Se puede crear un error de alineación de ejemplo cambiando el código fuente de badptr.d para utilizar la dirección (int *)2 en vez de NULL. Ya que int tiene 4 bytes y 2 no es múltiplo de 4, la expresión *x dará lugar a un error de alineación de DTrace.

Para obtener más detalles sobre el mecanismo de error de DTrace, consulte Sondeo ERROR.

Declaraciones de matriz y almacenamiento

D proporciona compatibilidad paramatrices escalares además de las matrices asociativas dinámicas que se describen en el Capítulo 3. Las matrices escalares son un grupo de longitud fija de ubicaciones de memoria consecutivas que almacenan un valor cada una del mismo tipo. A las matrices escalares se accede haciendo referencia a cada ubicación con un entero comenzando a partir de 0. Las matrices escalares se corresponden directamente en concepto y sintaxis con las matrices en C y C++. Las matrices escalares no se utilizan con tanta frecuencia en D que las matrices asociativas y sus adiciones homólogas más avanzadas, pero a veces son necesarias al acceder a estructuras de datos de matriz de un sistema operativo existente declaradas en C. Las adiciones se describen en el Capítulo 9Adiciones.

Una matriz en D escalar de 5 enteros se declararía usando el tipo int y colocando un sufijo a la declaración con el número de elementos entre corchetes de esta forma:

int a[5];

El siguiente diagrama muestra una representación visual del almacenamiento de matriz:

Figura 5–1 Representación de matriz escalar

El diagrama muestra una imagen de una matriz de cinco objetos.

La expresión en D a[0] se utiliza para hacer referencia al primer elemento de la matriz, a[1] hace referencia al segundo, etc. Desde el punto de vista sintáctico, las matrices escalares y matrices asociativas son muy parecidas. Se puede declarar una matriz asociativa de los cinco enteros a los que se hace referencia mediante una clave de entero de esta forma:

int a[int];

y también se hace referencia a esta matriz mediante la expresión a[0]. Pero desde una perspectiva de almacenamiento e implementación, las dos matrices son muy diferentes. La matriz estática a consiste en cinco ubicaciones de memoria consecutivas numeradas desde cero, y el índice hace referencia a un desplazamiento en el almacenamiento asignado para la memoria. Una matriz asociativa, por otro lado, no tiene un tamaño predefinido y no almacena elementos en ubicaciones de memoria consecutivas. Además, las claves de matrices asociativas no tienen relación con la ubicación de almacenamiento del valor correspondiente. Se puede acceder a elementos de matrices asociativas a[0] y a[-5] y sólo se asignarán dos palabras de almacenamiento por Dtrace, que pueden ser o no consecutivas. Las claves de matrices asociativas son nombres abstractos para el valor correspondiente que no tienen relación con las ubicaciones de almacenamiento del valor.

Si crea una matriz utilizando una asignación inicial y utiliza una única expresión de enteros como el índice de matriz (por ejemplo, a[0] = 2), el compilador en D creará siempre una nueva matriz asociativa, incluso aunque en esta expresión a también se pueda interpretar como una asignación a una matriz escalar. Las matrices escalares se deben declarar con anterioridad en esta situación, de forma que el compilador de D pueda ver la definición del tamaño de la matriz y detectar que la matriz es una matriz escalar.

Relación de matrices y punteros

Los punteros y matrices tienen una relación especial en D, al igual que la tienen en ANSI-C. Una matriz está representada por una variable que se asocia a la dirección de su primera ubicación de almacenamiento. Un puntero también es la dirección de una ubicación de almacenamiento con un tipo definido, de forma que D permite el uso de la notación de índice [ ] de matriz con las variables de puntero y las variables de matriz. Por ejemplo, los dos siguientes fragmentos en D son equivalentes en significado:

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

En el fragmento de la izquierda, el puntero p se asigna a la dirección del primer elemento de matriz en a aplicando el operador & a la expresión a[0]. La expresión p[2] rastrea el valor del tercer elemento de matriz (índice 2). Dado que p ahora contiene la misma dirección asociada a a, esta expresión da lugar al mismo valor que a[2], mostrado en el fragmento de la derecha. Una consecuencia de esta equivalencia es que C y D le permiten acceder a cualquier índice de cualquier puntero o matriz. La comprobación de enlaces de matriz no se realiza por el compilador ni por el entorno de tiempo de ejecución de DTrace. Si accede a la memoria sobrepasando el final de un valor predefinido de la matriz, obtendrá un resultado inesperado o DTrace informará de un error de dirección no válida, como se muestra en el ejemplo anterior. Como siempre, no se puede dañar a DTrace o a su sistema operativo, pero necesitará depurar su programa en D.

La diferencia entre punteros y matrices es que una variable de puntero hace referencia a una parte independiente de almacenamiento que contiene la dirección del entero de algún otro almacenamiento. Una variable de matriz nombra al propio almacenamiento de matriz, no a la ubicación de un entero que, a su vez, contiene la ubicación de la matriz. La diferencia se muestra en el siguiente diagrama:

Figura 5–2 Almacenamiento de punteros y matrices

El diagrama muestra un puntero a una matriz de cinco objetos.

La diferencia se manifiesta en la sintaxis en D si intenta asignar punteros y matrices escalares. Si x e y son variables de puntero, la expresión x = y es legal; simplemente copia la dirección del puntero en y en la ubicación de almacenamiento nombrada por x. Si x e y son variables de matrices escalares, la expresión x = y no es legal. Es posible que las matrices no se asignen como un todo en D. Sin embargo, una variable de matriz o nombre de símbolo se podrá utilizar en cualquier contexto donde se permita un puntero. Si p es un puntero y a es una matriz, la instrucción p = a está permitida; esta instrucción equivale a la instrucción p = &a[0].

Aritmética de punteros

Ya que los punteros son enteros utilizados como direcciones de otros objetos en memoria, D proporciona un conjunto de características para realizar operaciones aritméticas en punteros. Sin embargo, la aritmética de punteros no es idéntica a la aritmética de enteros. La aritmética de punteros ajusta implícitamente la dirección subyacente multiplicando o dividiendo los operadores por el tamaño del tipo al que el puntero hace referencia. El siguiente fragmento en D ilustra esta propiedad:

int *x;

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

Este fragmento crea un puntero de enteros x y, a continuación, rastrea su valor, su valor incrementado en uno y su valor incrementado en dos. Si crea y ejecuta este programa, DTrace informará de los valores de enteros 0, 4 y 8.

Dado que x es un puntero a un entero (4 bytes de tamaño), al aumentar x agrega 4 al valor de puntero subyacente. Esta propiedad resulta útil al emplear punteros para hacer referencia a ubicaciones de almacenamiento consecutivas, como pueden ser matrices. Por ejemplo, si x se asignó a la dirección de una matriz a como la que se muestra en la Figura 5–2, la expresión x + 1 sería equivalente a la expresión &a[1]. De forma similar, la expresión *(x + 1) haría referencia al valor a[1]. La aritmética de punteros es implementada por el compilador en D siempre que un valor de puntero se incremente utilizando los operadores +=, + o ++.

La aritmética de punteros también se aplica cuando un entero se resta de un puntero de la parte izquierda, cuando un puntero se resta de otro puntero o cuando el operador -- se aplica a un puntero. Por ejemplo, el siguiente programa en D rastrearía el resultado 2:

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

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

Punteros genéricos

A veces resulta útil representar o manipular una dirección de puntero genérico en un programa en D sin especificar el tipo de datos a los que el puntero hace referencia. Los punteros genéricos se pueden especificar utilizando el tipo void *, donde la palabra clave void representa la ausencia de información de tipo específica o utilizando el alias de tipo incorporado uintptr_t, que utiliza un alias de tipo de entero sin signo con un tamaño apropiado para un puntero del modelo de datos actual. Puede no aplicar aritmética de punteros a un objeto de tipo void *, y a estos punteros no se les puede dejar de hacer referencia sin convertirlos primero en otro tipo. Puede convertir un puntero al tipo uintptr_t cuando necesite realizar operaciones aritméticas de enteros en un valor de puntero.

Se pueden utilizar punteros a void en cualquier contexto donde sea necesario un puntero a otro tipo de datos, como una expresión tupla de matrices asociativas o la parte derecha de una instrucción de asignación. De forma similar, se puede utilizar un puntero a cualquier tipo de datos en un contexto donde sea necesario un puntero a void. Para utilizar un puntero a un tipo no void en lugar de otro tipo de puntero no void, se requiere una conversión explícita. Siempre debe utilizar conversiones explícitas para convertir punteros a tipos enteros, como uintptr_t, o para volver a convertir estos enteros al tipo de puntero adecuado.

Matrices multidimensionales

Las matrices escalares multidimensionales no se suelen utilizar en D, pero se proporcionan a efectos de compatibilidad con ANSI-C y para observar y acceder a las estructuras de datos del sistema operativo creadas utilizando esta capacidad en C. Una matriz multidimensional se declara como una serie consecutiva de tamaños de matrices escalares entre corchetes [ ] siguiendo el tipo base. Por ejemplo, para declarar una matriz rectangular bidimensional de tamaño fijo de enteros de dimensiones de 12 filas por 34 columnas, debería escribir la declaración:

int a[12][34];

A una matriz escalar multidimensional se accede utilizando una notación similar. Por ejemplo, para acceder al valor almacenado en la fila 0 de la columna 1 debería escribir la expresión en D:

a[0][1]

Las ubicaciones de valores de matrices escalares multidimensionales se calculan multiplicando el número de fila por el número total de columnas declaradas y, a continuación, agregando el número de columna.

Deberá tener cuidado para no confundir la sintaxis de matrices multidimensionales con la sintaxis en D de accesos a matrices asociativas (es decir, a[0][1] no es igual que a[0, 1]). Si utiliza una tupla incompatible con una matriz asociativa o intenta el acceso a una matriz asociativa de una matriz escalar, el compilador en D emitirá un mensaje de error adecuado y rechazará compilar el programa.

Punteros a objetos de DTrace

El compilador en D le prohíbe utilizar el operador & para obtener punteros a los objetos de DTrace como matrices asociativas, funciones incorporadas y variables. Se le prohibe obtener la dirección de estas variables, de forma que el entorno de tiempo de ejecución de DTrace pueda volver a ubicarlas conforme sea necesario entre los lanzamientos de sondeos, para administrar con más eficacia la memoria requerida por los programas. Si crea estructuras compuestas, es posible construir expresiones que recuperen la dirección del núcleo del almacenamiento de objetos de DTrace. Debería evitar crear estas expresiones en sus programas en D. Si necesita utilizar dicha expresión, asegúrese de no guardar en la antememoria la dirección a través de los lanzamientos de sondeos.

En ANSI-C, los punteros también se pueden utilizar para realizar llamadas a funciones indirectas o para realizar asignaciones, como colocar una expresión mediante un operador de anulación de referencia unario * en la parte izquierda de un operador de asignación. En D no se permiten estos tipos de expresión con punteros. Sólo puede asignar valores directamente a variables en D mediante su nombre o aplicando el operador de índice de matrices [] a una matriz escalar o asociativa. Sólo puede llamar a funciones definidas por el entorno de DTrace, tal como se especifica en el Capítulo 10Acciones y subrutinas. Las llamadas a funciones indirectas mediante punteros no están permitidas en D.

Punteros y espacios de direcciones

Un puntero es una dirección que proporciona una traducción dentro de espacios de direcciones virtuales a una parte de memoria física. DTrace ejecuta sus programas en D en espacio de direcciones del propio núcleo del sistema operativo. Todo el sistema Solaris administra un gran número de espacios de direcciones: uno para el núcleo del sistema operativo y uno para cada proceso de usuario. Dado que cada espacio de direcciones proporciona la ilusión de que puede acceder a toda la memoria del sistema, se puede reutilizar el mismo valor de puntero de dirección virtual a través de espacios de direcciones, pero traducirlo a una memoria física diferente. Por tanto, al escribir programas en D que utilizan punteros, debe ser consciente del espacio de direcciones correspondiente a los punteros que pretende utilizar.

Por ejemplo, si utiliza el proveedor syscall para instrumentar la entrada a una llamada de sistema que lleva un puntero a un entero o matriz de enteros, como un argumento (por ejemplo, pipe(2)), no será válido dejar de hacer referencia a ese puntero o matriz mediante los operadores * o [], porque la dirección en cuestión es una dirección del espacio de direcciones del proceso de usuario que realizó la llamada de sistema. La aplicación de los operadores * o [] a esta dirección en D daría lugar a un acceso de espacio de direcciones del núcleo, que provocaría un error de dirección no válida o a la devolución de datos inesperados a su programa en D dependiendo de si la dirección coincide con una dirección del núcleo válida.

Para acceder a la memoria del proceso de usuario desde un sondeo de DTrace, debe aplicar una de las funciones copyin(), copyinstr() o copyinto() que se describen en el Capítulo 10Acciones y subrutinas al puntero del espacio de direcciones de usuario. Tenga cuidado al escribir sus programas en D a la hora de nombrar y comentar las variables que almacenan direcciones de usuario de forma adecuada para evitar confusiones. También puede almacenar direcciones de usuario como uintptr_t, de forma que no compile accidentalmente código en D que les deje de hacer referencia. Las técnicas para utilizar DTrace en procesos de usuario se describen en el Capítulo 33Seguimiento de procesos de usuario.