系列文章
: 深入理解计算机系统笔记
文章目录
- 系列文章
- 3.9 异质的数据结构
- 3.9.1 结构
- 3.9.2 联合
- 3.9.3 数据对齐
- 3.10 在机器级程序中将控制和数据结合起来
- 3.10.1 理解指针
- 3.10.2 应用:使用GDB调试器
- 3.10.3 内存越界引用和缓冲区溢出
- 3.10.4 对抗缓冲区溢出攻击
- 3.10.5 支持变长栈帧
- 3.11 浮点代码
- 3.11.1 浮点传送和转换操作
- 3.11.2 过程中的浮点代码
- 3.11.3 浮点运算操作
- 3.11.4 定义和使用浮点常数
- 3.11.5 在浮点代码中使用位级操作
- 3.11.6 浮点比较操作
- 3.11.7 对浮点代码的观察结论
- 3.12 小结
3.9 异质的数据结构
- struct和union
3.9.1 结构
- 结构的所有组成部分存放在连续的空间中,编译器维护每个结构的信息,指示每个字段的字节偏移,将偏移作为内存引用指令中的位移,从而引用结构元素
struct rec {
int i;
int j;
int a[2];
int *p;
};
//
r->p = &r->a[r->i + r->j];
//汇编实现
1. movl 4(%rdi), %eax # Get r->j
2. addl (%rdi), %eax # Add r->i
3. cltq # Extend to 8 bytes
4. leaq 8(%rdi, %rax, 4), %rax # Compute &r->a[r->i + r->j]
5. movq %rax, 16(%rdi) # Store in r->p
3.9.2 联合
- 不同的字段引用相同的内存块,字段是互斥的,将各种不同大小的数据类型结合到一起时字节序很重要
3.9.3 数据对齐
- 基本思想:将数据成员放置在符合特定对齐边界的地址上,这些对齐边界通常是数据类型的大小的倍数。
- 对齐规则
- 基本对齐:每个数据成员的地址必须是其大小的倍数。例如,一个int类型通常需要在4字节边界上对齐。
- 结构体对齐:整个结构体的起始地址也要遵循一定的对齐规则,这个规则通常是结构体中最大成员的对齐要求。
- 结构体对齐的原因
- 提高访问速度:现代处理器通常在读取内存时会加载整块数据(例如一个64位系统一次可能加载64位的数据)。如果数据未对齐,处理器可能需要进行额外的读取和拼接操作,导致性能下降。
- 硬件要求:某些硬件架构要求数据必须按照特定的对齐方式存储,否则会导致硬件错误或性能大幅下降。
//修改结构体对齐为1字节
#pragma pack(push, 1)
- x86-64的16字节对齐:内存分配,栈帧边界,特殊的指令
3.10 在机器级程序中将控制和数据结合起来
3.10.1 理解指针
- 指针:对不同数据结构中的元素产生引用,指针映射到机器代码的关键原则:
- 每个指针都对应一个类型,这是C语言提供的一种抽象
- 每个指针都有一个值,这个值是指定对象的地址
- &取地址运算符创建指针
- *间接引用
- 数组和指针关系精密,数组名即首元素地址
- 指针强转改变类型(运算和解释方式),不改变值
- 指针可以指向函数并调用函数
3.10.2 应用:使用GDB调试器
3.10.3 内存越界引用和缓冲区溢出
- C对数组引用不进行任何边界检查,且局部变量和状态信息都存放在栈中,对越界数组元素的写操作会破坏存储在栈中的状态信息
3.10.4 对抗缓冲区溢出攻击
- 栈随机化(Stack Randomization),通常指的是地址空间布局随机化(Address Space Layout Randomization,ASLR)中的一种技术。防止安全单一化(被一种方式攻击多个系统)。栈随机化在Linux系统中,可以通过/proc/sys/kernel/randomize_va_space文件来控制ASLR的启用与否。也需要编译器的支持。使栈的位置每次运行都在变化,但是需要提前分配多余的空间,所以空间不会太大,容易被“空操作雪橇(nop sled)”暴力破解
- **栈破坏检测(Stack Smashing Protection)**检测何时栈被破坏。
栈保护者(stack protector),在局部缓冲区和栈状态之间存储随机生成的栈金丝雀(Stack Canary)函数返回时,程序会检查这个金丝雀值是否被修改(是不是和内存段中的一样)。如果检测到金丝雀值被更改,程序会终止并返回错误信息。在有保护的程序中,局部变量比buf更接近栈顶,buf溢出就不会破坏值,在溢出方向和状态信息间插入金丝雀值检测溢出。 - 限制可执行代码区域(Executable Space Protection通过将内存区域标记为不可执行,从而防止攻击者利用漏洞向这些区域注入并执行恶意代码,NX位(No-eXecute bit)不执行位,DEP(Data Execution Prevention)操作系统级,W^X(Write XOR Execute)读或者写
3.10.5 支持变长栈帧
- ebp基址和esp
push ebp ; 保存基址指针
mov ebp, esp ; 设置新的基址指针
sub esp, n ; 分配n字节的栈空间
; 函数体
mov esp, ebp ; 恢复栈指针
pop ebp ; 恢复基址指针
ret ; 返回
3.11 浮点代码
- 处理器的浮点体系结构包括:
- 如何存储和访问浮点数值
- 对浮点数操作的指令
- 向函数传递浮点数参数和从函数返回浮点数结果的规则
- 函数调用过程中保存寄存器的规则
- AVX浮点体系
3.11.1 浮点传送和转换操作
- 内存和XMM寄存器之间及寄存器之间传送浮点数的指令,内存引用的指定方式与整数MOV指令相同
- s(单精度)d(双精度)a(对齐的,不满足16字节对齐会导致异常)
- 浮点数与整数类型和不同浮点格式的转换指令,浮点转整数时,会截断并向0舍入
3.11.2 过程中的浮点代码
- XMM寄存器向函数传递浮点数参数的规则
- 寄存器%xmm0-7最多可以传递8个浮点数参数
- 使用寄存器%xmm0返回浮点值
- 所有的XMM寄存器都是调用者保存,被调用者不用保存就可以覆盖
3.11.3 浮点运算操作
double funct(double a, float x, double b, int i) {
return a * x - b / i;
}
// x86-64 代码如下:
funct:
vunpcklps %xmm1, %xmm1, %xmm1 // 将x从单精度浮点转换为双精度浮点的第一步
vcvtps2pd %xmm1, %xmm1 // 将x从单精度浮点转换为双精度浮点的第二步
vmulsd %xmm0, %xmm1, %xmm0 // 计算 a * x
vcvtsi2sd %edi, %xmm1, %xmm1 // 将i转换为双精度浮点
vdivsd %xmm1, %xmm2, %xmm2 // 计算 b / i
vsubsd %xmm2, %xmm0, %xmm0 // 计算 a * x - b / i
ret
3.11.4 定义和使用浮点常数
- 和整数运算不同,AVX浮点操作不能以立即数作为操作数,也就是说编译器要为所有的常量值分配和初始化存储空间,并使用加载指令将这些常量值载入寄存器
3.11.5 在浮点代码中使用位级操作
- 位级异或vxorps,vorpd D<—S2^S1
- 位级于vandps,andpd D<—S2&S1
3.11.6 浮点比较操作
- ucomiss(ucomisd) S1,S2 效果S2 - S1
类似于CMP指令,S2必须在XMM寄存器,S1可以在内存 - 三个条件码:ZF,CF,PE,当两个操作数的任意一个为NaN时是无序的。
设置条件如下:
typedef enum {NEG, ZERO, POS, OTHER} range_t;
range_t find_range(float x) {
int result;
if (x < 0)
result = NEG;
else if (x == 0)
result = ZERO;
else if (x > 0)
result = POS;
else
result = OTHER;
return result;
}
// x86-64 代码如下:
find_range:
vxorps %xmm1, %xmm1, %xmm1 // Set %xmm1 = 0
vucomiss %xmm0, %xmm1 // Compare 0:x
ja .L5 // If >, goto neg
vucomiss %xmm0, %xmm0 // Compare x:0
jp .L8 // If NaN, goto posornan
movl $1, %eax // result = ZERO
je .L3 // If =, goto done
.L8:
vucomiss .LC0(%rip), %xmm0 // Compare x:0
setbe %al // Set result = NaN ? 1 : 0
movzbl %al, %eax // Zero-extend
addl $2, %eax // result += 2 (POS for > 0, OTHER for NaN)
ret
.L5:
movl $0, %eax // result = NEG
.L3:
rep; ret // Return
3.11.7 对浮点代码的观察结论
- AVX2的机器代码风格类似于整数的风格,使用寄存器保存操作以及传递数值,还有能力在封装好的数据上执行并行计算
3.12 小结
- 本章代码只能在x86-64机器上运行
- 机器级程序和它们的汇编代码表示,与C程序的差别很大。各种数据类型之间的差别很小。程序是以指令序列来表示的。每条指令都完成一个单独的操作。部分程序状态,如寄存器和运行时栈,对程序员来说是直接可见的。编译器必须使用多条指令来产生和操作各种数据结构,以及实现条件、循环和过程这样的结构。
- 编译C++与编译C就非常相似。实际上,C++的早期实现就是简单地进行了从C++到C的源到源的转换,并对结果运行C编译器,产生目标代码。尽管C++的对象用结构表示,类似于C的struct。C++的方法是用指向实现方法的代码的指针数组的形式。Java也是相同的。Java的目标代码是一种特殊的二进制编码表示,称为Java字节码,而Java的实现方式完全不同。Java的目标代码可以看成是虚拟机的机器级程序,这种编码并不是直接在硬件处理平台上运行,而是用解释器来处理字节码,模拟机器行为。另外,有一种称为及时编译的方法动态的将字节码翻译成实际机器指令进行执行。当你要执行多次(循环)时,这种方法更快。字节码的优点是相同的代码可以在不同的机器上执行。