§第六章 存储器层次结构

  1. 在简单模型中,存储器系统是一个线性的字节数组,而CPU能够在一个常数时间内访问每个存储器位置。实际上,存储器系统(memory system)是一个具有不同容量、成本和访问时间的存储器层次结构。CPU寄存器保存着最常用的数据。靠近CPU的小的、快速的高速缓冲存储器(cache memory)作为一部分存储在相对慢速的主存储器(main memory)中的数据和指令的缓冲区域。主存暂时存放存储在容量较大、慢速磁盘上的数据,而这些磁盘又常常作为存储在通过网络连接的其它机器的磁盘上的数据的缓冲地带。

  2. 如果程序所需的数据存储在CPU寄存器中,那么在指令的执行期间,在0个周期内就能访问到它们。如果在高速缓冲存储器内,需要130个周期。如果存储在主存中,需要50200个周期。而如果在磁盘上,则需要大约几千万个周期。

3.存储器层次结构围绕着计算机程序的一个称为局部性(locality)的基本属性。具有良好局部性的程序倾向于一次又一次的访问相同的数据项集合,或是倾向于访问邻近的数据项集合。局部性通常有两种不同的形式:时间局部性(temporal locality)和空间局部性(spatial locality)。

  1. 由于历史原因,虽然ROM中有的类型既可以读又可以写,但是整体上还是叫做只读存取器(Read-Only Memory,ROM),存储在ROM中的 程序常常被称为固件(firmware)。

  2. 理解存储器层次结构本质的程序员能够利用这些知识编写出更有效的程序,无论具体的存储器系统是怎样实现的。特别地,我们推荐以下技术:1)将注意力集中在内循环上,大部分计算和存储器访问都发生在这里。2)通过按照数据对象存储在存储器中的顺序、以步长为1来读取数据,从而使程序的空间局部性最大。3)一旦程序中读入了一个数据对象,就尽可能多的使用它,从而使程序中的时间局部性最大。

Read More

§第三章 程序的机器级表示

  1. GCC以汇编代码的形式产生输出,汇编代码是机器代码的文本表示,给出程序中的每一条指令。然后GCC调用汇编器和链接器,从而根据汇编代码生成可执行的机器代码。

  2. 现代编译器的优化产生的代码至少与一个熟练的汇编语言程序员手工编写的代码一样的高效和简洁。用高级语言编写的程序可以在很多不同的机器上编译和执行,而汇编代码则是与特定机器密切相关的。

  3. 程序员学习汇编代码的需求随着时间的推移也发生了变化,开始时要求程序员能直接使用汇编语言编写程序,现在则要求他们能够阅读和理解编译器产生的代码。

  4. 对于机器级编程来说,其中两种抽象尤为重要。第一种是机器级程序的格式和行为,定义为指令集体系结构(Instruction set architecture,ISA)它定义了处理器状态,指令的格式,以及每条指令对状态的改变。大多数ISA,包括IA32和x86_64,将程序的行为描述成好像每条指令按顺序执行的,一条信息结束后,下一条再开始。处理器的硬件远比描述的精细复杂,它们并发地执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行完全一致。第二种抽象是,机器级程序使用的存储器地址是虚拟地址,提供的存储器模型看上去像是一个非常大的字节数组。储存器系统的实现实际上是将多个硬件存储器和操作系统软件组合起来的。

  5. 虽然C语言提供了一种模型,可以在存储器中声明和分配各种数据类型的对象,但是机器代码只是简单的把存储器看成一个很大的、按字节寻址的数组。C语言中的聚合数据类型,例如数组和结构,在机器代码中用连续的一组字节来表示。即使是标量数据类型,汇编代码也不区分有符号数或无符号整数,不区分各种类型的指针,甚至不区分指针和整数。

  6. 虽然IA32的32位地址可以寻址4GB的地址范围,但是通常一个程序只会访问几兆字节。操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器储存器(processor memory)中的物理地址。

  7. 由于是从16位体系结构扩展成32位的,Intel用术语“字”(word)表示16位数据类型。因此,称32位数为“双字”(double words),称64位数为“四字”(quad words)。

Read More

§第一章 计算机系统漫游

  1. 只由ASCII字符构成的文件称为文本文件,所有其他的文件都称为二进制文件。

  2. 区分不同数据对象的唯一方法是我们读到这些数据时候的上下文。

  3. 汇编为不同的高级语言的编译器提供了通用的输出语言。

  4. 从物理上来说,主存是由一组动态随机存取存储器(DRAM)组成的。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(即数组索引),这些地址是从0开始的。

  5. 利用直接储存器存取(DMA),数据可以不通过处理器而直接从磁盘到达主存。

  6. 对处理器而言,从磁盘驱动器上读取一个字的开销要比从主存中读取的开销大100万倍。

  7. 高速缓存的局部性原理:即程序具有访问局部区域里的数据和代码的趋势。

  8. 操作系统有两个基本的功能:1)防止硬件被失控的应用程序滥用 2)向应用程序提供简单一致的机制来控制复杂而通常大相径庭的低级硬件设备。操作系统通过几个基本的抽象概念(进程、虚拟存储器和文件)来实现这两个功能。

Read More

上回我们简单的介绍了缓冲区溢出的基本原理和机器级代码的解释,对此类问题的分析和研究都必须建立在对程序的机器级表示有一定的了解的基础上。记得有句话是这样说的,“真正了不起的程序员是对自己代码的每一个字节都了如指掌的程序员。”我们也许做不到每一字节,但至少得明晰机器级程序的组成结构和执行流程。

言归正传,我们今天在上回的基础上继续探索缓冲区溢出。之前的例子都是简单的通过越界访问来实现对程序执行流程的变动,而且执行的函数都是编译前写入的,那么如何对一个发行版的可执行程序进行缓冲区溢出呢? 首先,这个程序必须存在缓冲区溢出漏洞(这不是废话么),一般来说C语言中容易引起缓冲区溢出的函数有strcpy,strcat之类的不顾及缓冲区大小的内存操作函数以及scanf,gets之类的IO函数。如果你使用vs2010以及vs2012附带的C编译器cl.exe编译使用了这些函数的C代码,编译器一般会给出一个编号为4996的警告,大致的意思是这类函数如scanf不安全,请使用它们的安全版本scanf_s什么的。其实也就是给这些函数加上一个描述缓冲器大小的参数,以防止缓冲区溢出。

我们就以一个相对简单的函数gets开始研究吧。gets函数的实现想必大家都比较清楚吧,gets不考虑缓冲区大小,将输入缓冲中的内容逐一复制到内存指定位置,遇’\n’结束并且自动将’\n’替换为’\0’。

编译后我们同objdump反汇编,命令是 objdump -d -M intel overflow (overflow是可执行文件名字),同理,我们只要 main函数的实现:

Read More