CSAPP学习笔记 -- 第三章 程序的机器级表示(下)

3.6 控制

代码结构
  • 顺序执行
  • 条件语句
  • 循环语句
  • 分支语句
 
除了整数寄存器,CPU还维护着一组单个位的条件码寄存器,它们描述了最近的算术或者逻辑操作的属性
  • CF:进位标志
    • 最近的操作时最高位产生了进位
    • 可用来检查无符号操作的溢出
  • ZF:零标志
    • 最近的操作得出的结果为零
  • SF:符号标志
    • 最近的操作得到的结果为负数
  • OF:溢出标志
    • 最近的操作导致一个补码溢出——正溢出或负溢出
 
leaq指令不改变任何条件吗,除此之外,图3-10中的所有指令都会设置条件码
 
CMP与SUB指令
  • CMP指令根据两个操作数之差来设置条件码
  • CMP不更新目的寄存器
  • 在AT&T格式中,操作数顺序相反
 
TEST和AND指令
  • TEST指令根据两个操作数之按位与来设置条件码
  • TEST不更新目的寄存器
  • 典型的用法是,两个操作数是一样的
 
 
访问条件码的三种常用方法
  • 可以根据条件码的某种组合,将一个字节设置为0/1
    • SET类指令
      • 一条SET类指令的目的操作数是低位单字节寄存器元素之一,或者一个字节的内存位置,指令会将这个字节设置成0/1
  • 可以条件跳转到程序的某个其他的部分
  • 可以有条件的传输数据
 
跳转指令
  • 导致指令执行切换到程序一个全新的位置
  • 跳转目的地用一个标号label指明
  • 产生目标代码文件时,汇编器会确定所有带标号指令的地址
  • 无条件跳转
    • 直接跳转:跳转目标是作为指令的一部分编码的
    • 间接跳转:跳转目标是从寄存器或内存位置中读出的
  • 有条件跳转
    • 根据条件吗的某种组合,或者跳转,或者继续执行代码序列中的下一条指令
    • 这些指令的名字和跳转条件与SET指令的名字和设置条件是匹配的
    • 条件跳转只能是直接跳转
 
跳转指令的编码
  • PC-相对的
    • 将目标指令的地址和紧跟在跳转指令后面那条指令的地址之间的差作为编码
    • 这些地址偏移量可以编码为1、2、4个字节
  • 给出绝对地址
    • 用四个字节直接指定目标,汇编器和链接器会选择适当的跳转目的的编码
 
用条件控制实现条件分支
  • 当条件满足时,程序沿着一条执行路径执行;当条件不满足时,就走另一条路径
  • 机制简单,但是低效
  • C语言中的if-else语句的通用形式模版如下
if ( test-expr )
    then-statement
else
    else-statement
  • C语言描述汇编控制流
    t = test-expr
    if ( !t )
        goto false;
    then-statement
    goto done;
false:
    else-statement
done:
 
用条件传送实现条件分支
  • 计算一个条件操作的两种结果,根据条件是否满足从中选取一个
  • 分支预测错误的性能损失
 
循环
  • 用条件测试和跳转组合起来实现循环效果
  • 理解产生的汇编代码与原始源代码之间的关系,关键是找到程序值和寄存器之间的映射关系
  • GCC和其他汇编器产生的循环代码主要基于两种基本的循环模式
    • do-while循环
    • while循环
      • 跳转到中间(jump to middle)
        • 执行一个无条件跳转到循环结尾处的测试,以此来执行初始的测试
      • guarded-do
 
switch语句
  • switch语句可以根据一个整数索引值进行多重分支
  • 通过跳转表这种数据结构可以使得实现更加高效
  • 执行switch语句的关键步骤是通过跳转表来访问代码位置
 
 
3.7 过程

过程
  • 软件中很重要的一种抽象
  • 提供了一种封装代码的方法,用一组指定的参数和一个可选的返回值实现了某种功能
  • 形式多样
    • 函数function
    • 方法method
    • 子例程subroutine
    • 处理函数handle
  • 实现机制
    • 传递控制
    • 传递数据
    • 分配和释放内存
 
运行时栈
  • 当前正在执行的过程的帧总是在栈顶
  • P调用Q时,返回地址作为P的栈帧的一部分,Q的栈帧包括他的寄存器的值,局部变量,以及为它调用的过程设置参数
 
转移控制
  • 指令call Q
    • 把地址A压入栈中,并将PC设置为Q的起始地址
    • 压入的地址A被称为返回地址,是紧跟在call指令后面的那条指令的地址
    • 对应的ret指令从栈中弹出A,并把PC设置为A
 
数据传送
  • 大部分过程间的数据传送是通过寄存器实现的
  • 通过寄存器最多传递6个整型(整数和指针)参数
  • 寄存器的使用有顺序
  • 寄存器使用的名字取决于要传递数据的大小
  • 如果一个函数有大于6个整型参数,超过6个的部分就要通过栈来传递
  • 示例
 
栈上的局部存储
  • 有时候,局部数据必须放在内存中
    • 寄存器不足够存放所有的本地数据
    • 对一个局部变量使用地址运算符‘&’,因此必须能够为它产生一个地址
    • 某些局部变量的是数组或结构,因此必须能够用过数组或结构引用被访问到
 
寄存器中的局部存储空间
  • 必须确保当一个过程(调用者P)调用另一个过程(被调用者Q)时,被调用者不会覆盖调用者稍后会使用的寄存器值
  • 寄存器%rbx,%rbp,%r12-%r15被划分为被调用者保存寄存器,其他寄存器(除了%rsp)都是调用者保存寄存器
    • 根本不去改变这些寄存器的值
    • 将原始数据压入栈,改变寄存器的值,然后再返回前从栈中弹出旧值并恢复
    • 压入寄存器的值会在栈帧中创建标号为“被保存的寄存器”的一部分
    • 保存“调用者保存寄存器”的值是调用者的责任
 
递归过程
  • 栈规则提供了一种机制,每次函数调用都有他自己私有的状态信息(保存的返回位置和被调用者保存寄存器的值)存储空间
  • 如果需要,他还可以提供局部变量的存储
 
 
3.8 数组分配和访问

C语言中的数组是一种将标量数据聚集成更大数据类型的方式。
C语言的一个不同寻常的特点就是可以产生指向数组中元素的指针,并对这些指针进行运算
 
基本原则
 
指针运算
  • 善用lea和mov类指令
 
嵌套的数组
  • 要访问多维数组的元素,编译器会以数组起始为基地址,(可能需要经过伸缩的)偏移量为索引,产生计算期望的元素的偏移量,然后使用某种mov指令
 
定长数组
  • 代码优化
 
变长数组A[ i , j ]
  • 动态版本必须使用乘法指令对 i 伸缩 n 倍,而不能用一系列的移位和加法
  • 在一些处理器中,惩罚会导致严重的性能问题,但这种情况无可避免
  • 如果允许使用优化,GCC能够识别出程序访问多维数组的元素的步长,然后生成的代码会避免直接计算元素内存地址导致的乘法,显著提高程序的性能。
 
 
3.9 异质的数据结构

结构
  • 用关键字 struct 来声明,将多个对象集合到一个单位中
  • 结果的所有组成部分都存放在内存中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址
  • 常用指针进行数据引用
 
联合
  • 用关键字 union 来声明,允许用几种不同的类型来引用一个对象
  • 他们用不同的字段引用相同的内存
  • 一个联合的总大小总等于它最大字段的大小
  • 绕过了C语言类型系统提供的安全措施
  • 减少空间占用
  • 联合还可以用来访问不同数据类型的位模式
    • 不同数据类型的数值之间没有关系
  • 用联合将各种不同大小的数据类型结合到一起时,字节顺序问题就变得很重要
 
数据对齐
  • 简化了形成处理器和内存系统之间接口的硬件设计
  • 对其原则
    • 任何K字节的基本对象的地址必须是K的倍数
    • 结构体的对其要求是其最大字段长
    • 结构体之间的对齐后一结构体的对去要求为基准
 
 
3.10 在机器级程序中将控制和数据结合起来

理解指针
  • 每个指针都对应一个类型
    • 特殊的void*类型代表通用指针
    • 指针类型不是机器代码的一部分,只是C语言提供的一种抽象
  • 每个指针都有一个值
    • 这个值是某个指定类型的对象的地址
    • 特殊的NULL(0)表示指针没有指向任何地方
  • 指针用‘&’符号创建
  • ‘*’操作符用于间接引用指针
    • 间接引用通过内存引用实现
  • 数组与指针紧密联系
    • 数组名可以像指针一样引用,但是不能修改
  • 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值
  • 指针可以指向函数
    • 提供了强大的存储和向代码传递引用的功能,这些引用可以被程序的某个其他部分调用
 
应用:使用GDB调试器
 
内存越界引用和缓冲区溢出
  • 缓存区溢出
    • 对越界的数组元素的写操作会破坏存储在栈中的状态信息
    • 缓冲区一个更致命的使用就是让程序执行他原本不愿意执行的函数
      • 这是最常见的通过计算机网络攻击系统安全的方法
 
对抗缓冲区溢出攻击
  • 栈随机化
    • 安全单一化:栈的位置相对固定容易预测,使得病毒攻击容易实施
    • 栈随机化的思想使得栈的位置在程序每次运行时都不一样
    • 实现:程序开始时,在栈上分配一段合适大小的空间,程序不使用它,但是程序每次执行时后续的栈位置都发生了变化
    • 地址空间布局随机化ASLR,每次运行时程序的不同部分(程序代码、库代码、栈、全局变量、堆数据)都会被加载到不同的地方
    • 空操作雪橇:用nop sled蛮力克服破解随机化
  • 栈破坏检测
    • GCC在产生的代码中加入了一种“栈保护者”机制
    • 思想是在栈帧中任何局部缓冲区和栈状态之间存储一个特殊的“金丝雀值”,是程序每次运行时随机产生的,在程序恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或该函数调用的某个函数的操作改变了,如果是的,那么程序异常中止。
  • 限制可执行代码区域
    • 消除攻击者向系统中插入可执行代码的能力
    • 方法一:限制哪些内存区域能够存放可执行代码
      • 现有的一些机制可以限定一些内存页可读不可执行,虽然这会带来严重的性能损失(引入NX位后解决性能损失问题)
 
支持变长栈帧
  • %rbp作为基指针(帧指针)
  • leave指令将帧指针恢复到他之前的值
 
3.11 浮点代码

不是重点,略过
posted @ 2020-08-11 19:41  Yoke_cc  阅读(239)  评论(0编辑  收藏  举报