3.8.3 嵌套的数组

3.8.3 嵌套的数组

当我们创建数组的数组时,数组分配和引用的一般原则也是成立的。例如,声明

int A[5][3];

等价于下面的声明

typedef int row3_t[3];
row3_t A[5];

数据类型 row3_t 被定义为一个 3 个整数的数组。数组 A 包含 5 个这样的元素,每个元素需要 12 个字节来存储 3 个整数。整个数组的大小就是 4 × 5 × 3 = 60 字节。

数组 A 还可以被看成一个 5 行 3 列的二维数组,用 A[0][0]A[4][2] 来引用。数组元素在内存中按照“行优先”的顺序排列,意味着第 0 行的所有元素,可以写作 A[0],后面跟着第 1 行的所有元素(A[1]),以此类推,如图 3-36 所示。

这种排列顺序是嵌套声明的结果。将 A 看作一个有 5 个元素的数组,每个元素都是 3 个 int 的数组,首先是 A[0],然后是 A[1],以此类推。

元素 地址
A[0] A[0][0] xA
A[0][1] xA + 4
A[0][2] xA + 8
A[1] A[1][0] xA + 12
A[1][1] xA + 16
A[1][2] xA + 20
A[2] A[2][0] xA + 24
A[2][1] xA + 28
A[2][2] xA + 32
A[3] A[3][0] xA + 36
A[3][1] xA + 40
A[3][2] xA + 44
A[4] A[4][0] xA + 48
A[4][1] xA + 52
A[4][2] xA + 56

图 3-36 按照行优先顺序存储的数组元素

要访问多维数组的元素,编译器会以数组起始为基地址,(可能需要经过伸缩的)偏移量为索引,产生计算期望的元素的偏移量,然后使用某种 MOV 指令。通常来说,对于一个声明如下的数组:

T D[R][C];

它的数组元素 D[i][j] 的内存地址为

&D[i][j] = x_D + L(C · i + j)                                    (3.1)

这里,L 是数据类型 T 以字节为单位的大小。作为一个示例,考虑前面定义的 5 × 3 的整型数组 A。假设 xA、i 和 j 分别在寄存器 %rdi%rsi%rdx 中。然后,可以用下面的代码将数组元素 A[i][j] 复制到寄存器 %eax 中:

A in %rdi, i in %rsi, and j in %rdx

1  leaq (%rsi,%rsi,2), %rax     Compute 3i
2  leaq (%rdi,%rax,4), %rax     Compute x_A + 12i
3  movl (%rax,%rdx,4), %eax     Read from M[x_A + 12i + 4j]

正如可以看到的那样,这段代码计算元素的地址为 xA + 12i + 4j = xA + 4(3i + j),使用了 x86-64 地址运算的伸缩和加法特性。

练习题 3.38 考虑下面的源代码,其中 M 和 N 是用 #define 声明的常数:

long P[M][N];
long Q[N][M];

long sum_element(long i, long j) {
    return P[i][j] + Q[j][i];
}

在编译这个程序中,GCC 产生如下汇编代码:

long sum_element(long i, long j)
i in %rdi, j in %rsi

1  sum_element:
2      leaq 0(,%rdi,8), %rdx
3      subq %rdi, %rdx
4      addq %rsi, %rdx
5      leaq (%rsi,%rsi,4), %rax
6      addq %rax, %rdi
7      movq Q(,%rdi,8), %rax
8      addq P(,%rdx,8), %rax
9      ret

运用逆向工程技能,根据这段汇编代码,确定 M 和 N 的值。