学习计时:共15小时

读书:7

代码:1

作业:3

博客:4

 

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

计算机执行机器代码,用字节序列编码低级的操作,包括处理数据、管理存储器、读写存储设备上的数据,以及利用网络通信。

逆向工程:通过研究系统和逆向工作,来试图了解系统的创建过程。

Intel IA32:当今大多数计算机的主导语言。

x86-64:是Intel IA32在64位机器上运行的扩展。

 

3.1 历史观点

Intel处理器系列俗称X86,开始时他是第一代单芯片、16位微处理器之一;后面随着发展能够利用进步的技术满足更高兴能和支持更高级操作系统的需求。

8086是第一代单芯片、16位微处理器之一。

i386:体系结构拓展到了32位,增加了平坦寻址模式。这是Intel系列中第一台支持Unix操作系统的机器。

摩尔定律:晶体管的数量每26个月就会翻一番。

Linux使用平坦寻址方式,将整个储存空间看做一个大的字节数组。

 

3.2 程序编码

命令gcc指的是GCC C编译器。

编译选项-01 告诉编译器使用第一级优化。

通常提高优化级别会使最终程序运行更快,但编译时间可能会变长,用调试工具对代码进行调试会更困难。

第二级优化(选项 -02指定)被认为是较好的选择

3.2.1 机器级代码

两种抽象:

     1.指令集体系结构:定义了处理器状态、指令的格式,以及每条指令对状态的影响。

     2.虚拟地址:存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。

汇编代码表示非常接近于机器代码,用可读性更好的文本格式来表示。

可见的处理器状态:程序计数器(PC),整数寄存器文件,条件码寄存器,浮点寄存器。

汇编代码不区分有符号或无符号整数,不区分各种类型的指针,甚至不区分指针和整数。

程序存储器包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,用户分配的存储器块。

操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器存储器中的物理地址。

3.2.2 代码示例

机器实际执行的程序是对一系列指令进行编码的字节序列,对产生这些指令的源代码一无所知。

要查看目标代码文件的内容,最有价值的是反汇编器,这些程序根据目标代码产生一种类似于汇编代码的格式。

反汇编器只是基于机器代码文件中的字节序列来确定汇编代码。他不需要访问程序的源代码或者汇编代码。

链接器将代码的地址移到一段不同的地址范围中。

链接器确定了存储全局变量accum的地址。

3.2.3 关于格式的注解

所有以“ . ”开头的行都是指导汇编器和链接器的命令。

 

3.3 数据格式

Intel术语:16位 —— 字 ; 32位 —— 双字 ; 64位 —— 四字。

大多数指令都是对字节或者双字操作的。大多数常用数据类型都是以双字形式储存的。

大多数GCC生成的汇编代码指令都有一个字符后缀,表明操作数的大小。

数据传送指令三个变种:movb(传送字节),movw(传送字),movl(传送双字)。

 

3.4 访问信息

一个CPU包含一组8个存储32位值得寄存器。这些寄存器用于储存整数数据以及指针。

3.4.1 操作数指令符

大多数指令有一个或多个操作数,指示出执行一个操作中要引用的源数据值,以及放置结果的目标位置。

源数据值可以以常数形式给出,或是从寄存器或存储器给出。

不同操作数的可能性分为3种类型:立即数$,寄存器,存储器

3.4.2 数据传送指令

把许多不同的指令分为指令类。

MOV类中的指令将元操作数的值复制到目的操作数中。

源操作数指定的值是一个立即数,存储在寄存器中或者存储器中。

MOVS和MOVZ指令将一个较小的源数据复制到一个较大的数据位置,高位用符号位扩展或零扩展进行填充。

用符号位扩展,目的位置的所有高位用源值的最高位数值进行填充。用零扩展,所有高位都用零填充。

栈是一个数据结构,可以添加或者删除值,不过要遵循“后进先出”的原则。通过Push操作把数据压入到栈中,通过pop操作删除数据。

pushl指令的功能是把数据压入到栈上,而popl指令是弹出数据。这些指令都只有一个操作数——压入的数据源和弹出的数据目的。

3.4.3 数据传送示例

间接引用指针就是将该指针放在一个寄存器中,然后在存储器引用中使用这个寄存器。

局部变量通常保存在寄存器中,寄存器访问比存储器访问要快得多。

 

3.5 算术和逻辑操作

给出的每个指令类都有对字节、字和双字数据进行操作的指令。这些操作被分为:加载有效地址、一元操作、二元操作、移位。

3.5.1 加载有效地址

加载有效地址指令leal实际上是movl指令的变形。

Leal的指令形式是从存储器读数据到寄存器。

目的操作数必须是一个寄存器。

3.5.2 一元操作和二元操作

一元操作有一个操作数,既是源又是目的。

二元操作有两个操作数,第二个操作数既是源又是目的。

第一个操作数可以是立即数、寄存器或者存储器位置。第二个操作数可以是寄存器或者存储器位置。但是两个操作数不能同时是存储器的位置。

3.5.3 移位操作

先给出移位量,再给出要移位的数值。他可以进行算数和逻辑右移。

左移:SAL.SHL效果一样

右移:SAR算术,SHR逻辑

3.5.4 讨论

通常,编译器产生代码中,会用一个寄存器存放多个程序值,还会在寄存器之间传送程序值。

3.5.5 特殊的算术操作

imull指令称为双操作数乘法指令,从两个32位操作数产生一个32位乘积。

无符号除法使用divl指令,通常事先将寄存器%edx设置为0。

 

3.6 控制

测试数据值,然后根据测试的结果来改变控制流或者数据流。

3.6.1 条件码

单个位的条件码寄存器

最常用条件码:CF,ZF,SF,OF

CF:进位标志。检测无符号溢出。

ZF:零标志。最近操作结果为0。

SF:符号标志。最近操作结果为负数。

OF:溢出标志。最近操作有补码溢出——正/负溢出。

Leal进行地址运算所以不改变条件码。

3.6.2 访问条件码

条件码通常不会直接读取,常用三种方法:

  1. 根据条件码的某个组合,将一个字节设置为0或者1
  2. 条件跳转到程序的某个其他部分
  3. 有条件的传送数据

3.6.3  跳转指令及其编码

跳转指令会导致执行切换到程序中一个全新的位置。

在汇编代码中,这些跳转的目的地通常用一个标号指明。

在产生目标代码文件时,汇编器会确定所有带标号指令的地址,并将跳转目标(目的指令的地址)编码为跳转指令的一部分。

jmp指令是无条件跳转,他可以直接跳转也可以间接跳转。

当执行PC有关寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。

3.6.4  翻译条件分支

有条件和无条件的跳转。

Goto语句,无条件跳转。使代码难以阅读和调试。

3.6.5  循环

  1. do-while
  2. while
  3. for

3.6.6  条件传送代码

实现条件操作的传统方式是利用控制的条件转移。

数据的条件转移是一种替代策略。

3.6.7  switch语句

switch(开关)语句可以根据一个整数索引值进行多重分支。

执行switch语句的关键步骤是通过跳转表来访问代码位置。

 

3.7     过程

一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。

3.7.1 栈帧结构

机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复、本地存储。

为单个过程分配的那部分栈成为栈帧。

寄存器%ebp为帧指针

寄存器%esp为栈指针

3.7.2 转移控制

Call指令有一个目标:指明被调用过程起始的指令地址。

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 数据对齐

对齐限制简化了形成处理器和存储器系统之间接口的硬件设计。

任何int型的对象或者指针的地址最低两位都必须是0。

 

3.10综合:理解指针

指针是C语言的一个重要特征,他们以一种统一的方式,对不同数据结构中的元素产生引用。

每个指针都对应一个类型。

每个指针都有一个值。

指针用&运算符创建。

运算符*用于指针的间接引用。

数组与指针紧密联系。

将指针从一种类型强制转换成另一种类型,只改变它的类型,不改变它的值。

指针也可以指向函数。

 

3.11  应用:使用GDB调试器

GDB:可以观察正在运行的程序,同时又对程序的执行有相当的控制。

 

3.12  存储器的越界引用和缓存区溢出

C对于数组引用不进行任何边界检查,而且局部变量和状态信息都存放在栈中。

常见的状态破坏:缓存区溢出。

解决:使用fgets函数,它包括一个参数,限制待读入的最大字节数。

让程序执行它本来不愿意执行的函数——一种常见的通过计算机网络攻击系统安全的方法。

两种攻击形式:

  1.使系统启用一个外壳程序,给攻击者提供一组操作系统函数;

  2.执行一些未授权的任务,修复对栈的破坏,第二次执行ret指令,正常返回给调用者。

对抗缓存区溢出攻击

  1. 栈随机化:防止安全单一化
  2. 栈破坏检测:加入一种栈保护者
  3. 现在可执行代码区域

 

3.13   x86-64:将IA32扩展到64

数据类型:指针需要8个字节,long将整数变成64位。

访问信息:寄存器16个,所以寄存器64位长,可以直接访问低32位。

算术指令:在四字上进行运算的指令,后缀加q。

控制:新增指令cmpq和testq,用于比较和测试四字。

保存惯例:有些用来保存临时值的寄存器被指定为调用者保存,另外一些是被调用者保存。

数据结构:数组作为同样大小的块的序列来分配,结构作为最长的块来分配,联合作为一个单独的块来分配。对任何K字节的标量数据类型来说,它的起始地址必须是K的倍数。

 

3.14    浮点程序的机器级表示

浮点体系结构:存储模型,指令和传递规则的组合。

 

实验练习

输入的代码

 

 

遇到的问题:

 

 

由于没学汇编,所以对这章有关汇编的内容理解起来相当吃力,但是对C语言部分有了更深的认识。

posted on 2015-10-08 20:29  20135231  阅读(245)  评论(1编辑  收藏  举报