3.8 数组分配和访问
3.8.1 基本原则
- C语言中,定义数组的方式:
- 表示数组的数据类型,表示数组的大小
- 令,则内存分配了字节的连续区域
- 令起始位置为,则第个数组元素存放在处
【例】64位操作系统中,定义了以下数组,则数组相关信息如下:
char A[12]; //char占1字节
char *B[8]; //指针占8字节
int C[6]; //int占4字节
double *D[5]; //指针占8字节
【例】数组的访问:将int数组的值赋值到%eax中。则
//C in %rdx, i in rcx
movl (%rdx, %rcx, 4), %eax
3.8.2 指针运算
- 指针的计算与数组数据类型大小相关。以3.8.1节数组的例子为例:
3.8.3 嵌套数组(多维数组)
- 二维数组的定义:
- 数组元素的内存地址为:
- 在内存本质上都存储为一维数组
【例】将的int数组的值赋值到%eax中。则
//A in %rdi, i in %rsi, j in %rdx
leaq (%rsi,%rsi,2), %rax //Compute 3*i
leaq (%rdi,%rax,4), %rax //Compute x_a+12i
movl (%rax,%rdx,4), %eax //Compute x_a+12i+4j
3.8.4 定长数组 & 3.8.5 变长数组
书中这两节的意思,其实是想说编译器在解析多维数组时,会尽可能地优化它。比如:GCC可以直接识别出多维数组的元素步长,生成的汇编指令避免直接计算,能显著提高程序性能。
【例】矩阵乘法例子,编译器有不少优化:
- 在第2行:取左移计算*64;
- 在第4行:使用
leaq计算元素地址; - 在第11行:+4运算取A数组的下一个行元素;
- 在第12行:+64运算取B数组的下一个列元素等等。
3.9 结构体和联合
3.9.1 结构体struct
- C语言中使用struct声明结构体:
struct rec {
int i; //4字节
int j; //4字节
int a[2]; //共8字节
int *p; //8字节
};
struct rec* r;
【例】结构体的访问,与数组访问类似,首地址加上对应偏移量r->j = r->i;
//r in %rdi
movl (%rdi), %eax //eax = r->i
movl %eax, 4(%rdi) //r->j = eax
【例】取&(r->a[i])的值
//r in %rdi, i in %rsi
leaq 8(%rdi, %rsi, 4), %rax //rax = r->a + 4i = rdi + 8 + rsi*4
【例】r->p = &r->a[r->i + r->j];
//r in %rdi
movl 4(%rdi), %eax //eax = r->j
addl (%rdi), %eax //eax = r->i + r->j
cltq //eax extend to rax
leaq 8(%rdi,%rax,4), %rax //rax = &r->a[r->i + r->j]
movq %rax, 16(%rdi) //r->p = rax
3.9.3 结构体对齐
- 结构体的数据有对齐限制,原则:任何K字节的基本对象的地址必须是K的倍数
【例】结构体S1,对齐后char会插入一个3字节的空隙
struct S1 {
int i;
char c;
int j;
};
【例】结构体S2,对齐后char会插入一个3字节的空隙
struct S2 {
int i;
int j;
char c;
};
【例】结构体,计算总结构体一共有56字节。这里如果按大小顺序降序排列结构元素,可以节省结构体大小
struct {
char *a;
short b;
double c;
char d;
float e;
char f;
long g;
int h;
};
【注】更多数据对齐的情况,还是要根据不同型号处理器和编译系统进行具体分析。
3.9.2 联合
- 联合union:所有元素共享同一存储区域。也就是说,联合体的大小取决于最大元素的大小
- 使用场景:已知两个不同字段是互斥的,可以声明为联合,减小分配空间的总量 (内核代码常用)
【例】联合体U3,大小是8字节。如果是结构体则占24字节。
union U3 {
char c; //1字节
int i[2]; //共8字节
double v; //8字节
};
【例】书中例子:二叉树数据结构
//使用结构体,每个节点需要32字节
struct node_s {
struct node_s *left; //左节点,内部节点
struct node_s *right; //右节点,内部节点
double data[2]; //叶子节点
};
//使用联合体,总计24字节(4字节type + 4字节padding + 16字节struct)
type enum { N_LEAF, N_INTERNAL } nodetype_t;
struct node_u {
nodetype_t type; //由于引入联合后无法区分是内部节点还是叶子节点,增加type类型,占4字节
union {
struct { //内部节点
struct node_u *left;
struct node_u *right;
} internal;
double data[2]; //叶子节点
} info;
};
【注意】联合体将不同数据类型结合在一起,有字节序问题(大端机器和小端机器表现不同)
include <stdio.h>
int main() {
union {
long l;
int i[2];
} temp;
temp.i[0] = 0x01234567;
temp.i[1] = 0x89abcdef;
printf("0x%lx\n", temp.l);
return 0;
}
【输出】在小端机器上输出:0x89abcdef01234567
3.10 数据和控制结合
3.10.3 内存越界引用和缓存区溢出
缓冲区溢出(buffer overflow):C对于数组引用不进行任何边界检查,所以局部的数组如果溢出会破坏栈,造成严重错误
#include <stdio.h>
int main() {
char buf[8];
gets(buf);
puts(buf);
return 0;
}
【测试】输入abcdefghijklmn,出现栈溢出
abcdefghijklmn
abcdefghijklmn
*** stack smashing detected ***: ./main terminated
Aborted (core dumped)
3.10.4 对抗缓冲区溢出方法
病毒可能利用缓冲区溢出的特性破坏计算机系统。有几种对抗方法:
- 栈随机化:每次执行程序,栈的位置都不固定
#include <stdio.h>
int main()
{
long local;
printf("local at %p\n", &local);
return 0;
}
【测试】
root@ubuntu:~/test# ./main
local at 0x7ffd5d767218
root@ubuntu:~/test# ./main
local at 0x7ffea89123a8
- 栈破坏检测:在栈帧中任何局部缓冲区与栈状态之间,存储一个特殊的金丝雀值(canary)。当检测到值被改变,表示越界,程序异常终止(可以使用
-fno-stack-protector阻止GCC产生这种代码)
- 限制可执行代码区域:栈标记为是否可读写、可执行,消除攻击者向系统中插入可执行代码的能力。
3.10.5 变长栈帧
- 编译器通常能够提前确定一个函数要提前为栈帧分配多少空间。但有些函数的局部存储是变长的
【例】栈帧申请8n字节的变长数组,编译器无法确认n的值。返回时要释放栈帧
long vframe(long n, long idx, long *q) {
long i;
long *p[n];
p[0] = &i;
for (i = 1; i < n; i++) {
p[i] = q;
}
return *p[idx];
}
【编译】
//long vframe(long n, long idx, long *q)
//n in %rdi, idx in %rsi, q in %rdx
vframe:
pushq %rbp
movq %rsp, %rbp //栈指针记录在%rbp处
subq $16, %rsp //分配16字节空间存放long i; 需要16字节对齐
leaq 22(,%rdi,8), %rax //rax = 8n+22
andq $-16, %rax //rax = (8n+22) & 1111 0000,需要16字节对齐
subq %rax, %rsp //分配long *p[n],总共分配了rax字节空间
......
leave //恢复rsp和rbp, 等价于movq %rbp, %rsp; popq %rbp
ret
【说明】栈帧情况:使用subq %rax, %rsp的方式申请8n的内存大小,使用leave恢复初始的%rsp
3.11 浮点代码
3.11.1 浮点传送和转换操作
- 为支持图形和图像处理,引入media寄存器集(MMX、SSE、AVX等),支持浮点的数学运算。这些指令允许多个操作以并行模式运行。本书基于AVX2版本
- GCC编译浮点程序,给定命令行参数
-mavx2时,会生成AVX2代码 - 浮点处理时,允许数据存放在16个YMM寄存器中,名字是
%ymm0~%ymm15。每个YMM寄存器是256位(32字节),低128位(16字节)对应XMM寄存器%xmm0~%xmm15
3.11.1 浮点传送和转换操作
- VMOV:把数据从一个XMM寄存器复制到另一个XMM寄存器,或者从内存复制。类似于整数的MOV
【例】
float float_mov(float v1, float *src, float *dst) {
float v2 = *src;
*dst = v1;
return v2;
}
【编译】
//float float_mov(float v1, float *src, float *dst)
//v1 in %xmm0, src in %rdi, dst in %rsi
float_mov:
vmovaps %xmm0, %xmm1 //%xmm1 = v1
vmovss (%rdi), %xmm0 //%xmm0 = *src
vmovss %xmm1, (%rsi) //*dst = v1
ret //return %xmm0
- 浮点数和整数之间转换——
VCVT- 浮点数整数:指令会执行截断,向零舍入
vcvttsd2siq (%rdx), %r8:从内存读取浮点数,转为长整型
- 整数浮点数:三元操作,第二个源通常可以忽略
vcvtsi2sdq %rax, %xmm1, %xmm1:从%rax读取一个长整数,转为double,存进%xmm1的低字节中
- 单精度浮点双精度浮点:把XMM寄存器的两个低位单精度值扩展为两个双精度值 (不必细扣)
vunpcklps %xmm0, %xmm0, %xmm0vcvtps2pd %xmm0, %xmm0
- 双精度浮点单精度浮点:把XMM寄存器的两个双精度值扩展为两个低位单精度值 (不必细扣)
vmovddup %xmm0, %xmm0vcvtpd2psx %xmm0, %xmm0
- 浮点数整数:指令会执行截断,向零舍入
【例】书中例子
【编译】
3.11.2 函数中的浮点代码
- 函数参数:
- 整数和指针使用通用寄存器传递
- 浮点数使用XMM寄存器传递:
%xmm0~%xmm7传递最多8个浮点参数
- 函数返回:使用
%xmm0返回浮点值
【注】所有的XMM寄存器都是调用者保存的,被调用者可以随意覆盖
3.11.3 浮点运算
- AVX2浮点指令:区分单精度和双精度。
- 指令含1个或2个源操作数,一个目的操作数。
- 第一个源操作数可以是XMM寄存器或内存位置
- 第二个源操作数和目的操作数都必须是XMM寄存器
【例】
double funct(double a, float x, double b, int i) {
return a*x - b/i;
}
【编译】
//double funct(double a, float x, double b, int i)
//a in %xmm0, x in %xmm1, b in %xmm2, i in %edi, return %xmm0
funct:
vunpcklps %xmm1, %xmm1, %xmm1 //The following tow instructions convert x to double
vcvtps2pd %xmm1, %xmm1
vmulsd %xmm0, %xmm1, %xmm0 //Multiply a by x
vcvtsi2sd %edi, %xmm1, %xmm1 //Convert i to double
vdivsd %xmm1, %xmm2, %xmm2 //Compute b/i
vsubsd %xmm2, %xmm0, %xmm0 //Substract from a*x
ret
3.11.4 浮点常量定义
【例】书中例子
double cel2fahr(double temp) {
return 1.8 * temp + 32.0;
}
【编译】
cel2fahr:
vmulsd .LC0(%rip), %xmm0, %xmm0
vaddsd .LC1(%rip), %xmm0, %xmm0
ret
.LC0:
.long 3435973837 //数字1.8的低4字节
.long 1073532108 //数字1.8的高4字节
.LC1:
.long 0 //数字32.0的低4字节
.long 1077936128 //数字32.0的高4字节
【说明】.LC0表示1.8的常量,含两个值,3435973837(0xcccccccd)和1073532108(0x3ffccccc)
- 按小端法字节排序,常数编码为:0x3ffccccc cccccccd
- 根据第2章分析,,阶码位k=10,偏移Bias=2^k-1=1023,小数位n=52
- 符号,表示正数
- 指数字段,减去偏移
- 小数字段,二进制是0.8,规格化后值为1.8
- 所以小数
3.11.5 浮点代码的位级操作
- 位级异或(XOR)、位级与(AND)
3.11.6 浮点比较
- 比较单精度(ucomiss)、比较双精度(ucomisd),与正数的
cmpq效果一样
- 浮点数比较后,会设置三个条件码:
ZF(零标志位)、CF(进位标志位)、PF(奇偶标志位)- 注:这里比整数运算多一个PF位,目的是区分任一操作数为的情况
【例】
3.11.7 总结
- 相似:AVX2指令的浮点数操作 类似于 整数的操作
- 区别:AVX2指令的规则会更复杂,且支持并行操作,使计算执行得更快