《深入理解计算机系统》第3周

第三章主要是从汇编代码级来了解程序是如何运行,通过阅读汇编代码一方面可以理解编译器的优化能力帮助分析优化源码,另一方面可以了解程序存储运行控制信息的细节可以用来防御代码漏洞等。

有一定的汇编基础,所以这章比上章读起来稍微轻松一些,长假期间闲时顺手翻完。

主要记录了以下几个知识点:

首先是定义

  • PC,存储将要执行的下一条指令在内存中的地址
  • 整数寄存器,存储整数数据和指针,有的jicunq被用来记录某些重要的程序状态
  • 条件码,保存最近执行的算术或逻辑指令的状态信息,用来实现控制或数据流中的条件变化
  • 向量寄存器用来存放一个或多个整数或浮点数值。
  • 一个X86-64的CPU中包含一组16个64位通用目的寄存器,用来存储整数数据和指针。其中%rsp是栈指针用来指明运行时栈的结束位置。

数据传输指令、入栈出栈数据指令、算术逻辑操作以及跳转指令之前都有一定的了解,这次补充了下乘法和除法的运算逻辑。

2个64位有符号/无符号整数相乘需要128位来表示,X86-64对128位数的操作提供有限的支持。

mulq(无符号数乘法)和imulq(补码乘法)都要求一个参数必须在寄存器%rax中,另一个作为指令的源操作数给出,乘积存放在寄存器%rdx(高64位)和%rax(低64位)中。

除法或取模运算,由单操作数除法指令来提供,idivl将寄存器%rdx(高64位)和%rax(低64位)中的128位数作为被除数,除数作为指令操作数给出,将商存储在%rax中,余数存储在%rdx中。

条件码通常不会直接读取,常用的三种方法:

  • 根据条件码的某种组合将一个字节设置为0/1;
  • 条件跳转到程序的其他部分;
  • 有条件地传送数据。 常用的条件码寄存器:
    • CF:进位标志,可用来检查无符号操作的溢出
    • ZF:零标志位
    • SF:符号标志
    • OF:溢出标志,最近的操作导致一个补码溢出

基于条件数据传送的代码会比基于条件控制转移的代码性能要好是因为:

首先,现代处理器通过pipelining重叠连续指令的步骤来获取高性能。

当机器遇到条件跳转指令时只有当分支条件求值完成后才能决定分支往哪走,分支预测逻辑可靠的情况下流水线中依旧会充满指令并行,但是当跳转错误预测时需要丢掉它为跳转指令后续所有指令已做的工作,浪费大约15-30个时钟周期,导致程序性能严重下降。

同条件跳转不同,处理器不需要预测结果就可以执行条件传送,处理器只是读源值,检查条件码,然后要么更新目的寄存器要么保持不变。

不过条件传送也不总是会提高代码效率,需要权衡复杂计算开销和分支预测错误开销来判断具体使用哪种。

过程是软件中的一种重要抽象,要提供对过程的机器级支持需要处理传递控制、传递数据、分配和释放内存等机制。

C语言过程调用机制的一个关键特性在于使用了栈数据结构的后进先出的内存管理原则,每个过程在栈中都有自己的私有空间,多个未完成调用的局部变量不会相互影响。

C语言对数据引用不进行任何边界检查,局部变量和状态信息都存放在栈中。这两种情况结合在一起,对越界的数组元素写操作会破坏存储在栈中的状态信息,即常见的缓冲区溢出问题

这种问题可以被用来攻击系统安全,将攻击代码包含在输入字符串中或者将一个指向攻击代码地址的指针覆盖返回地址等。

现代的编译器和操作系统实现了很多机制限制入侵者通过缓冲区溢出攻击获得系统控制。常见的机制:

  • 栈随机化:为了插入攻击代码,攻击者既要插入代码又要插入指向该代码的指针。这个指针也是攻击字符串的一部分,要产生这个指针需要知道字符串存放的栈地址。栈的位置在程序每次运行时都有变化使得改地址难以预测。在Linux中栈随机化已经成为标准行为,ASLR地址空间布局随机化。
  • 栈破坏检测:第二道防线是能够检测到何时栈被破坏,栈保护者机制通过canary金丝雀值是否被改变来判断,该值是程序每次运行随机产生的,如果被某个函数或操作改变那么异常中止该程序。
  • 限制可执行代码区域:消除攻击者向系统中插入可执行代码的能力。