CSAPP 第三章 读书笔记

程序的机器级表示

AT&T与Intel格式的汇编代码

我们的表述是ATT(根据“AT&T”命名的, AT&T是运营贝尔实验室多年的公 司)格式的汇编代码,这是GCC、 OBJDUMP和其他一些我们使用的工具的默认格式。 其他一些编程工具,包括Microsoft的工具,以及来自Intel的文档,其汇编代码都是Intel格式的。这两种格式在许多方面有所不同。比如下面的几点:

  • intel代码省略了指示大小的后缀。比如使用push和pop,而不是pushl和popl。
  • intel代码省略了寄存器前面的“%”符号,用的是rbx而不是%rbx。
  • intel代码用不同的方式来描述内存中的位置,例如是“QWORD PTR[rbx]”,而不是“(%rbx)”。
  • 在带有多个操作数的指令情况下,列出操作数的顺序相反。当在两种格式之间进 行转换的时候,这一点非常令人困惑。

数据格式

由于是从16位体系结构扩展成32位的, Intel用术语“字(word)”表示16位数据类型。因此,称32位数为“双字(double words)”,称64位数为“四字(quad words)”。 在x86-64中。标准int值存储为双字(32位)。指针(在此用char *表示)存储为8字节的四字, 64位机器本来就预期如此。

信息访问

一个x86-64的中央处理单元(CPU)包含一组16个存储64位值的通用目的寄存器。 这些寄存器用来存储整数数据和指针。

寻址模式

有多种寻址模式,如下图所示:

数据传送 MOV类指令

这些指令把数据从源位置复制到目的位置,不做任何变化。MOV类由四条指令组成‥ movb、 movb、 movl和 movq。这些指令都执行同样的操作;主要区别在于它们操作的数据大小不同:分别是1、2、4和8字节。

算数逻辑操作

这种操作可以分为四种::加载有效地址、一元操作、二元操作和移位。

其中,leaq指令能执行加法和有限形式的乘法,在编译如上简单的算术表达式时,是很有用处的。

控制

机器代码提供两种基本的低级机制来实现有条件的 行为:测试数据值,然后根据测试的结果来改变控制流或者数据流。

条件码

除了整数寄存器, CPU还维护着一组单个位的条件码(condition code)寄存器,它们描述了最近的算术或逻辑操作的属性。可以检测这些寄存器来执行条件分支指令。最常用的条件码有:

  • CF:进位标志。最近的操作使最高位产生了进位。可用来检查无符号操作的溢出。
  • ZF:零标志。最近的操作得出的结果为0。
  • SF:符号标志。最近的操作得到的结果为负数。
  • OF:溢出标志。最近的操作导致一个补码溢出一正溢出或负溢出。

对于逻辑操作,例如xoR,进位标志和溢出标志会设置成0。对于移位操作,进位标志将设置为最后一个被移出的位,而溢出标志设置为0。INC和DEC指令会设置溢出和零标志,但是不会改变进位标志。

跳转指令

跳转(jump)指令会导致执行切换到程序中一个全新的位置。在汇编代码中,这些跳转的目的地通常用一个标号(lable)指明。常用的跳转指令如下:

循环

C语言提供了多种循环结构,即do-While、While和for。汇编中没有相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。

运行时栈

C语言过程调用机制的一个关键特性(大多数其他语言也是如此)在于使用了栈数据结构提供的后进先出的内存管理原则。在过程P调用过程Q的例子中,可以看到当Q在执行时, P以及所有在向上追溯到p的调用链中的过程,都是暂时被挂起的。当Q运行时,它只需要为局部变量分配新的存储空间,或者设置到另一个过程的调用。 另一方面,当Q返回时,任何它所分配的局部存储空间都可以被释放。因此,程序可以用栈来管理它的过程所需要的存储空间,栈和程序寄存器存放着传递控制和数据、分配内存所需要的信息。当P调用Q时,控制和数据信息添加到栈尾。当p返回时,这些信息会释放掉。通用的堆栈示意图如下:

栈上的局部存储

有些时候,局部数据必须存放在内存中,常见的情况包括:

  • 寄存器不足够存放所有的本地数据。
  • 对一个局部变量使用地址运算符“&”,因此能够为它产生一个地址。
  • 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。在描述数组和结构分配时,我们会讨论这个间题。

一般来说,过程通过减小栈指针在栈上分配空间。分配的结果作为栈帧的一部分,标 号为“局部变量”。

数组的分配和访问

C语言中的数组是一种将标量数据聚集成更大数据类型的方式。C语言实现数组的方式非常简单,因此很容易翻译成机器代码。C语言的一个不同寻常的特点是可以产生指向数组中元素的指针,并对这些指针进行运算。在机器代码中,这些指针会被翻译成地址计算。优化编译器非常善于简化数组索引所使用的地址计算。

指针运算

C语言允许对指针进行运算,而计算出来的值会根据该指针引用的数据类型的大小进行伸缩。单操作数操作符‘&’和‘*’可以产生指针和间接引用指针。对应的汇编代码如图所示。

变长数组

ISO C99引人了一种功能,允许数组的维度是表达式,在数组被分配的时候才计算出来。 在变长数组的C版本中,我们可以将一个数组声明如下:

int A[exp1][exp2];

它可以作为一个局部变量,也可以作为一个函数的参数,然后在遇到这个声明的时候,通过对表达式exp1和exp2求值来确定数组的维度。因此,例如要访问n×n的数组的元素i,j,我们可以写一个如下的函数:

对应的汇编代码如下:

异构的数据结构

结构

C语言的struct声明创建一个数据类型,将可能不同类型的对象聚合到一个对象中。用名字来引用结构的各个组成部分。类似于数组的实现,结构的所有组成部分都存放在内存中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址。编译器维护关于每个结构类型的信息,指示每个字段(field)的字节偏移。它以这些偏移作为内存引用指令中的位移,从而产生对结构元素的引用。

联合

联合提供了一种方式,能够规避C语言的类型系统,允许以多种类型来引用一个对象。联合声明的语法与结构的语法一样,只不过语义相差比较大。它们是用不同的字段来引用相同的内存块。

数据对齐

许多计算机系统对基本的数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2,4,8)的倍数。这种对其限制简化了形成处理器和内存系统之间接口的硬件设计。无论数据是否对齐,x86-64的硬件都能正确工作。不过,intel还是建议要对其数据以提高系统性能。

理解指针

  • 每个指针都对应一个类型。
  • 每个指针都有一个值
  • 指针用&运算符创建。
  • *操作符用于间接引用指针。
  • 数组与指针紧密联系。一个数组的名字可以像一个指针变量一样引用。
  • 将指针从一种类型转换成另一种类型不会改变它的值。
  • 指针也可以指向函数。

GDB调试器常用命令

内存越界引用和缓冲区溢出

c对数组不进行任何边界检查,因此当对越界的数组元素进行写操作会破坏存储在栈中的信息。一种特别常见的状态破坏称为缓冲区溢出。

缓冲区溢出一个很致命的使用就是让程序执行它本来不想执行函数,这是一种常见的通过计算机网络攻击系统安全的方法。输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击代码,另外还有一些字节会用一个指向攻击代码的指针覆盖返回区域。那么执行ret指令的效果就是跳转到攻击代码。

对抗缓冲区溢出可以采取以下方法:

  • 栈随机化。使得栈的位置在程序每次运行时都有变化。
  • 栈破坏检测。能够检测到栈何时已被破坏。最新的GCC版本加入了一种栈保护者机制来检测缓冲区越界。其思想是在任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值。
  • 限制可执行代码区域。消除攻击者向系统中插入可执行代码的能力。

猜你喜欢

转载自www.cnblogs.com/thechosenone95/p/10056390.html