程序的机器级表示--数组与异质结构
数组
数组是一种将标量数据聚集成更大数据类型的方式。实现方式是产生一个指向数组元素的指针,并对这些指针进行运算。
基本原则
数组:
`T A[n]`
- 首先:他在内存中分配一个L·N字节的连续区域。L是T类型的大小。
- 其次:引入标识符A,作为指向数组开头的指针,这个指针的值就是数组起始位置X。
- 可以用
0 ~ N-1
的整数索引来访问该数组元素。数组元素i会被存放在地址为X + L·i
的地方 - 指令
movl (%rdx, %rcx, 4), %eax
会执行地址计算X + 4i
,读A[4]
这个位置内存的值,并把结果放在寄存器%eax中。 - 伸缩因子1、2、4、8覆盖了所有基本简单的数据类型大小。例如char型数组
char B[n]
就是12单字节元素组成,伸缩因子为1。
指针运算
- C允许指针进行计算,并根据数据类型进行伸缩。例如,数组
int A[4];
,A + i
就会转为A + 4i
。 -
&
用来产生指针。例如:&A[i]
等于A + i
-
*
用来间接引用指针。例如:A[i]
等于* (A + i)
。 - 计算同一个数据结构类型的指针之差,结果的数据类型为long,值等于两个地址之差除以该数据类型的大小。
假设int型数组E的起始位置与索引放在%rdx
和%rcx
中,结果存放在%eax
中,则:
表达式 | 类型 | 值 | 汇编 |
---|---|---|---|
E | int* | X | movq %rdx, %rax |
E[0] | int | M[X] | movl (%rdx), %eax |
E[i] | int | M[X + 4i] | movl (%rdx, %rcx, 4), %eax |
&E[2] | int* | X + 8 | leaq 8(%rdx), %rax |
E + i - 1 | int* | X + 4i -4 | leaq -4(%rdx, %rcx, 4), %rax |
*(E + i -3) | int | M[X + 4i - 12] | movl -12(%rdx, %rcx, 4), %eax |
&E[i] - E | long | i | movq %rcx, %rax |
伸缩因子根据数据类型进行变换,指针的大小都是8字节。
数组嵌套
数组分配和引用的规则对多维数组也生效。
多维数组int A[5][3]
等价于:
typedef int row3_t[3];
row3_t A[5];
元素D[i][j]
的地址为:
&D[i][j] = X +L(C·i + j)
其中,X为数组的起始地址,C为数组列数。以上述例子A[5][3]
举例:
行 | 元素 | 地址 |
---|---|---|
A[0] | A[0][0] |
X |
A[0][1] |
X + 4 | |
A[0][2] |
X + 8 | |
A[1] | A[1][0] |
X + 12 |
A[1][1] |
X + 16 | |
A[1][2] |
X + 20 | |
A[2] | A[2][0] |
X + 24 |
A[2][1] |
X + 28 | |
A[2][2] |
X + 32 | |
A[3] | A[3][0] |
X + 36 |
A[3][1] |
X + 40 | |
A[3][2] |
X + 44 | |
A[4] | A[4][0] |
X + 48 |
A[4][1] |
X + 52 | |
A[4][2] |
X + 56 |
异质结构
结构
定义:将不同类型的对象聚合为一个对象,用名字来区分结构的各个部分。
存储:结构的所有组成部分都存放在内存中一段连续的区域,而指向结构的指针就是结构第一个字节的地址。编译器以便宜作为内存中引用指令中的位移,从而产生对结构元素的引用。
声明与引用:
//定义
struct newStruct {
char a;
int b;
...
};
//声明
struct newStruct A;
//引用
A.a = 1;
A.b = 'a';
//即声明又引用
struct newStruct A = {1, 'a'};
// 结构指针
// 指向结构的指针从一个地方传递到另一个地方,而不是复制他们。
// 结构指针的声明
struct newStruct B = &A;
//结构指针的引用
B->a = 1;//或 *(B).a,必须要加括号,因为.的优先级高于*,会被翻译成*(B.a)
举个例子:
struct example{
int i;
int j;
int a[2];
int *p;
};
内存中的相对位置如下
0 4 8 16 24
偏移内容| i | j | a[0] | a[1] | p |
将结构体变量r,r->i内容复制到r->j上汇编如下:
Registers: r in %rdi
movl (%rdi), %eax Get r->i
movl %eax, 4(%rdi) Store in r->j
//产生指向结构内部对象的指针,只需将结构地址加上偏移量
leaq 8(%rdi, %rsi, 4), %rax Set%rax to &r->a[i]
结构的各个字段的选取完全是在编译时处理的。
联合
定义:以多种类型来引用同一个对象。以不同的字段引用同一块内存。
struct S3{
char c;
int i[2];
double v;
};
union U3{
char c;
int i[2];
double v;
};
字段偏移如下:
类型 | c | i | v | 大小 |
---|---|---|---|---|
S3 | 0 | 4 | 16 | 24 |
U3 | 0 | 0 | 0 | 8 |
可以看到不同与结构体,联合的总的大小等于它最大字段的大小。
联合绕过了C语言的安全措施,在实现知道数据结构互斥的场景非常有用,可以节省分配的空间总量,但是可能引起一些讨厌的错误。
联合可以用来访问不同数据类型的位模式。例如:
//将double型数据转换位unsigned long型数据
unsigned long u = (unsigned long) d;
//使用以上强制转换会丢失精度之外还会使位表示发生变化
unsigned long double2bits(double d){
union{
double d;
unsigned long u;
}temp;
temp.d = d;
return temp.u;
}
//使用以上转换就会使u与d的位表示一摸一样
但在字节顺序问题变得重要的场景,联合就得小心使用。
double uu2double(unsigned word0, unsigned word1){
union{
double d;
unsigned u[2];
}temp;
temp.u[0] = word0;
temp.u[1] = word1;
return temp.d;
}
在小端法机器上,参数word0是d的地位4个字节,而word1是高位4个字节。在大端法机器上则恰好相反。
对齐
定义:要求类型对象的地址必须是K(通常是2、4、8)的倍数。
优点:简化了形成处理器和内存系统之间的接口设计,但无论对不对齐,都不影响硬件的正确工作。
汇编指令:.align 8
举个例子:
struct S1{
int i;
char c;
int j;
};
假设是4字节对齐,画出的图来是这样的
//假设是9字节的话,如下:
0 4 5 9
偏移内容 | i | c | j |
//但是9字节无法满足4的倍数,所以需要插入间隙
0 4 5 8 12
偏移内容 | i | c | | j |
强制对齐:
某些型号的Intel和AMD处理器对实现多媒体操作的SSE指令需要强制对齐16字节,否则无法正确的执行。不满足对齐要求的地址访问都会导致异常而强制终止程序。
- 任何分配函数(alloca、malloc、calloc或realloc)生成的块的起始地址都必须是16的倍数
- 大多数的函数的栈帧边界都必须是16字节的倍数。
- AVx指令是SSE指令的超集,但是没有强制性的对齐要求。