CSAPP:第3章 程序的机器级表示

CSAPP:第3章 程序的机器级表示

文章目录

3.1 A Historical Perspective
  • Intel CPU发展史
3.2 Program Encodings
3.2.1 Machine-Level Code(机器级代码)
  • 该节讲述了指令的概念以及CPU对汇编可见的寄存器:
    • PC=program counter=程序计数器
    • 整数寄存器=Integer Register
    • 条件码寄存器=condition code register
    • 向量寄存器= vector register
  • 一条指令只执行非常基本的操作
3.2.2 代码示例
  • 我将示例代码在Linux上使用GCC编译了一下,截图如下:
image-20210116154918861
  • 其中 -S是指 指完成第一部分的编辑,得到汇编文件,如果吧-S去掉,就会得到目标代码文件(mstore.o),他是二进制的。
  • 利用指令objdump -d mstore.o,得到可以将机器代码反汇编,得到如下信息,可以看到基本和上面-S的一致
    • image-20210121220346393
  • 但还是有一些区别
    • 以.开头的指令都是指导汇编器和连接器工作的伪指令
    • 反编译出来的指令没有自负后缀(表示操作数大小),比如是mov而不是movq(q=quad),push而不是pushq
C程序中插入汇编代码
  • 法一:编写完整的函数放到独立的汇编文件中,让汇编器和链接器把它和C语言代码合并
  • 法二:GCC内联汇编(inline assembly)
3.3 数据格式
image-20210121224206252
3.4 访问信息
整数寄存器
image-20210121232001944
3.4.1 操作数指示符
  • 操作数可被分为三种
    • 第一种类型是立即数(immediate)
    • 第二种类型是寄存器(register )
    • 第三类操作数是内存引用(需要注意的是内存与内存不能直接交互)
3.4.2 数据传送指令
  • mov类(使用最频繁)

    • 根据操作数的大小不同分为movb、movw、movl、moq、movbsq(绝对的四字)
    • movl 指令以寄存器作为目的时,它会把该寄存器的高位 4 字节设置为 0。造成这个例外的
      原因是 x86_64 采用的惯例,即任何为寄存器生成 32 位值的指令都会把该寄存器的高位部
      分置成 0
    • 常规的 movq 指令只能以表示为 32 位补码数字的立即数作为源操作数,然后把这个值符号扩展得到 64 位的值,movabsq 指令能够以任意 64 位立即数值作为源操作数
  • 扩展指令

    • 0扩展=z,符号扩展=s
    • 对与0扩展,并没有movzlq,原因是上文提到的movl的特性:高四字节置0
    • image-20210123201737213
    • image-20210123201815628
3.4.3 数据传输示例
  • 指针= 地址-》放寄存器
  • 局部变量一般放寄存器
  • image-20210123215504573
3.4.4 押入和弹出栈数据
  • image-20210123215607444
  • 一般来说栈底是高地址,栈顶是低地址

  • pop和push都可以分解成mov+add\sub

3.5 算术和逻辑操作
  • image-20210123215857865
3.5.1 加载有效地址(leaq)
  • 加载有效地址(load effective address)指令 leaq 实际上是 movq 指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读人数据,而是将有效地址写人到目的操作数。
  • 指令 leaq 7( %rdx,%rdx,4 ),% rax 将设置寄存器%rax 的值为 5x+7,不难发现,利用该特性可以完成表示多项式的任务
3.5.2 —元和二元操作
  • 指的是操作数的个数
3.5.3 移位操作
  • 右移会有逻辑和算数之分(因为有符号位的存在)
3.5.4 讨论
  • 描述3.5图中的指令只有右移才会区分无符号数和符号数,因此是补码运算成为比较好的运算方法的原因之一。
3.5.5 特殊的算数操作(乘除)
  • image-20210123220729506
  • imulq(补码乘法)有两种模式:

    • 双操作数(3.5图)
    • 单操作数:要求一个参数必须在寄存S%rax 中,而另一个作为指令的源操作数给出。然后乘积存放在寄存器%rdx(高 64 位)和%rax(低 64 位)中。
  • 汇编器通过计算操作数的数目来判定imul属于哪种模式

  • clto:这条指令不需要操作数—它隐含读出%rax 的符号位,并将它复制到%rdx 的所有位。

3.6 控制
3.6.1 条件码
  • image-20210123221617904
  • leaq 指令不改变任何条件码,因为它是用来进行地址计算的。
  • cmp与test
    • 这里请注意CMP指令的比较顺序,是S2-S1
    • image-20210123221702207
3.6.2 访问条件码
  • 条件码的使用方法
    • 可以根据条件码的某种组合,将一个字节设置为 0 或者 1
    • 可以条件跳转到程序的某个其他的部分
    • 可以有条件地传送数据。
  • set指令
    • image-20210123221750488
3.6.3 跳转指令
  • jmp:无条件跳转

    • 直接跳转:跳转目标是作为指令一部分的编码(即直接给出地址)
    • 间接跳转:地址从寄存器、内存读出
      • 写法:‘*’后面跟一个操作数指示符,如:jmp *%rax
  • 条件跳转

    • image-20210123222357558
  • 跳转的编码方式

    • image-20210123222659919
    • 如上图所示,jmp跳到8目标地址为:loop+0x8,jg类似,

      • 这说明,当执行 PC 相对寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。
  • 关于rep(repz)的作用(AMD的说法)

    • 用rep后面跟ret的组合来避免使ret指令成为条件跳转指令的目标。
    • 而rep指令是一种空操作,插入它反而能使AMD的代码运行速度更快
3.6.5 利用条件控制来实现条件分支(if-else)
  • C语言代码的goto风格的转换
    • image-20210123223558700
    • image-20210123223626130
3.6.6 利用条件传送来实现条件分支
  • 3.6.5,低效,改进方法是:
    • 数据的条件转移——猜结果,直接运行,再判断是否正确,若错误则返回执行,若正确则不返回(依赖猜对的正确率)
    • 高效的原因是:流水线机制,3.5.5会导致流水线阻塞,二此方法是在减少阻塞的发生。
  • 条件传送指令
    • image-20210123224328218
    • 同条件跳转不同,处理器无需预测测试的结果就可以执行条件传送。处理器只是读源值(可能是从内存中),检查条件码,然后要么更新目的寄存器,要么保持不变。
3.6.7 循环(=条件测试+跳转)
  • do-while的翻译
    • image-20210123224547311
    • image-20210123224618560
  • while的翻译
    • image-20210123224702072
    • 法1:跳转到中间(jump to middle), 它执行一个无条件跳转跳到循环结尾处的测试,以此来执行初始的测试。
      • image-20210123224726155
    • 法2:guarded-do,首先用条件分支,如果初始条件不成立就跳过循环,把代码变换为 do-while 循环。
      • 下图第一部分是吧while翻译成do-while形式,再写成goto形式
      • image-20210123224850611
    • for循环的翻译
      • image-20210123225227502image-20210123225318902
      • 上面的for循环等同于下面的while
      • image-20210123225318902
      • GCC 为 for 循环产生的代码是 while 循环的两种翻译之一,这取决于优化的等级。
      • 跳转到中间策略会得到如下 goto 代码:
        • image-20210123225158911
      • 而guarded-do 策略得到
        • image-20210123225420681
    • 综上:do-while,while,for都可以用一种简单的策略来翻译,产生包含一个或多个条件分支的代码。
3.6.8 switch语句
  • switch(开关)语句可以根据一个整数索引值进行多重分支(multiway branching)。
  • 它们不仅提高了 C 代码的可读性,而且通过使用**跳转表(jump table)**这种数据结构使得实现更加高效。
  • GCC 根据开关情况的数量和开关情况值的稀疏程度来翻译开关语句。
    • 当开关情况数量比较多(例如 4 个以上),并且值的范围跨度比较小时,就会使用跳转表。
  • 例:
    • C语言以及其goto风格
      • image-20210123230008900
      • 汇编代码
        • image-20210123230040961
        • 跳转表
          • image-20210123230122255
3.7 过程
  • 关于过程,我个人的第一反应就是function,实际上,过程除了function还有method、subroutine(子例程)、handler(处理函数)
3.7.1 运行时栈
  • 过程调用需要借助运行时栈,通用形式如下:
    • image-20210123230630672
    • 有意思的是当x86-64的过程的需要的存储空间大于寄存器能够存放的大小时,就会在栈上分配空间(栈帧:stack frame),即调用需要传送的参数存不下的时候,使用栈
    • 在不调用+寄存器够存的情况下,可以不用栈,这种过程称为叶子过程(有时)
3.7.2 转移控制(call、ret)
  • call:有一个目标——被调用过程的起始地址=PC以及各寄存器压栈+起始地址上PC
  • ret:pop回PC及各寄存器
3.7.3 数据传送
  • X86-64中过程调用一般用寄存器传送数据(但最多6个),一旦超过6个,则需要用到栈来传送
    • 1-6还是寄存器上,7-n在栈帧中,其中7在栈顶
3.7.4 栈上的局部存储
  • 即局部变量如何在栈中存储(加减指针。。。)
  • 局部数据必须在内存的情况
    • 寄存器不足够存放所有的本地数据。
    • 对一个局部变量使用地址运算符因此必须能够为它产生一个地址。
    • 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。
3.7.5 寄存器中的局部存储空间
  • 寄存器是所有过程共享的资源。
  • 但是:寄存器%rbx、%rbp和%r12、%r15被划分为被调用者保存寄存器——子过程调用需先保存原来的值
3.7.6 递归过程
  • =调用自身过程(但有个递归出口)
3.8 数组的分配与访问
3.8.1 基本原则
  • 看图说话
image-20210124001656706
3.8.2 指针运算
  • 表达式 Expr和*&Expr 是等价的。
    • 由此:A为数组起始地址的引用,故数组引用 A[i]等同于表达式 * (A+ i )。
3.8.3 嵌套数组(多维数组)
  • 这里就牵扯到地址计算的问题(内存中是一维存储的)。
3.8.4 定长数组
  • 字面意思
3.8.5 变长数组
  • 这里的变长不是我们通常意义上的可变长度(如:Cpp中的vector),而是指在调用的时候才确定长度,如下:
    • image-20210124002755836
    • 它可以被优化成
    • image-20210124002834961
    • 其汇编代码为
    • image-20210124002857230
3.9 异构的数据结构
  • 特指struct和union
3.9.1 结构(structure)
  • 类似于数组的实现,如下:
    • image-20210124003327550
3.9.2 联合(union)
  • 对于同样的数据结构来说struct和union表示如下:
    • image-20210124003848964
    • 其大小如下,可以观察到,union中不同数据结构的偏移是从0开始的(结构体不是)——这就导致了union在申请空间的时候可以减少分配的空间总量(即分配部分即可)
    • image-20210124003909889
3.9.3 数据对齐
  • 即常说的边界对齐(对象的地址必须是K的倍数(与处理器的处理位数有关)),采用边界对齐可以减少数据读取的次数。
  • 否则对于下面的例子可能会多读取一次
    • image-20210124004859840
3.10 在机器级程序中将控制与数据结合起来
3.10.1 理解指针
  • 指针的特点
    • 每个指针都应指明类型,避免寻址错误
    • 每个指针都有一个值(地址),NULL(0)表示不指向任何地址
    • 指针运用 ‘&’运算符创建(引用)
    • ‘*’操作符用于简介引用指针(用于取值)
    • 数组与指针紧密联系(数组名也是个引用)
    • 指针强转只改变类型,不改变值
    • 指针也可以指向函数
      • 对于下面的fun函数原型
      • image-20210124160042391
3.10.2 应用:使用GDB调试器
  • 命令
    • linux> gdb filename
  • 调试命令
    • image-20210124161745963
3.10.3 内存越界引用与缓冲区溢出
  • C不对数组引用做边界检查——可能导致缓冲区溢出(如:蠕虫病毒)
    • image-20210124164426255
    • 而对对照GCC产生的echo汇编代码
      • image-20210124164805443
3.10.4 对抗缓冲区溢出攻击
栈随机化
  • 栈随机化的思想使得栈的位置在程序每次运行时都有变化,其原理就是使得攻击者的代码指针失效
  • 实现:
    • 程序开始时,在栈上分配一段0 -n 字节之间的随机大小的空间,例如,使用分配函数 alloca (类似malloc,但是在栈上分配空间)在栈上分配指定字节数量的空间。程序不使用这段空间,但是它会导致程序每次执行时后续的栈位置发生了变化。
    • 分配的范围必须足够大,才能获得足够多的栈地址变化,但是又要足够小,不至于浪费程序太多的空间==大小适度。
栈破坏检测
  • 能够检测帧何时已经被破坏——stack protector(帧保护者机制)
  • 其思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金•丝雀(canary )值 ,也称为哨兵值(guard value), 是在程序每次运行时随机产生的,因此,攻击者没有简单的办法能够知道它是什么。
  • 接下来只需要返回时候检测该值是否改变即可
    • 即:
    • image-20210124172542808
限制可执行代码区域
  • 限制哪些区域可以用来存放可执行代码
3.10.5 支持变长栈帧
  • 当编译器遇到不能确定分配的帧栈大小时,就需要变长帧栈的支持,如:
    • 由于n的不确定,故每次调用分配的数组指针大小都可能不同,即编译器无法确定分配帧栈的大小
    • image-20210124173115770
    • 为了管理变长栈帧,X86-64 代码使用寄存器%rbp作为帧指针(frame pointer)(有时称为基指针( base pointer) , 这也是%rbp 中 bp两个字母的由来)。
    • 首先把%rbp 的当前值压人栈中,将紅 设置为指向当前的栈位置(第 2 3 行)。然后,在栈上分配 16 个字节,其中前 8 个字节用于存储局部变量 i,而后 8个字节是未被使用的。接着,为数组 p 分配空间(第 5 ~11 行)。
      • image-20210124175057267
3.11 浮点代码
  • AVX( Advanced Vector Extension, 高级向量扩展)。每个扩展都是管理寄存器组中的数据,
  • 在 AVX 中称为 “YMM” 寄存器;YMM 是 2 5 6 位的,每个YMM寄存器可以存放 8 个 3 2 位值,或 4 个 6 4 位值,这些值可以是整数,也可以是浮点数。
  • 当对标量数据操作时,这些寄存器只保存浮点数,而且只使用低 32 位(对于 float ) 或 64 位(对于 double)。
  • 汇编代码用寄存器的 SSE XMM 寄存器名字%xmm0~%xmm15来命名他们,每个 XMM 寄存器都是对应的 YMM 寄存器的低 128 位(16 字节)
image-20210124180757701
3.11.1 浮点传送和转换操作
  • 浮点传送指令

    • image-20210124182646079
    • image-20210124182618249
  • 浮点数-整数转换

    • image-20210124190230828
3.11.2 过程中的浮点代码
  • XMM传递浮点数规则
    • XMM 寄存器%xmm0~%xmm15最多可以传递 8 个浮点参数。按照参数列出的顺序使用这些寄存器。可以通过栈传递额外的浮点参数。
    • 函数使用寄存器%xmm0来返回浮点值。
    • 所有的 XMM 寄存器都是调用者保存的。被调用者可以不用保存就覆盖这些寄存器中任意一个。
    • 下面这个例子说明只有浮点数才会被分配到xmm中,浮点指针不行
    • image-20210124192505037
3.11.3 浮点运算操作
  • image-20210124193116351
3.11.4 定义和使用浮点常数
  • 编译器必须为所有的常量值分配和初始化存储空间。然后代码在把这些值从内存读入。
  • image-20210124193932351
3.11.5 在浮点代码中使用位级操作
  • 对 XMM 寄存器中的所有 128 位进行布尔操作
  • image-20210124194117698
3.11.6 浮点比较操作
  • image-20210124194345373
  • 浮点比较指令会设置三个条件码:零标志位 ZF、进位标志位 CF 和奇偶标志位 PF
    • 当任一操作数为 NaiV 时,就会出现无序的情况。
    • image-20210124194617315
3.11.7 对浮点代码的观察结论
  • 用 AVX2 为浮点数上的操作产生的机器代码风格类似于为整数上的操作产生的代码风格。它们都使用一组寄存器来保存和操作数据值,也都使用这些寄存器来传递函数参数。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

椰子奶糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值