Solaris 动态跟踪指南

第 5 章 指针和数组

指针是操作系统内核或用户进程的地址空间中数据对象的内存地址。D 允许创建和处理指针,并将它们存储在变量和关联数组中。本章介绍用于指针的 D 语法、可用于创建或访问指针的运算符,以及指针和固定大小标量数组之间的关系。另外,还讨论了有关在不同地址空间中使用指针的问题。


注 –

如果您是有经验的 C 或 C++ 语言程序员,可略过本章的大部分内容,因为 D 指针语法与相应的 ANSI-C 语法相同。您应阅读指向 DTrace 对象的指针指针和地址空间,因为这两节介绍了特定于 DTrace 的功能和问题。


指针和地址

Solaris 操作系统使用称为虚拟内存的技术,在您的系统中为每个用户进程提供独立的内存资源的虚拟视图。内存资源的虚拟视图又称为地址空间,它将一定范围的地址值(用于 32 位地址空间的 [0 ... 0xffffffff] 或者用于 64 位地址空间的 [0 ... 0xffffffffffffffff])与一组转换相关联,操作系统和硬件使用这些转换将每个虚拟地址转换为相应的物理内存位置。D 中的指针是一些数据对象,用于存储整数虚拟地址值并将其与 D 类型(说明存储在相应内存位置中数据的格式)关联。

可通过以下方式将某个 D 变量声明为指针类型:首先指定被引用数据的类型,然后将星号 (*) 附加到类型名称中以指示要声明指针类型。例如,D 编译器在内部使用以下声明:

int *p;

声明了名为 p 的 D 全局变量,它是指向整数的指针。此声明意味着 p 本身是大小为 32 位或 64 位的整数,它的值表示位于内存中某个位置的另一整数的地址。因为编译格式的 D 代码在操作系统内核本身中触发探测器时执行,所以 D 指针通常是与内核的地址空间关联的指针。可使用 isainfo(1) -b 命令来确定活动操作系统内核用于指针的位数。

如果要创建指向内核中某个数据对象的指针,可使用 & 运算符来计算该对象的地址。例如,操作系统内核源代码将声明 int kmem_flags 可调参数。可以通过在 D 中跟踪对该对象的名称应用 & 运算符的结果来跟踪此 int 的地址:

trace(&`kmem_flags);

* 运算符可用于引用通过指针寻址的对象,并且充当 & 运算符的逆向运算符。例如,以下两个 D 代码段在含义上是等效的:

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

左边代码段创建 D 全局变量指针 p。因为 kmem_flags 对象属于 int 类型,所以 &`kmem_flags 的结果类型为 int *(即,指向 int 的指针)。左边代码段将跟踪 *p 的值,该值可以跟踪指向数据对象 kmem_flags 的指针。因此,此代码段与右侧代码段相同,后者直接使用名称来跟踪数据对象的值。

指针安全

如果您是 C 或 C++ 语言程序员,在阅读上节内容后可能会有些担心,因为您知道在程序中错误地使用指针会导致程序崩溃。DTrace 是一个强大、安全的环境,在其中执行 D 程序时,错误地使用指针不会导致程序崩溃。即使您确实编写了有很多错误的 D 程序,无效的 D 指针访问也绝对不会导致 DTrace 或操作系统内核故障或崩溃。相反,DTrace 软件会检测所有无效的指针访问,禁用检测过程并报告问题以便进行调试。

如果您使用过 Java 编程语言进行编程,则您可能知道,基于同样的安全原因,Java 语言不支持指针。因为指针在 C 语言中是操作系统实现的固有部分,所以它们在 D 中是必需的,但 DTrace 实现了与 Java 编程语言中相同的安全机制,可以避免包含错误的程序破坏自身或彼此破坏。DTrace 的错误报告类似于 Java 编程语言的运行时环境,它可以检测编程错误并向您报告异常。

要了解 DTrace 的错误处理和报告,请编写使用指针并且有意出错的 D 程序。在编辑器中,键入以下 D 程序并将其保存在名为 badptr.d 的文件中:


示例 5–1 badptr.d:DTrace 错误处理的演示

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

badptr.d 程序将创建名为 x 的 D 指针,它是指向 int 的指针。程序将特殊的无效指针值 NULL 赋给此指针,该值是地址 0 的内置别名。根据约定,地址 0 始终定义为无效,所以 NULL 可用作 C 程序和 D 程序中的标记值。程序使用强制转换表达式将 NULL 转换为指向整数的指针。然后程序使用表达式 *x 取消引用指针,并且将结果赋给另一个变量 y,然后尝试跟踪 y。在执行 D 程序时,DTrace 会在执行 y = *x 语句时检测无效的指针访问,并且报告错误:


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

使用无效指针的程序可能产生的另一个问题就是对齐错误。按照体系结构约定,基础数据对象(如整数)将根据大小在内存中对齐。例如,2 字节整数在 2 的倍数的地址上对齐,4 字节整数在 4 的倍数的地址上对齐,依此类推。如果对指向 4 字节整数的指针取消引用,并且指针地址是无效值(不是 4 的倍数),则访问将失败,并且产生对齐错误。D 中的对齐错误几乎都是指示:指针因为 D 程序中的错误而包含无效的值或损坏的值。可通过将 badptr.d 的源代码更改为使用地址 (int *)2 而不是 NULL 来创建示例对齐错误。因为 int 是 4 字节,并且 2 不是 4 的倍数,所以表达式 *x 将导致 DTrace 对齐错误。

有关 DTrace 错误机制的详细信息,请参见ERROR 探测器

数组声明和存储

除了第 3 章中说明的动态关联数组之外,D 还支持标量数组。标量数组是一组固定长度的连续内存位置,每个位置存储一个相同类型的值。可通过使用从零开始的整数引用每个位置来访问标量数组。标量数组直接对应于 C 和 C++ 中数组的概念和语法。虽然在 D 中使用标量数组不如使用关联数组和其高级对应项(聚合)那么频繁,但在访问 C 中声明的现有操作系统数组数据结构时有时会需要使用标量数组。将会在第 9 章中介绍聚合。

由 5 个整数组成的 D 标量数组应这样声明:使用类型 int,并在声明后加上用方括号括起来的元素数目,如下所示:

int a[5];

下图显示了数组存储的可视表示:

图 5–1 标量数组表示

该图显示了由 5 个对象组成的数组的图片。

D 表达式 a[0] 用于引用第一个数组元素,a[1] 引用第二个数组元素,依此类推。从语法的角度来看,标量数组和关联数组非常相似。可声明由 5 个整数(通过整数键引用)组成的关联数组,如下所示:

int a[int];

并且使用表达式 a[0] 来引用此数组。但从存储和实现角度来看,两个数组有很大的不同。静态数组 a 由 5 个连续内存位置(编号从 0 开始)组成,并且索引表示存储空间中为此数组分配的偏移。另一方面,关联数组没有预定义大小,并且不会将元素存储在连续内存位置中。此外,关联数组键与相应的值存储位置无关。可访问关联数组元素 a[0]a[-5],并且 DTrace 只分配两个词的存储空间,它们可能连续,也可能不连续。关联数组键是相应值的抽象名称,与值存储位置无关。

如果使用初始赋值创建数组,并将一个整数表达式用作数组索引(如 a[0] = 2),则 D 编译器将始终创建新的关联数组,即使在此表达式中 a 还可解释为对标量数组的赋值。在此情况下,必须预先声明标量数组,以便 D 编译器能够了解数组大小的定义并推断此数组为标量数组。

指针和数组关系

就像在 ANSI-C 中一样,指针和数组在 D 中也有特殊关系。数组由与其第一个存储位置的地址关联的变量表示。指针也是具有所定义类型的存储位置的地址,所以 D 允许将 array [ ] 索引表示法与指针变量和数组变量配合使用。例如,以下两个 D 代码段的含义等价:

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

在左边代码段中,通过对表达式 a[0] 应用 & 运算符,指针 p 被赋给 a 中的第一个数组元素的地址。表达式 p[2] 将跟踪第三个数组元素(索引为 2)的值。因为 p 现在包含与 a 关联的同一地址,所以此表达式将生成与 a[2] 相同的值,如右侧代码段中所示。等价的一个后果就是 C 和 D 允许您访问任何指针或数组的任何索引。编译器或 DTrace 运行时环境不会执行数组界限检查。如果访问超出数组预定义值的内存,则会产生意外结果或者 DTrace 会报告无效地址错误,如先前示例中所示。与平常一样,不会破坏 DTrace 本身或操作系统,但您需要调试 D 程序。

指针与数组之间的区别在于:指针变量引用一个单独的存储块,该存储块包含另一存储空间的整数地址。数组变量会对数组存储空间本身命名,而不是对包含数组位置的整数位置命名。下图中显示了此区别:

图 5–2 指针和数组存储

该图显示了指向由 5 个对象组成的数组的指针。

如果尝试对指针和标量数组赋值,则 D 语法中将显示此区别。如果 xy 都是指针变量,则表达式 x = y 合法;表达式只将 y 中的指针地址复制到 x 命名的存储位置。如果 xy 是标量数组变量,则表达式 x = y 不合法。在 D 中不能将数组作为一个整体赋值。但是,可在允许使用指针的任何上下文中使用数组变量或符号名。如果 p 是指针而 a 是数组,则允许使用语句 p = a;此语句等价于语句 p = &a[0]

指针运算

因为指针只是用作内存中其他对象地址的整数,所以 D 提供了一组功能来对指针执行运算。但是,指针运算与整数运算并不完全相同。指针运算通过将指针引用的类型大小与操作数相乘或相除,来隐式调整基础地址。以下 D 代码段将说明此属性:

int *x;

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

此代码段创建整数指针 x 并跟踪其值,首先将值递增 1,然后将值递增 2。如果创建并执行此程序,DTrace 会报告整数值 0,4 和 8。

因为 x 是指向整数的指针(大小为 4 字节),递增 x 将对基础指针值增加 4。在使用指针来引用连续存储位置(如数组)时,此属性很有用。例如,如果将数组 a 的地址赋给 x(如图 5–2 中所示),则表达式 x + 1 将等价于表达式 &a[1]。类似地,表达式 *(x + 1) 将引用值 a[1]。每次使用 +=+++ 运算符递增指针的值时,D 编译器将实现指针运算。

在从左边的指针中减去某个整数时、从一个指针中减去另一个指针时或者对指针应用 -- 运算符时,也将应用指针运算。例如,以下 D 程序将跟踪结果 2:

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

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

通用指针

有时在 D 程序中,在不指定指针引用的数据类型的情况下表示或处理通用指针地址会很有用。可使用类型 void *(其中关键字 void 表示缺少特定信息)或使用内置类型别名 uintptr_t(它表示对应当前数据模型中某个指针的大小的无符号整数类型别名)来指定通用指针。不能对 void * 类型的对象应用指针运算,并且只有在先将这些指针的类型强制转换为另一类型之后才能取消对它们的引用。在需要对指针值执行整数运算时,可将指针强制转换为 uintptr_t 类型。

可在需要指向另一数据类型的指针的任何上下文(如关联数组元组表达式或赋值语句的右侧)中使用指向 void 的指针。同样,可在需要指向 void 的指针的上下文中使用指向任何数据类型的指针。要用指向非 void 类型的指针来代替另一非 void 指针类型,需要使用显式强制类型转换。必须始终使用显式强制类型转换将指针转换为整数类型(如 uintptr_t),或者将这些整数转换回相应的指针类型。

多维数组

多维标量数组在 D 中使用较少,提供它们是为了与 ANSI-C 兼容,以及观察和访问在 C 中使用此功能创建的操作系统数据结构。通过基本类型和其后用方括号([ ])括起来的一系列连续的标量数组大小来声明多维数组。例如,要声明固定大小的二维矩形数组(12 行 x 34 列),应编写以下声明:

int a[12][34];

可使用类似表示法来访问多维标量数组。例如,要访问存储在第 0 行第 1 列中的值,应编写以下 D 表达式:

a[0][1]

多维标量数组值的存储位置是通过将行号与声明的总列数相乘并加上列号计算得出的。

应该注意的是,不要将多维数组语法与关联数组访问的 D 语法相混淆(即 a[0][1]a[0, 1] 不同)。如果将不兼容的元组与关联数组配合使用,或者尝试通过关联数组来访问标量数组,则 D 编译器将报告相应的错误消息并拒绝编译程序。

指向 DTrace 对象的指针

D 编译器禁止使用 & 运算符来获取指向 DTrace 对象(如关联数组、内置函数和变量)的指针。禁止获取这些变量的地址,可以使 DTrace 运行时环境根据需要在探测器触发之间重新对变量定位,以便更有效地管理程序所需的内存。如果创建复合结构,则可以构造不检索 DTrace 对象存储的内核地址的表达式。应避免在 D 程序中创建这类表达式。如果需要使用这样的表达式,一定不要在探测器触发之间高速缓存地址。

在 ANSI-C 中,还可使用指针执行间接函数调用或赋值,如将使用一元 * 取消引用运算符的表达式放在赋值运算符的左边。在 D 中,不允许这些使用指针的表达式类型。您只能通过使用名称或对 D 标量数组或关联数组应用数组索引运算符 [] 来直接对 D 变量赋值。只能通过第 10 章中指定的名称来调用 DTrace 环境定义的函数。D 中不允许那些使用指针进行的间接函数调用。

指针和地址空间

指针是一个地址,用于将某些虚拟地址空间转换为物理内存块。DTrace 在操作系统内核本身的地址空间中执行 D 程序。整个 Solaris 系统将管理许多地址空间:操作系统内核使用一个地址空间,每个用户进程也使用一个地址空间。因为每个地址空间表现为它可以访问系统上的所有内存,所以同一虚拟地址指针值可在地址空间中重复使用,但会转换为不同的物理内存。因此,在编写使用指针的 D 程序时,必须注意对应于要使用的指针的地址空间。

例如,如果使用 syscall 提供器来检测将指向整数或整数数组的指针作为参数(如 pipe(2))的系统调用入口,则使用 *[] 运算符来对指针或数组取消引用是无效操作,因为要使用的地址是执行系统调用的用户进程地址空间中的地址。在 D 中,对此地址应用 *[] 运算符将导致内核地址空间访问,这又将导致无效地址错误或对 D 程序返回意外数据(取决于地址是否与有效内核地址相匹配)。

要通过 DTrace 探测器访问用户进程内存,必须对用户地址空间指针应用第 10 章中介绍的 copyin()copyinstr()copyinto() 函数之一。要注意的是,为了避免混淆,在编写 D 程序时应对存储用户地址的变量进行相应的命名和注释。还可将用户地址存储为 uintptr_t,这样就不会无意中编译取消引用它们 的 D 代码。第 33 章中介绍了在用户进程中使用 DTrace 的方法。