文章目录
- 深入理解计算机操作系统-第一章
-
- 1.1 信息就是位 上下文
- 1.2 其他程序将程序翻译成不同的格式
- 1.3 了解编译系统如何工作是非常有益的
- 1.4 处理器读取并解释存储在内存中的指令
- 1.4.1 系统的硬件组成
-
- I/O 设备
- 1.4.2 运行 hello 程序
- 1.5 高速缓存至关重要
- 1.6 存储设备形成层次结构
- 1.7 操作系统管理硬件
-
- 1.7.1 进程
- 1.7.2 线程
- 1.7.3 虚拟内存
- 1.7.4 文件
- 1.8 网络通信系统之间使用网络通信系统
- 1.9 重要主题
-
- 1.9.1 Amdahl 定律
- 1.9.2 并发和并行
-
- 1. 线程级并发
- 2. 指令级并行
- 3. 单指令,多数据并行
- 1.9.3 抽象在计算机系统中的重要性
- 1.10 小结
- 第 2 章:信息的表达和处理
-
- 第一部分:程序结构和执行:
- 2.1 信息存储
-
- 2.1.1 十六进制表示法
- 2.1.2 字数据大小
- 2.1.3 找址和字节顺序
- 2.1.4 表示字符串
- 2.1.5 表示代码
- 2.1.6 布尔代数简介
- 2.1.7 C语言的位级运算
- 2.1.8 C语言逻辑操作
- 2.1.9 C语言移位操作
- 2.2.1 整形数据表示
- 2.2 .5 C语言中的符号数和无符号数
- 2.2.8 有符号数和无符号数
- 2.3 整数运算
-
- 2.3.1 无符号加法
- 2.3.2 补码加法
- 2.3.4 无符号乘法
- 2.3.5 补码乘法
- 2.4 浮点数
- 2.5 小结
- 第 3 章:程序的机器级表示
-
- 3.2 程序编码
-
- 3.2.1 机器级代码
- 3.2.2代码示例
- 3.2.3 格式注释
-
- 网络旁注
- 3.3 数据格式
- 3.4 访问信息
-
- 3.4.1 操作数指示符
- 3.4.2数据传输指令
- 3.4.3 数据传送示例
- 3.4.4 压入和弹出栈数据
- 3.5 算术和逻辑操作
-
- 3.5.加载有效地址
- 3.5.2 一元和二元操作
- 3.5.3移位操作
- 3.5.5 特殊算数操作
- 3.6 控制
-
- 3.6.1条件码
- 3.6.2 访问条件码
- 3.6.3 跳转指令
- 3.6.4 编码跳转指令
- 3.6.5 实现条件分支
- 3.6.6 实现条件分支
- 3.6.7 循环
-
- dowhile
- while
- for
- 3.6.8 switch语句
- 3.7 过程
-
- 3.7.1 运行时栈
- 3.7.2 转移控制
- 3.7.3 数据传送
- 3.7.4 局部存储在栈上
- 3.7.5 寄存器中的局部存储空间
- 3.8 数组分配和访问
-
-
-
- 3.8.1 基本原则
- 3.8.2 指针运算
- 3.8.3 嵌套的数组
- 3.8.4 定长数组
- 3.8.5 变长数组
-
-
- 3.9 异质数据结构
-
-
-
- 3.9.2 联合
- 3.9.3 数据对齐
- 强制对齐
-
-
- 3.10 将控制与数据结合到机器级程序中
-
- 3.10.1 理解指针
- 3.10.2 应用:使用GDB调试器
- 3.10.3 内存越界引用和缓冲区溢出
- 3.10.4 对抗缓冲区溢出
-
- 栈随机化
- 栈破坏检测
- 限制可执行代码区域
- 3.10.5 支持变长栈帧
- 3.11 浮点代码
-
-
- 3.11.1 浮点传输和转换操作
- 3.11.2 过程中的浮点数
- 3.11.3 浮点运算
- 3.11.7 浮点代码观察结论
-
- 3.12 小结
- 第六章 存储层次结构
-
- 6.1 存储技术
-
- 6.1.1 随机访问存储器
-
- 静态RAM
- 动态RAM
- 传统的DRAM
- 内存模块
- 增强的DRAM
- 非易失性存储器
- 访问主存
- 6.1.2 磁盘存储
-
- 磁盘构造
- 磁盘容量
- 磁盘操作
- 逻辑磁盘块
- 连接IO设备
- 访问磁盘
- 6.1.3 固态硬盘
- 6.1.4存储器技术趋势
- 6.2 局部性
-
- 6.2.1对程序数据引用的局部性
- 6.2.2 取指令的局部性
- 6.2.3局部小结
- 6.3存储器层次结构
-
- 6.3.1 存储器层次结构中的缓存
-
- 缓存命中
- 缓存不命中
- 缓存不命中的种类
- 缓存管理
- 6.3.2 存储器层次结构概念小结
- 6.4高速缓存存储器
-
- 6.4.1 通用的高速缓存器组织结构
- 6.4.2直接映射高速缓存
-
- 直接映射高速缓存中的组选择
- 直接映射高速缓存中的行匹配
- 直接映射高速缓存中的字选择
- 直接映射高速缓存中不命中时的行替换
- 综合 运行中的直接映射高速缓存
- 为什么使用中间的位来做索引
- 6.4.3组相联高速缓存
-
- 组相联高速缓存中的组选择
- 组相联高速缓存中的行匹配和字选择
- 6.4.4 全相联高速缓存
-
- 全相联高速缓存中的组选择
- 全相联高速缓存中的行匹配和字选择
- 6.4.5有关写的问题
- 6.4.6一个真实的高速缓存层次结构
- 6.4.7高速缓存参数的性能影响
- 6.5编写高速缓存友好代码
- 6.6综合 :高速缓存对程序性能的影响
-
- 6.6.1存储器山
- 6.6.2重新排列循环以提高空间局部性
- 6.6.3在程序中利用局部性
- 6.7小结
- 第七章链接
-
- 7.1 编译器驱动程序
- 7.2 静态链接
- 7.3 目标文件
- 7.4 可重定位目标文件
- 7.5 符号和符号表
- 7.6 符号解析
-
- 7.6.1 链接器如何解析多重定义的全局符合
- 7.6.2 与静态库链接
- 7.6.3 链接器如何使用静态库
- 7.7 重定位
-
- 7.7.1 重定位条目
- 7.7.2 重定位符号引用
-
- 1 重定位PC相对引用
- 2 重定位绝对引用
- 7.8 可执行目标文件
- 7.9 加载可执行目标文件
-
-
- 加载器如何工作的
-
- 7.10 动态链接共享库
- 7.11 从应用程序中加载和链接共享库
- 7.12 位置无关代码
-
- 1 PIC 数据引用
- 2 PIC 函数调用
- 7.13 库打桩机制
-
- 7.13.1编译时打桩
- 7.13.2 链接时打桩
- 7.13.3 运行时打桩
- 7.14 处理目标文件的工具
- 7.15 小结
- 第八章异常控制流
-
- 8.1 异常
-
- 硬件异常和软件异常
- 8.1.1 异常处理
- 8.1.2 异常的类别
-
- 中断
- 陷阱和系统调用
- 故障
- 终止
- 8.1.3Linux /x86-64系统中的异常
-
- linux /x86-64故障和终止
- linux/86-64 系统调用
- 8.2进程
-
- 8.2.1逻辑控制流
- 8.2.2并发流
- 8.2.3私有地址空间
- 8.2.4 用户模式和内核模式
- 8.2.5 上下文切换
- 8.3系统调用错误处理
- 8.4进程控制
-
- 8.4.1 获取进程ID
- 8.4.2创建和终止进程
- 8.4.3回收子进程
-
- 判定等待集合的成员
- 修改默认行为
- 检查已回收子进程的退出状态
- 错误条件
- wait函数
- 使用waitpid的示例
- 8.4.4 让进程休眠
- 8.4.5加载并运行程序
- 8.4.6 利用fork和execve运行程序
- 8.5 信号
-
- 8.5.1信号术语
- 8.5.2 发送信号
-
- 进程组
- 用/bin/kill 程序发送信号
- 从键盘发送信号
- 用kill函数发送信号
- 使用alarm函数发送信号
- 8.5.3 接收信号
- 8.5.4阻塞和解除阻塞信号
- 8.5.5 编写信号处理程序
-
- 安全的信号处理
- 正确的信号处理
- 可移植的信号处理
- 8.5.6 同步流以避免讨厌的并发错误
- 8.5.7 显式的等待信号
- 8.6 非本地跳转
- 8.7 操作进程的工具
- 8.8 小结
- 第九章 虚拟内存
-
- 9.1 物理和虚拟寻址
- 9.2地址空间
- **9.3 虚拟内存作为缓存的工具**
-
- 9.3.1 DRAM 缓存的组织结构
- 9.3.2 页表
- 9.3.3 页命中
- 9.3.4 缺页
- 9.3.5 分配页面
- 9.3.6 又是局部性救了我们
- 9.4虚拟内存作为内存管理工具
- 9.5 虚拟内存作为内存保护的工具
- 9.6 地址翻译
-
- 9.6.1 结合高速缓存和虚拟内存
- 9.6.2 利用 TLB 加速地址翻译
- 9.6.3 多级页表
- 9.6.4 综合:端到端的地址翻译
- **9.7 案例研究:Intel Core i7 / Linux 内存系统**
-
- 9.7.1 Core i7 地址翻译
- 9.7.2 Linux 虚拟内存系统
- 1. Linux 虚拟内存区域
- 2 Linux 缺页异常处理
- 9.8内存映射
-
- 9.8.1 再看共享对象
- 9.8.2 再看 fork 函数
- 9.8.3 再看 execve 函数
- 9.9动态内存分配
-
- 9.9.1 malloc 和 free 函数
- 9.9.2 为什么要使用动态内存分配
- 9.9.3 分配器的要求和目标
- 9.9.4 碎片
- 9.9.5 实现问题
- 9.9.6 隐式空闲链表
- 9.9.7 放置已分配的块
- 9.9.8 分割空闲块
- 9.9.9 获取额外的堆内存
- 9.9.10 合并空闲块
- 9.9.11 带边界标记的合并
- 9.9.12 综合:实现一个简单的分配器
- 9.9.13 显式空闲链表
- 9.9.14 分离的空闲链表
-
- 1. 简单分离存储
- \2. 分离适配
- 3.伙伴系统
- 9.10垃圾收集
-
- 9.10.1 垃圾收集器的基本知识
- 9.10.2 Mark&Sweep 垃圾收集器
- **9.11 C 程序中常见的与内存有关的错误**
-
- 9.11.1 间接引用坏指针
- 9.11.2 读未初始化的内存
- 9.11.3 允许栈缓冲区溢出
- 9.11.4 假设指针和它们指向的对象是相同大小的
- 9.11.5 造成错位错误
- 9.11.6 引用指针,而不是它所指向的对象
- 9.11.7 误解指针运算
- 9.11.8 引用不存在的变量
- 9.11.9 引用空闲堆块中的数据
- 9.11.10 引起内存泄漏
- **9.12 小结**
- Labs
- Data labs
-
- 实验附件
- 实验简介
-
- tips
- 第一题
- 第二题
- 第三题
- 第四题
- 第五题
- 第六题
- 第七题
- 第八题
- 第九题
- Bomb Labs
-
- phase1
- phase2
-
- tips
- phase3
-
- tips
- phase4
-
- tips
- phase_5
- phase_6
- secret_phase
-
- tips
- 简介
- Attack Lab
-
- 前置
- 第一部分:代码注入攻击
-
- level1
- level2
- tips
- level3
- 第二部分:返回导向编程ROP
-
- 前言
- 攻击方式
-
- level2
- gadget表
-
- level2-续
- level3
- shell lab
-
- linux信号简介
- Shell 简介
-
- 提示
- 实验开始
- eval
- builtin_cmd
- do_bgfg
- waitfg函数
- sigchld_handler
- sigint_handler
- sigtsp_handler
- 结果
-
- parseline
- Cache lab
- 实验附件
-
- readme
- 概述
- Part A:Writing a Cache Simulator
-
- getopt()
- fscanf()
- Malloc/Free
- Part B: Optimizing Matrix Transpose
-
- PRE
- 为什么需要缓存
- 缓存为什么起作用
- 矩阵乘法分析
-
- 先证明分块矩阵的乘法结果和不分块一致
- **3.2 那么分块矩阵为什么有用那**
- 开始
- 软件安装
- Malloc Lab
-
- PRE
-
- 背景知识
- 相关函数
- 内部碎片和外部碎片
- 吞吐量和利用率
- 查找空闲块
- 第一版隐式空闲链表
-
- 合并的四种情况
- 1.4 总结
- 开始
- 分析代码
深入理解计算机操作系统-第一章
1.1 信息就是位 + 上下文
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
hello.c的 ascii文本表示
像 hello.c 这样只由 ASCII 字符构成的文件称为文本文件,所有其他文件都称为二进制文件。
hello.c 的表示方法说明了一个基本思想∶系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。比如,在不同的上下文中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。
1.2 程序被其他程序翻译成不同的格式
译过程可分为四个阶段完成,如图 1-3 所示。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilation system)。
1.3 了解编译系统如何工作是大有益处的
1.4 处理器读并解释储存在内存中的指令
1.4.1 系统的硬件组成
I/O 设备
1.4.2 运行 hello 程序
利用直接存储器存取(DMA,将在第 6 章中讨论)技术,数据可以不通过处理器而直接从磁盘到达主存。这个步骤如图 1-6 所示。
一旦目标文件 hello 中的代码和数据被加载到主存,处理器就开始执行 hello 程序的 main 程序中的机器语言指令。这些指令将 “hello, world\n” 字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。这个步骤如图 1-7 所示
1.5 高速缓存至关重要
1.6 存储设备形成层次结构
1.7 操作系统管理硬件
操作系统有两个基本功能∶(1)防止硬件被失控的应用程序滥用;(2)向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。操作系统通过几个基本的抽象概念()来实现这两个功能。如图 1-11 所示,文件是对 I/O 设备的抽象表示,虚拟内存是对主存和磁盘 I/O 设备的抽象表示,进程则是对处理器、主存和 I/O 设备的抽象表示。我们将依次讨论每种抽象表示。
1.7.1 进程
是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中,需要运行的进程数是多于可以运行它们的 CPU 个数的。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论是在单核还是多核系统中,一个 CPU 看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为。为了简化讨论,我们只考虑包含一个 CPU 的的情况。我们会在 1.9.2 节中讨论多处理器系统。
1.7.2 线程
尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。由于网络服务器中对并行处理的需求,线程成为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。当有多处理器可用的时候,多线程也是一种使得程序可以运行得更快的方法,我们将在 1.9.2 节中讨论这个问题。在第 12 章中,你将学习并发的基本概念,包括如何写线程化的程序。
1.7.3 虚拟内存
虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。图 1-13 所示的是 Linux 进程的虚拟地址空间(其他 Unix 系统的设计也与此类似)。在 Linux 中,地址空间最上面的区域是保留给操作系统中的代码和数据的,这对所有进程来说都是一样。地址空间的底部区域存放用户进程定义的代码和数据。请注意,图中的地址是
。对所有的进程来说,代码是从同一固定地址开始,紧接着的是和 C 全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的,在示例中就是可执行文件 hello。在第 7 章我们研究链接和加载时,你会学习更多有关地址空间的内容。
。代码和数据区后紧随着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像 malloc 和 free 这样的 C 标准库函数时,堆可以在运行时动态地扩展和收缩。在第 9 章学习管理虚拟内存时,我们将更详细地研究堆。
共享库。大约在地址空间的中间部分是一块用来存放像 C 标准库和数学库这样的共享库的代码和数据的区域。共享库的概念非常强大,也相当难懂。在第 7 章介绍动态链接时,将学习共享库是如何工作的。
。位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。特别地,每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩。在第 3 章中将学习编译器是如何使用栈的。
。地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。
1.7.4 文件
就是字节序列,仅此而已。每个I/O设备,包括磁盘、键盘、显示器,甚至网络,都可以看成是文件。系统中的所有输入输出都是通过使用一小组称为 Unix I/O 的系统函数调用读写文件来实现的。
文件这个简单而精致的概念是非常强大的,因为它向应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的 I/O 设备。例如,处理磁盘文件内容的应用程序员可以非常幸福,因为他们无须了解具体的磁盘技术。进一步说,同一个程序可以在使用不同磁盘技术的不同系统上运行。你将在第 10 章中学习 Unix I/O。
1.8 系统之间利用网络通信
1.9 重要主题
1.9.1 Amdahl 定律
1.9.2 并发和并行
数字计算机的整个历史中,有两个需求是驱动进步的持续动力:一个是我们想要计算机做得更多,另一个是我们想要计算机运行得更快。当处理器能够同时做更多的事情时,这两个因素都会改进。我们用的术语(concurrency)是一个通用的概念,指一个同时具有多个活动的系统;而术语(parallelism)指的是用并发来使一个系统运行得更快。并行可以在计算机系统的多个抽象层次上运用。在此,我们按照系统层次结构中由高到低的顺序重点强调三个层次。
1. 线程级并发
构建在进程这个抽象之上,我们能够设计出同时有多个程序执行的系统,这就导致了并发。使用线程,我们甚至能够在一个进程中执行多个控制流。自 20 世纪 60 年代初期出现时间共享以来,计算机系统中就开始有了对并发执行的支持。传统意义上,这种并发执行只是模拟出来的,是通过使一台计算机在它正在执行的进程间快速切换来实现的,就好像一个杂耍艺人保持多个球在空中飞舞一样。这种并发形式允许多个用户同时与系统交互,例如,当许多人想要从一个 Web 服务器获取页面时。它还允许一个用户同时从事多个任务,例如,在一个窗口中开启 Web 浏览器,在另一窗口中运行字处理器,同时又播放音乐。在以前,即使处理器必须在多个任务间切换,大多数实际的计算也都是由一个处理器来完成的。这种配置称为
当构建一个由单操作系统内核控制的多处理器组成的系统时,我们就得到了一个多处理器系统。其实从 20 世纪 80 年代开始,在大规模的计算中就有了这种系统,但是直到最近,随着多核处理器和超线程(hyperthreading)的出现,这种系统才变得常见。图 1-16 给出了这些不同处理器类型的分类。
多核处理器是将多个 CPU(称为“核”)集成到一个集成电路芯片上。图 1-17 描述的是一个典型多核处理器的组织结构,其中微处理器芯片有 4 个 CPU 核,每个核都有自己的 L1 和 L2 高速缓存,其中的 L1 高速缓存分为两个部分——一个保存最近取到的指令,另一个存放数据。这些核共享更高层次的高速缓存,以及到主存的接口。工业界的专家预言他们能够将几十个、最终会是上百个核做到一个芯片上。
超线程,有时称为(simultaneous multi-threading),是一项允许一个 CPU 执行多个控制流的技术。它涉及 CPU 某些硬件有多个备份,比如程序计数器和寄存器文件,而其他的硬件部分只有一份,比如执行浮点算术运算的单元。常规的处理器需要大约 20000 个时钟周期做不同线程间的转换,而超线程的处理器可以在单个周期的基础上决定要执行哪一个线程。这使得 CPU 能够更好地利用它的处理资源。比如,假设一个线程必须等到某些数据被装载到高速缓存中,那 CPU 就可以继续去执行另一个线程。举例来说,Intel Core i7 处理器可以让每个核执行两个线程,所以一个 4 核的系统实际上可以并行地执行 8 个线程。
2. 指令级并行
在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行。早期的微处理器,如 1978 年的 Intel 8086,需要多个(通常是 3~10 个)时钟周期来执行一条指令。最近的处理器可以保持每个时钟周期 2~4 条指令的执行速率。其实每条指令从开始到结束需要长得多的时间,大约 20 个或者更多周期,但是处理器使用了非常多的聪明技巧来同时处理多达 100 条指令。在第 4 章中,我们会研究(pipelining)的使用。在流水线中,将执行一条指令所需要的活动划分成不同的步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤。这些阶段可以并行地操作,用来处理不同指令的不同部分。我们会看到一个相当简单的硬件设计,它能够达到接近于一个时钟周期一条指令的执行速率。
3. 单指令、多数据并行
1.9.3 计算机系统中抽象的重要性
抽象的使用是计算机科学中最为重要的概念之一。例如,为一组函数规定一个简单的应用程序接口(API)就是一个很好的编程习惯,程序员无须了解它内部的工作便可以使用这些代码。不同的编程语言提供不同形式和等级的抽象支持,例如 Java 类的声明和C语言的函数原型。
在学习操作系统时,我们介绍了三个抽象:。我们再增加一个新的抽象∶ ,包括操作系统、处理器和程序。虚拟机的思想是 IBM 在 20 世纪 60 年代提出来的,但是最近才显示出其管理计算机方式上的优势,因为一些计算机必须能够运行为不同的操作系统(例如,Microsoft Windows、MacOS 和 Linux)或同一操作系统的不同版本设计的程序。
1.10 小结
操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象∶1)文件是对 I/O 设备的抽象;2)虚拟内存是对主存和磁盘的抽象;3)进程是处理器、主存和 I/O 设备的抽象。
计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们依据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是 ASCII 文本,然后被编译器和链接器翻译成二进制可执行文件。
处理器读取并解释存放在主存里的二进制指令。因为计算机花费了大量的时间在内存、I/O 设备和 CPU 寄存器之间复制数据,所以将系统中的存储设备划分成层次结构——CPU 寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM 主存和磁盘存储器。在层次模型中,位于更高层的存储设备比低层的存储设备要更快,单位比特造价也更高。层次结构中较高层次的存储设备可以作为较低层次设备的高速缓存。通过理解和运用这种存储层次结构的知识,程序员可以优化C程序的性能。
最后,网络提供了计算机系统之间通信的手段。从特殊系统的角度来看,网络就是一种 I/O 设备
第 2 章:信息的表示和处理
第一部分:程序结构和执行
我们研究三种最重要的数字表示。(unsigned)编码基于传统的二进制表示法,表示大于或者等于零的数字。 (two’s-complement)编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。(floating-point)编码是表示实数的科学记数法的以 2 为基数的版本。计算机用这些不同的表示方法实现算术运算,例如加法和乘法,类似于对应的整数和实数运算。
计算机的表示法是用有限数量的位来对一个数字编码,因此,当结果太大以至不能表示时,某些运算就会 (overflow)。溢出会导致某些令人吃惊的后果。例如,在今天的大多数计算机上(使用 32 位来表示数据类型 int),计算表达式 200∗300∗400∗500\small 200300400*500200∗300∗400∗500 会得出结果 -884901888。这违背了整数运算的特性,计算一组正数的乘积不应产生一个负的结果
2.1 信息存储
2.1.1 十六进制表示法
转换
2.1.2 字数据大小
64位向下兼容
gcc -m32 prog.c
ISOC99 固定了int32_t int64_t 固定为4个字节,8个字节,不根据编译器带来的奇怪行为,数据大小是固定的。
如下四个都是一个意思
2.1.3 寻址和字节顺序
小端模式,大端模式
2.1.4 表示字符串
练习题2.7 下面对show_bytes的调用将输出什么结果?
const char *s = "abcdef";
show_bytes((byte_pointer) s, strlens(s));
注意字母 ‘a' ~ 'z' 的ASCII码为0x61~0x7A
解:输出 61 62 63 64 65 66(库函数strlen不计算终止的空白符,所以show_bytes只打印到字符 ’f’ )
linux64 的指针值是8字节
2.1.5 表示代码
二进制代码不兼容的,很少能苟互相组合,
2.1.6 布尔代数简介
2.1.7 C语言的位级运算
2.1.8 C语言的逻辑运算
逻辑运算和位运算不同,
2.1.9 C语言的移位运算
算数右移,不同,所有的编译器,都对有符号的数字使用算数右移
无符号数,右移必须是逻辑
加法比移位运算优先级高
2.2.1 整型数据表示
2.2 .5 C语言中的有符号数与无符号数
有u即被认为是无符号
u的话会有隐式转换
2.2.8 关于有符号数,与无符号数
非直观的隐式转换非常容易产生错误
产生的bug
2.3 整数运算
两个正数,相加 居然会出现 负数,这是因为 计算机运算性能的有限造成的
机组学的----------忘了QAQ
2.3.1 无符号加法
2.3.2 补码加法
2.3.4 无符号乘法
2.3.5 补码乘法
2.4 浮点数
我们表示分数只能近似的表示,不能准确的表示
2.5 小结
位模式 最终也就是16进制的标准
浮点数 需要采用IEEE标准754 定义的 可以表示特殊值+∞ 等
浮点数运算必须小心,不遵守普遍的算术属性
第 3 章:程序的机器级表示
3.2 程序编码
假设有一个C程序 两个文件p1.c 和p2.c 我们使用 unix编译
linux> gcc -Og -o p p1.c p2.c
编译选项 ✦-Og✦ 告诉编译器使用会生成符合原始 C 代码整体结构的机器代码的优化等级。
使用较高级别优化产生的代码会严重变形,以至于产生的机器代码和初始源代码之间的关系非常难以理解
。实际中,从得到的程序的性能考虑,较高级别的优化(例如,以选项 -O1 或 -O2 指定)被认为是较好的选择
GCC 版本 4.8 引入了这个优化等级,较早的gcc和gnu编译器不认识这个选项
扩展源代码,插入所有用 命令指定的文件,并扩展所有 声明指定的宏。其次,产生两个源文件的汇编代码,名字分别为 p1.s 和 p2.s。接下来,会将汇编代码转化成二进制 p1.o 和 p2.o。目标代码是机器代码的一种形式,它包含所有指令的二进制表示,但是还没有填入全局值的地址。最后,将两个目标代码文件与实现库函数(例如 printf)的代码合并,并产生最终的可执行代码文件 p(由命令行指示指定的)。
3.2.1 机器级代码
正如在 1.9.3 节中讲过的那样,计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。对于机器级编程来说,其中两种抽象尤为重要。第一种是由指,它定义了处理器状态、指令的格式,以及每条指令对状态的影响。大多数 ISA,包括 x86-64,将程序的行为描述成好像每条指令都是按顺序执行的,一条指令结束后,下一条再开始。处理器的硬件远比描述的精细复杂,它们并发地执行许多指令,但是可以釆取措施保证整体行为与 ISA 指定的顺序执行的行为完全一致**。第二种抽象是,机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组。**存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来
在整个编译过程中,编译器会完成大部分的工作,将把用 C 语言提供的相对比较抽象的执行模型表示的程序转化成处理器执行的非常基本的指令。汇编代码表示非常接近于机器代码。与机器代码的二进制格式相比,汇编代码的主要特点是它用可读性更好的文本格式表示。能够理解汇编代码以及它与原始 C 代码的联系,是理解计算机如何执行程序的关键一步
3.2.2代码示例
生成了汇编文件s
生成目标代码文件,他是二进制形式的
这个命令必须分开,否则会报错
gcc -O -g -c mstore.c
要查看机器代码,可以使用objdump
objdump -d mstore.o
每组机器语言代表的汇编指令,每组一到五个字节
只有指令push q %rbx是以字节值53开头的
反汇编中q被省略了
main函数用来运行链接器
我们编译一个main
#include<stdio.h>
void mulstore(long,long,long *);
int main(){
long d;
multstore(2,3,&d);
printf("2 * 3 --> %ld\n", d);
return 0;
}
long mult2(long a,long b){
long s = a * b;
return s;
}
执行命令
gcc -O -g -o prog main.c mstore.c
文件prog不仅包含了两个过程的代码,还包含了用来启动贺终止程序的代码,以及用来与操作系统交互的代码,我们也可以反编译prog文件
中间包含了multstore
几乎一致,他callq不一致,这里链接器填上了callq指令调用函数mult2 使用的地址,
多了一个nop 是为了使函数代码变成16字节
能够更好的防止一个代码块
3.2.3 关于格式的注释
-S 生成汇编语言
我们首先执行命令
gcc -Og -S mstore.c
以点开头的行都是指导汇编器贺链接器工作的伪指令,可以忽略
C语言中直接内联汇编
ATT与intel汇编代码格式
网络旁注
asm:easm
3.3 数据格式
我们是从16位 扩展成为32位的,
字称为16位数据类型,
32位为双字
64位为四字
指针存储为8字节的四字,
x86-64位中数据类型long 实现为64位,也就是4字
图如下
单精度 4字节 float
双精度 8字节 double
不建议使用long double 只有x86可以使用
大多数gcc有一个字符的后缀
3.4 访问信息
寄存器名字的更替
64位寄存器
3.4.1 操作数指示符
大多数指令有一个或多个操作数,
执行一个操作要使用的源数据,以及放置结果的目的位置
源数据值可以 以常数形式给出,或者从寄存器或者内存中 读出
结果可以放在寄存器或者内存中,
一、 立即数
$后跟C表示法表示的整数
二、寄存器
三、内存引用
3.4.2数据传送指令
最频繁的使用的指令是将数据从一个位置复制到另一个位置的指令,操作数的通用性,使得一条简单的数据传送指令能够完成在许多机器好几条指令的功能,
mov分为 movb movw movl movq 主要是操作的数据大小不同
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KXL1jauh-1635949435769)(…/…/AppData/Roaming/Typora/typora-user-images/image-20211031211625651.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DdfU3LPp-1635949435770)(…/…/AppData/Roaming/Typora/typora-user-images/image-20211031211620630.png)]
源操作数 指定的 是一个 立即数 存储在 寄存器 或者内存中
但是
x86-64中,两个操作数不能都指向内存位置,将一个值从内存位置复制到另一个内存位置,需要两条指令,第一个将源值加载到寄存器,第二条写入目的位置
同时,寄存器部分的大小必须 与 指令的最后一个字符 指定的大小匹配,
大多数中,mov指令只会更新目的操作数指定的
那些寄存器字节或者内存位置
例外:mol指令 以寄存器作为目的时,他会把寄存器的高四位设置成0,
x86特性,任何寄存器生成32为值得指令, 都会把高位部分设置成0
零扩展:汇编中,装在到更长的寄存器或者内存中,前面多的补0,就叫零扩展
3.4.3 数据传送示例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RMGhkNVQ-1635949435776)(https://gitee.com/dingpengs/image/raw/master/imgwin//20211031221317.png)]
指针其实就是地址
x这种局部变量,通常保存在寄存器中,而不是内存中
3.4.4 压入和弹出栈数据
栈顶元素的地址在所有栈中元素地址中最低
我们倒着画图 栈顶在图的底部
栈指针 %rsp保存着栈顶元素的地址
3.5 算术和逻辑操作
add由四种构成
3.5.1加载有效地址
也就是leaq 也就是movq指令的变形
也就是从内存读数据到寄存器,但实际上根本没有引入内存,他是将有效地址写入到目的操作数
也就相当于 C语言中的地址操作符 &S 说明这种计算,这种指令可以为后面内存的内存引用产生指针,还可以简介的描述普通的算术操作,
leaq有很多的灵活用法, 和有效地址计算无关,目的操作数必须是一个寄存器
leaq指令能执行加法和有限形式的乘法,在编译简单的算数表达式很有用
3.5.2 一元和二元操作
3.5.3移位操作
一个字节的移位量
左移,指令有两个,SAL SHL 两者效果一样,右边填0
右移 SAR 算数移位,填符号位,SHR逻辑移位 填0
3.5.5 特殊的算数操作
3.6 控制
3.6.1条件码
实现条件操作的两种方式,以及 描述 表达循环和switch语句的方法
条件码,检测寄存器执行条件分支指令
leaq指令不改变任何条件吗,因为他用来进行地址计算
还有两类指令 只设置条件吗而不改变其他寄存器
CMP 以及Sub
ATT格式中 列出操作数的顺序是相反的
TEST和AND指令一样,
3.6.2 访问条件码
setl
set指令
3.6.3 跳转指令
jmp 无条件跳转
3.6.4 跳转指令的编码
jg向后跳转
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HV7Hk5Ha-1635949435808)(https://gitee.com/dingpengs/image/raw/master/imgwin//20211102215433.png)]
相对寻址的优势
不用改代码,就能跳到不同的地址
rep 和repz 是为了在amd上运行的更快
repz 是rep同义词,retq 是ret的同义名
3.6.5 用条件控制来实现条件分支
跳转其实和goto有相似的地方,
3.6.6 使用条件传送来实现条件分支
3.6.7 循环
也就是do while while for 三个
dowhile
while
for
3.6.8 switch语句
跳转表
3.7 过程
过程是软件的一种很重要的抽象,提供了封装代码的方式,用一组指定的参数和一个可选的返回值 实现了某种功能
然后可以在程序中的不同地方调用这个函数·,可以隐藏某个行为的具体实现,不同的编程语言,过程的形式是多样的
函数
方法
子例程
处理函数等
3.7.1 运行时栈
C语言使用了栈机械能内存管理,在过程P 调用过程Q的例子中,,P在向上追溯调用链的过程中,都是被暂时挂起的
运行q时,只需要为局部变量分配新的存储空间,或者设置到另一个过程的调用
当Q返回时,所有他分配的局部内存都将始放,当P调用Q的时候,控制和数据信息添加到栈尾,当P返回时,这些信息就会被释放掉
当过程需要的存储空间超出寄存器,就会在栈分配空间,这个部分成为过程的栈帧如图3-25
3.7.2 转移控制
使用call Q 调用过程Q来记录
、、对栈的操作
3.7.3 数据传送
需要对数据作为参数传送,返回值还可能包括返回值
可以通过寄存器来当参数
超过6个使用栈来存储
3.7.4 栈上的局部存储
有些时候,局部数据必须存放在内存中,因为寄存器放不下
3.7.5 寄存器中的局部存储空间
3.8 数组分配和访问
3.8.1 基本原则
DB因为8 是因为他们是指针数组
读取e[i]
3.8.2 指针运算
也就是包括一些& * 可以产生指针和间接引用指针
&Expr 是给出该对象地址的一个指针,
*Aexpr给出该地址处的值
假设整型的数组E 起始地址和整数索引i分别存放在%rdx %rcx中
3.8.3 嵌套的数组
例如 int A[5][3]
也就等价于
3.8.4 定长数组
我们使用常数的时候一般使用 #define
3.8.5 变长数组
3.9 异质的数据结构
结构体 struct 声明
联合 union 声明
3.9.2 联合
3.9.3 数据对齐
计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须式某个值K 就是 2/4/8的倍数
这种对齐 简化了形成处理器和内存系统之间接口的硬件设计
补充
编译器结构的末尾可能需要一些补充
强制对齐
3.10 在机器级程序中将控制与数据结合起来
熟练GDB
3.10.1 理解指针
第一个是int 类型对象的指针,第二个是 cpp指针指向的对象自身就是一个char类型的指针
特殊的void * 代表通用指针
比如,malloc 函数返回一个通用指针,然后通过显示强制类型转换 或者赋值 的隐式强制类型转换,将他转换成一个有类型的指针,
指针是C语言提供的抽象
这里为什么是 (int *)p+7 结果却是p+28 呢
因为这里 p由char 转为了 int指针, 那么一个int 占用了4个字节,也就是 p+1 也就相当于p+4 (对于指针来说)(要对齐)
所以就是28、
3.10.2 应用:使用GDB调试器
gdb prog
通常可以在程序中感兴趣的地方附近设置断点,断点可以增加在函数入口的后面,或者是一个程序的地址处
程序在执行过程中遇到一个断点的时候,程序就会停下来,兵控制返回给用户,
3.10.3 内存越界引用和缓冲区溢出
c语言对于数组的引用不进行任何边界检查,而且局部变量和状态信息 (例如保存的寄存器值和返回地址)都存放在栈中,这两种情况就能造成严重的车工序错误,对于越界的数组元素的写操作就会破坏存储在栈中的状态信息,当程序使用这个个被破坏的状态,试图重新加载寄存器或者执行ret指令时,就会有严重的错误
3.10.4 对抗缓冲区溢出
栈随机化
使得栈的位置在程序每次运行时都有变化,所以,即使许多机器都运行同样的代码,但是他的栈地址不同,
linux中 栈随机化已经成为标准,
技术被称为 地址空间布局随机化
每次运行程序时,包括程序代码,库代码,栈,全局变量,堆数据,都会被加载到内存的不同区域
但是我们可以进行蛮力克服,反复的使用不同的地址攻击,
栈破坏检测
计算机的第二道防线 是能够检测到何时栈已经被破坏,我们在echo函数示例中看到,破坏通常发生在 当超越局部缓冲区的边界时,在C语言中,没有可靠的方法来防止对数组的越界写 。但是当我们能够在发生了越界写的时候,在造成任何有害结果前,检测到。
有栈保护者,stack protector 机制,检测缓冲区越界,
在栈帧 中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值,
这个值也被叫做 哨兵值
每次程序运行时候随机生成,因此攻击者无法简单的知道 它是什么,在恢复寄存器状态和从函数返回之前,在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者 该函数调用的某个函数的某个操作改变,改变,程序就会异常终止
如果我们想要使用栈溢出,我们就得使用命令行
-fno-stack-protector
阻止GCC产生这种代码
使用栈保护时,得到如下汇编(编译echo函数)
限制可执行代码区域
只保存编译器产生的代码的那部分内存才需要是可执行的,其他部分可以被限制为只允许读和写
AMD最近 为他的64位处理器 的内存保护引入了NX 不执行位,将读和执行访问模式分开,
这个特性,栈可以被标记为可读和可写,但是不可执行,而检查页是否可执行由硬件来完成,效率没有损失
3.10.5 支持变长栈帧
alloca时候 可以在栈上分配任意字节数量的存储,
有些函数需要局部存储变长,而不是预先确定需要分配多少
需要理解 对齐和数组
使用了一个帧指针,也叫做基指针,用来保存旧的状态
3.11 浮点代码
3.11.1 浮点传送和转换操作
图3.48整数转换为浮点数
3.11.2 过程中的浮点数
xmm寄存器 最多传递8个浮点参数