《深入理解计算机系统》
数组分配和访问
对于数组A,A就是指向数组开头的指针,值为$x_A$,数组元素i
存放在地址为$x_A+L·i$的地方,对应汇编代码movl (%rdx,%rcx,4),%eax
。常量4,我们称为伸缩因子,值1、2、4、8覆盖了所有基本简单数据类型的大小。
数据结构
struct的所有组成部分都存放在内存中一段连续的区域内,指向结构的指针就是结构第一个字节的地址。编译器维护关于每个结构类型的信息,指示每个字段的字节偏移,以这些偏移作为内存引用指令中的偏移,从而产生对结构元素的引用。
结构的各个字段的选取完全是在编译时处理的,机器代码不包含关于字段申明或字段名字的信息。
unio则是使用不同的字段来引用相同的内存块。总的大小等于最大字段的大小。当知道一种数据结构中的两种不同字段的使用是互斥的,将这两个字段声明为unio的一部分,就能减少分配空间的总量。
控制与数据
存在一些指针映射到机器代码的关键原则???
- 每个指针都对应一个类型。指针类型不是机器代码的一部分,而是C语言提供的抽象,避免程序员出现寻址错误
- 每个指针都有一个值。这个值是某个指定类型的对象的地址
- 指针使用
&
运算符创建,其所对应的机器代码常使用leap
计算表达式的值,leap
就是设计用来计算内存引用的地址 *
用于间接引用指针,结果是一个值,类型和指针类型一致,间接引用是用内存引用来实现的,要门存储到一个指定的地址,要么从指定的地址读取- 数组和指针紧密联系
- 将指针从一种类型强制转换为另一种类型,只改变它的类型,不改变它的值。它的效果就是改变指针运算的伸缩
- 指针也可以指向函数。函数指针的值是该函数机器代码表示中的第一条指令的地址
越界与溢出
C对于数组引用不进行任何边界检查,而且局部变量和状态信息都存放在栈中。对越界数组的写操作就会破坏栈中的状态信息。
一种常见的状态破坏就是缓冲区溢出,当调用gets、strcpy、sprintf等函数,不需要告诉目标缓冲区的大小,当写入的数据大于缓冲区大小时,就会破坏栈中的数据。这一点在C源码上看不出来的,只有在研究机器代码级别的程序时才能理解内存越界写的影响。
另一种缓冲区溢出就是让程序执行它本来不愿意执行的函数,这是网络攻击的常见方法。通过给程序输入一个字符串,这个字符串包含一些可执行代码的字节编码,即为攻击代码,另一些字节会用一个指向攻击代码的指针覆盖返回地址。那么,指向ret指令的效果就是跳转到攻击代码。
为了对抗这种攻击,linux上最新的GCC版本提供了一些新的机制
栈随机化
攻击者既要插入代码,也要插入指向攻击代码的指针,要产生这个指针,就要知道攻击代码字符串的栈地址。
在以往的操作系统中,栈的位置是相当固定的。因此攻击者确定一个常见Web服务器使用的栈空间,就能设计在多台机器上攻击的代码。
栈随机化的思想就是在栈的位置在每次运行时都有变化。实现方式就是在程序开始时,在栈上分配一段随机大小的空间,如下所示
栈破坏检测
C语言中,没有可靠地方法防止对数组的越界写,但是能够在发生越界写的时候,在造成有害结果之前,尝试检测到他
这种机制称为栈保护着,其思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值,也称为哨兵值,这个值是在每次运行时随机产生的,攻击者没有办法知道值是什么
限制可执行代码区域
这种方法是限制攻击者向系统中插入可执行代码的能力。只有保存编译器产生的代码的那部分内存需要是可执行的
变长栈帧
对于类似于alloc这类函数,局部存储是变长的
为了管理变长栈帧,x86使用寄存器%rbp
作为栈指针(frame point/base point)。代码一开始将%rbp
之前的值保存到栈中,因为他是一个被调用者保存寄存器,然后在函数的整个执行过程中,都使得%rbp
指向那个时刻栈的位置。
在函数结尾处,leave
指令将栈指针恢复到之前的值,其等价于下面命令,即将栈指针设置为保存%rbp
值的位置,然后将该值从栈中弹出%rbp
,这个效果就是释放整个栈帧的效果1
2movq %rbp,%rsp
popq %rbp