学习计时:共7小时
读书:2小时
代码:1小时
作业:2小时
博客:2小时
第三章程序的机器级表示
3.1 历史观点
Intel处理器系统:x86(加入了很多处理小整数和浮点数向量的格式和指令
指令集:IA32:Intel32位体系结构
Linux使用平坦寻址方式,将整个储存空间看做一个大的字节数组
3.2 程序编码
命令gcc指的是GCC C编译器
-01 使用第一级优化(提高优化级别会使最终程序运行更快,但编译时间可能会变长
第二级优化被认为是较好的选择
将源代码转化成可执行代码的过程以前学过,不再详述
3.2.1 机器级代码
两种抽象:指令集体系结构,虚拟地址
汇编代码的特点:用可读性更好的文本格式来表示
可见的处理器状态:程序计数器(PC),整数寄存器文件,条件码寄存器,浮点寄存器
汇编代码不区分有符号或无符号整数,不区分各种类型的指针,不区分指针和整数
程序存储器:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,用户分配的存储器块
操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器存储器中的物理地址
3.2.2 代码示例
机器实际执行的程序是对一系列指令进行编码的字节序列,对源代码一无所知‘
反汇编器:根据目标代码产生一种类似于汇编代码的格式
链接器将代码的地址移到一段不同的地址范围中
链接器确定了存储全局变量accum的地址
3.2.3 关于格式的注解
所有以.开头的行都是指导汇编器和链接器的命令
3.3 数据格式
16位 字
32位 双字
64位 四字
大多数数据类型都是双字
指针:4字节的双字
单精度 4字节
双精度 8字节
扩展精度 10字节/12字节
字符后缀
具体见P111图3-1
数据传送指令三个变种:movb,movw,movl
3.4 访问信息
一个CPU包含一组8个存储32位值得寄存器
3.4.1 操作数指令符
大多数指令有一个或多个操作数,指示出执行一个操作中要引用的源数据值,以及放置结果的目标位置
不同操作数的可能性分为3种类型:立即数$,寄存器,存储器
寻址模式:见p113图3-3
底部的是最常用的形式,书上有解释
3.4.2 数据传送指令
把许多不同的指令分为指令类
见图P114 图3-4
MOV类中的指令将元操作数的值复制到目的操作数中
MOVS和MOVZ指令将一个较小的源数据复制到一个较大的数据位置,高位用符号位扩展或零扩展进行填充
Pushl指令:把数据压入到栈上
3.4.3 数据传送示例
过程体
间接引用指针就是将该指针放在一个寄存器中,然后在存储器引用中使用这个寄存器
局部变量通常保存在寄存器中,寄存器访问比存储器访问要快得多
3.5 算术和逻辑操作
加载有效地址、一元操作、二元操作、移位
3.5.1 加载有效地址
Leal:从存储器读数据到寄存器
目的操作数必须是一个寄存器
3.5.2 一元操作和二元操作
一元操作有一个操作数
二元操作有两个操作数
两个操作数不能同时是存储器的位置
3.5.3 移位操作
先给出移位量,再给出要移位的数值
左移:SAL.SHL效果一样
右移:SAR算术,SHR逻辑
3.5.4 讨论
编译器产生代码中,会用一个寄存器存放多个程序值,还会在寄存器之间传送程序值
3.5.5 特殊的算术操作
双操作数乘法指令,从两个32位操作数产生一个32位乘积
无符号除法使用divl指令,通常事先将寄存器%edx设置为0
3.6 控制
测试数据值,然后根据测试的结果来改变控制流或者数据流
Jump指令可以改变执行顺序
3.6.1 条件码
单个位的条件码寄存器
常用条件吗:CF,ZF,SF,OF
CF:无符号溢出
ZF:零
SF:负数
OF:有符号溢出
Leal进行地址运算所以不改变条件码
两种只会设置条件码的指令:CMP,SUB
3.6.2 访问条件码
三种方法:
- 根据条件码的某个组合,将一个字节设置为0或者1
- 条件跳转到程序的某个其他部分
- 有条件的传送数据
3.6.3 跳转指令及其编码
跳转指令会导致执行切换到程序中一个全新的位置
目的地通常用一个标号指明
当执行PC有关寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址
3.6.4 翻译条件分支
有条件和无条件的跳转
Goto语句,无条件跳转
3.6.5 循环
- do-while
- while
- for
3.6.6 条件传送代码
一种替代策略
3.6.7 switch语句
通过跳转表来访问代码位置
3.7 过程
3.7.1 栈帧结构
栈:传递过程参数、存储返回信息、保存寄存器用于以后恢复、本地存储
为单个过程分配的那部分栈成为栈帧
寄存器%ebp为帧指针
寄存器%esp为栈指针
3.7.2 转移控制
Call指令
目标:指明被调用过程起始的指令地址
效果:将返回地址入栈,并跳转到被调用过程的起始处
3.7.3 寄存器使用惯例
必须保证当一个过程调用另一个过程时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值
3.7.4 过程示例
3.7.5 递归过程
使代码继续到完成部分,回复栈和被调用者保存寄存器,然后返回的两种情况:
终止条件,递归调用
3.8 数组分配和访问
3.8.1 基本原则
声明 T A[N];
3.8.2 指针运算
单操作数的操作符&和*可以产生指针和间接引用指针
3.8.3 嵌套的数组
其实就是二维数组
声明:T D[R][C]
3.8.4 定长数组
Define N n
3.8.5 变长数组
用malloc或calloc分配存储空间,显式的编码
3.9 异质的数据结构
结构体,联合体
3.9.1 结构
Struct:将不同类型的对象聚合到一个对象中
3.9.2 联合
允许以多种类型来引用一个对象
3.9.3 数据对齐
对齐限制简化了形成处理器和存储器系统之间接口的硬件设计
3.10综合:理解指针
每个指针都对应一个类型
每个指针都有一个值
指针用&运算符创建
运算符*用于指针的间接引用
数组与指针紧密联系
将指针从一种类型强制转换成另一种类型,只改变它的类型,不改变它的值
指针也可以指向函数
3.11 应用:使用GDB调试器
GDB:可以观察正在运行的程序,同时又对程序的执行有相当的控制
见P175图3-30
3.12 存储器的越界引用和缓存区溢出
常见的状态破坏:缓存区溢出
解决:使用fgets函数,它包括一个参数,限制待读入的最大字节数
攻击代码:让程序执行它本来不愿意执行的函数
两种攻击形式:使系统启用一个外壳程序,给攻击者提供一组操作系统函数;执行一些未授权的任务,修复对栈的破坏,第二次执行ret指令,正常返回给调用者
对抗缓存区溢出攻击
- 栈随机化:防止安全单一化
- 栈破坏检测:加入一种栈保护者
- 现在可执行代码区域
3.13 x86-64:将IA32扩展到64位
数据类型:指针需要8个字节,long将整数变成64位
访问信息:寄存器16个,所以寄存器64位长,可以直接访问低32位
算术指令:在四字上进行运算的指令,后缀加q
控制:新增指令cmpq和testq,用于比较和测试四字
保存惯例:有些用来保存临时值的寄存器被指定为调用者保存,另外一些是被调用者保存
数据结构:数组作为同样大小的块的序列来分配,结构作为最长的块来分配,联合作为一个单独的块来分配。对任何K字节的标量数据类型来说,它的起始地址必须是K的倍数
3.14 浮点程序的机器级表示
浮点体系结构:存储模型,指令和传递规则的组合
两种:x87,SSE
遇到的问题:
- 和地址有关的地方有点弄不清,比如练习题3.1的倒数几个空就不大会
- 变长数组没看懂
- 很多题都是看着答案逆推会的,也就是说光看题很难做出来
心得:
这一次的学习任务乍一看很多,但里面的很多内容以前都或多或少接触过,所以这些部分的笔记写得也比较粗略,只列出了提纲,而没有接触过的部分的笔记就比较精细。但虽然学过,在这本书里,视角又不同了,又可以用新的视角去看这些知识,所以也并不容易,看了一遍书,掌握并不熟练,还是会有怎么看都看不明白的地方,有看不进去的地方,有虽然看起来很明白但是过目就忘的地方。