《深入理解计算机系统》第三章 程序的机器级表示学习 读书笔记
一、这章主要任务:
二、程序编码
计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。对于机器级编程来说,其中两种抽象尤为重要:
1、指令集体系结构或指令级框架:它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
IA32将程序的行为描述成好像每条指令时按顺序执行的,一条指令结束后,下一条再开始。(实际上处理器并发地执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行完全一致)
2、机器级程序使用的存储器地址是虚拟地址:提供的存储器模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。
程序存储器(program memory)包含:程序的可执行机器代码、操作系统需要的一些信息、栈、堆。程序存储器用虚拟地址来寻址(此虚拟地址不是机器级虚拟地址)。操作系统负责管理虚拟地址空间(程序级虚拟地址),将虚拟地址翻译成实际处理器存储器中的物理地址(机器级虚拟地址)。
用c语言写一个代码文件mstore.c:
在命令行上使用“-S”选项,就能看到C语言编译器产生的汇编代码:
Linux> gcc -Og -S mstore.c
通过ls查看可以发现,产生了一个汇编文件mstore.s,汇编代码文件包含各种声明,包括下面几行:
查看机器代码文件的内容,有一类称为反汇编器,带“-d”命令行:
linux> objdump -d mstore.o,
结构如下:
三、数据格式
四、访问信息
1.x86-64的CPU包含一组16个存储64位值的通用目的寄存器,用来存储整数数据和指针。
2.x86-64的CPU支持多种操作数格式,各种不同操作数可能性被分为三种类型,分为立即数,用来表示常数;寄存器,用来表示某个寄存器的内容;内存引用,根据计算出来的地址访问某个内存位置。
3.最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。将许多不同的指令划分为指令类,每一类中的指令执行相同的操作,只不过操作数大小不同。
4.MOV类由四条指令构成:movb、movw、mov1和movq。大多数情况下,MOV指令只会更新目的操作数指定的那些寄存器字节或内存位置。
6.最后两个数据传送操作可以将数据压入程序栈中,以及从程序栈中弹出数据。
五、算术和逻辑操作
1.x86-64的一些整数和逻辑操作。
加载有效地址指令leaq实际上是movq指令的变形。它的指令形式是从内存读数据到寄存器,但实际上并没有引用内存。
2.一元操作只有一个操作数,既是源又是目的。二元操作中第二个操作数既是源又是目的。
3.移位操作先给出移位量,然后第二项给出要移位的数,进行算术和逻辑右移。
六、控制
1.除了整数寄存器,CPU还维护着一组单个位的条件码寄存器,它们描述了最近的算术或逻辑操作的属性。
CF:进位标志。
ZF:零标志。
SF:符号标志。
OF:溢出标志。
2.leaq 指令不会改变任何条件码。CMP(和SUB行为一样)和TEST(和ADD行为一样)指令会设置条件码,但不改变任何其他寄存器。
3.条件码通常不会直接读取,通常使用的方法有三种:
1). 根据条件码的某种组合,将一个字节设置为0或者1。
2). 可以条件跳转到程序的某个其他部分。
3). 有条件地传送数据
4.跳转指令会导致执行切换到程序中一个全新的位置。jump指令是无条件跳转,它可以是直接跳转,也可以是间接跳转。
5.将条件表达式和语句从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。
6.实现条件操作的传统方法是通过使用控制的条件转移,一种替代的策略是使用数据的条件转移。
7.汇编中使用条件测试和跳转组合起来实现C语言中多种循环结构的效果。
七、过程
1.过程是软件中一种很重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现某种功能。然后,可以在程序中不同的地方调用这个函数。过程机制的构建需要实现传递控制、传递数据、分配和释放内存。
2.当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧。
八、异质的数据结构
Struct 和 Union有下列区别:
1.在存储多个成员信息时,编译器会自动给struct第个成员分配存储空间,struct 可以存储多个成员信息,而Union每个成员会用同一个存储空间,只能存储最后一个成员的信息。
2.都是由多个不同的数据类型成员组成,但在任何同一时刻,Union只存放了一个被先选中的成员,而结构体的所有成员都存在。
3.对于Union的不同成员赋值,将会对其他成员重写,原来成员的值就不存在了,而对于struct 的不同成员赋值 是互不影响的。
eg struct按声明的变量顺序来存放的,所以下面例子中在内存中至少占用 4+1+2 = 7 byte。然而实际中占用的内存并不是7 byte,这就涉及到了字节对齐方式。
struct sTest
{
int a; //sizeof(int) = 4
char b; //sizeof(char) = 1
shot c; //sizeof(shot) = 2
}x;
union 的不同之处就在于,它所有的元素共享同一内存单元,且分配给union的内存size 由类型最大的元素 size 来确定,如下的内存就为一个double 类型 size :
union uTest
{
int a; //sizeof(int) = 4
double b; //sizeof(double) = 8
char c; //sizeof(char) = 1
}x;
其他不同参考http://blog.csdn.net/firefly_2002/article/details/7954458
九、在机器级程序将控制与数据结合起来
2.对抗缓冲区溢出攻击:
1、栈随机化
为了在系统中插入攻击代码,攻击者不但要插入代码,还要插入指向这段代码的指针,这个指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。在过去,程序的栈地址非常容易预测,在不同的机器之间,栈的位置是相当固定的。
栈随机化的思想使得栈的位置在程序每次运行时都有变化。因此,即使许多机器都运行相同的代码。它们的栈地址都是不同的。
实现的方式是:程序开始时,在栈上分配一段0--n字节之间的随机大小空间。程序不使用这段空间,但是它会导致程序每次执行时后续的栈位置发生了变化。
在Linux系统中,栈随机化已经变成了标准行为。(在linux上每次运行相同的程序,其同一局部变量的地址都不相同)
2、栈破坏检测
在C语言中,没有可靠的方法来防止对数组的越界写,但是,我们能够在发生了越界写的时候,在没有造成任何有害结果之前,尝试检测到它。
最近的GCC版本在产生的代码中加入了一种栈保护者机制,用来检测缓冲区越界,其思想是在栈中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值。这个金丝雀值是在程序每次运行时随机产生的,因此,攻击者没有简单的办法知道它是什么。
在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者函数调用的某个操作改变了。如果是,那么程序异常终止。
3、限制可执行代码区域
限制那些能够存放可执行代码的存储器区域。在典型的程序中,只有保存编译器产生的代码的那部分存储器才需要是可执行的,其他部分可以被限制为只允许读和写。
现在的64位处理器的内存保护引入了”NX”(不执行)位。有了这个特性,栈可以被标记为可读和可写,但是不可执行,检查页是否可执行由硬件来完成,效率上没有损失。