3.8 数组的分配和访问
汇编中的数组和C语言的数组确实存在一定的对应关系, 假设E是一个int型的数组, 并且我们想计算E[i], 如果此时E存放在%edx中, i存放在%ecx中, 那么我们可以用 (%edx, %ecx, 4) 来获取E[i]位置的值, 由于缩放因子可以是1, 2, 4, 8, 这就意味着我们几乎可以对任何类型的数组进行访问...
3.8.3 嵌套的数组
同样, 多维数组的访问也差不多, 对于数组D[R][C], 如果我们要访问D[i][j], 我们可以使用&D[i][j] = Xd + L(C * i + j)来进行访问...
下图是对读取A[i][j]到%eax中的汇编代码 :
这种方式结合移位加法和伸缩成功的避免了使用开销更大的乘法... 而对于编译器来说这种简化地址计算的方式非常的常见.
3.8.4 定长数组
这里排版有问题, 其实就说了一个问题 : 编译器对于定长数组的优化... 如下例 :
可以看到汇编代码中对于循环中的每一次val的设置, 并不是简单的按照原始C代码中++i来计算的, 而是通过直接算出对角线上相邻两个点的地址距离从而计算得到的, 因为这个值是个定值, 所以能够节省运算量, 提高效率...
3.8.5 变长数组
历史上C语言只支持编译时就能确定大小的多位数组(一维数组可能有些例外), C99标准引入了变长的多维数组, 因此我们可以把一个多维数组声明为如下的形式 T arr_name[expr1][expr2]... 他可以作为一个局部变量或者函数参数... 例如 :
gcc 访问它的汇编代码如下图所示 :
会发现图中使用了乘法, 因为编译器无法对数组长度进行类似定长数组的概括, 所以编译器对其使用了可能会导致严重性能处罚的乘法, 但是这也是不可避免的...
但是在循环引用变长数组的过程中, 编译器却常常可以利用访问模式的规律性来优化索引的计算... 如图, 它使用4n来递增Bptr, 而不是固定值N, 但是效果其实是相同的, 可以看到编译器同样使用低效的乘法来访问数组.
另外一点值得注意的是, 由于不存在足够的寄存器, n被存在了内存中, 这就是所谓的寄存器溢出... 一般来讲, 读取内存中的值要比写入内存中的值容易的多, 所以为了提高效率, 如果发生寄存器溢出时, 尽量将只读变量(例如这里的n)存入内存当中...
3.9 异质的数据结构
C语言中异质的数据结构主要有结构体和联合两种... (这里要搞清楚枚举其实完全可以有宏定义等价替换, 所以并不是异质的数据结构...)
3.9.1 结构
其实结构体的实现类似于数组, 结构体将所有对象放在连续的区域中, 指向结构体的指针就是指向结构体中第一个字节的地址. 其维护结构体类型的信息是通过记录每个字段的字节偏移来实现的... 假设有如下结构体 :
假设%edx中存放的是该结构体的地址, 那么我们可以使用 movl 20(%edx) %eax来访问最后的整形指针...
书中举了个比较复杂的例子 :
3.9.2 联合
联合和结构体声明方式相似, 但是联合中的结构公用同一块内存... 值得注意的是 :
1. 如果我们要访问结合中的不同数据类型, 所产生的汇编代码中都看不到任何类型相关的痕迹(我感觉这里说的也不是很准确, 其实能够从访问字节数量判断出数据类型)...
2. 所以用结合将不同大小的数据类型结合到一起的时候, 要特别小心字节顺序 :
3.9.3 数据对齐
很多时候我们会看到一些对于数据对象地址的限制(要求某种类型的地址必须是某个值的倍数, 比如2, 4, 8等等), 这么做的原因是简化处理器和操作系统之间的接口的硬件设计. 例如, 假设一个处理器总是从存储器中取出8个字节, 那么地址必须是8个字节的倍数... 如果我们能保证所有的double类型的地址都是8的倍数, 那么就可以保证我们每次访问double类型的数值时只需要一次内存访问就行了...
对于IA32而言, 对其与否都能正确工作, 但是Intel还是提倡对其以提高处理器系统的性能, Linux沿用的对其策略具体有如下两点要求 :
1. 对于2字节数据类型(如short)的地址必须是2的倍数.
2. 而其他较大的数据类型(如int, int*, float, double)的地址必须是4的倍数.
之前我们说过IA32中有一个惯例是栈帧的长度必须是16的整数倍, 这样做的目是使得编译器能够在帧栈中以每个块都是16字节对齐的方式来分配存储, 这样就能满足那些实现多媒体操作的SSE指令的要求.(由于这些指令对16字节数据块进行操作, 所以如果SSE单元和寄存器之间传送的指令要求存储器地址必须是16的整数倍, 否则会导致异常).我们可以使用如下语句来声明伺候的数据都以4字节的形式对齐, 所以在这之后的数据对象的起始地址都将是4的倍数 :
.align 4
这时候我们需要考虑一个实际的问题, 如果我们声明对齐之后, 例如我们声明了4字节对齐, 那些无法对齐的数据对象怎么办 ? 例如 :
对于这种无法对齐的数据结构, 编译器可能需要在字段的分配中插入间隙...
另外一个差不多的例子, 不过是在结尾填充 :