第五周信息安全系统设计基础学习总结 20135306 黄韧

第三章 程序的机器级表示

3.1  历史观点

  1. Intel处理器系列俗称x86,开始时是第一代单芯片、16位微处理器之一,第一代是8086,也是汇编课程中学习的处理器型号。x86架构于1978年推出的Intel 8086中央处理器中首度出现,它是从Intel 8008处理器中发展而来的,而8008则是发展自Intel 4004的。8086在三年后为IBM PC所选用,之后x86便成为了个人计算机的标准平台,成为了历来最成功的CPU架构。

2.每个后继处理器的设计都是后向兼容的——较早版本上编译的代码可以在较新的处理器上运行

每个后继处理器的设计都是后向兼容的,可以保证较早版本上编译的代码在较新的处理器上运行。

3.X86 寻址方式经历三代:

     DOS时代的平坦模式,不区分用户空间和内核空间,很不安全

     8086的分段模式

     IA32的带保护模式的平坦模式

3.2 程序编码

几个处理器:

  • 程序计数器(CS:IP)
  • 整数寄存器(AX,BX,CX,DX)
  • 条件码寄存器(OF,SF,ZF,AF,PF,CF)
  • 浮点寄存器

 机器级代码

计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。

①、指令集体系结构(Instruction set architecture ISA)

它定义了处理器状态、指令的格式,以及每条指令对状态的影响。

IA32将程序的行为描述成好像每条指令时按顺序执行的,一条指令结束后,下一条再开始。(实际上处理器并发地执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行完全一致)

②、机器级程序使用的存储器地址是虚拟地址

提供的存储器模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。

③、程序存储器(program memory)包含:程序的可执行机器代码、操作系统需要的一些信息、栈、堆。程序存储器用虚拟地址来寻址(此虚拟地址不是机器级虚拟地址)。操作系统负责管理虚拟地址空间(程序级虚拟地址),将虚拟地址翻译成实际处理器存储器中的物理地址(机器级虚拟地址)。

机器代码和它的反汇编表示的一些特性:

  • IA32指令长度从1到15个字节不等。
  • 设计指令格式的方式是,从某个给定位置开始,可以将字节唯一的解码成机器指令。
  • 反汇编器只是基于机器代码文件中的字节序列来确定汇编代码,不需要访问程序的源代码或汇编代码。
  • 反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有些差别。

 

3.3数据格式

数据格式:由于是从16位体系结构扩展成32位,intel用术语字(word)表示16位数据类型,因此32位为双字(double words),64位数为4字(quad words)

1.Intel中:

8 位:字节

16位:字

32位:双字

64位:四字

2.c语言基本数据类型对应的IA32表示

char    字节    1字节

short   字      2字节

int            双字    4字节

long int 双字 4字节

long long int (不支持) 4字节

char *  双字 4字节

float   单精度 4字节

double  双精度 8字节

long double 扩展精度 10/12字节

3.数据传送指令的三个变种:

  • movb 传送字节
  • movw 传送字
  • movl 传送双字

3.4 访问信息

与其他等级的编程语言一样,汇编语言能够用许多方式来访问变量。变量有三种基本的存储方式。

1. 全局变量/静态变量- 在程序数据区(program data section)分配

2. 局部变量/参数- 在栈上分配

3. 堆变量- 在堆上分配

 a.全局,静态变量

全局变量存储在一个固定的地址上(至少对于程序来说,他们是固定的)。访问这些变量的最通常的方式是在指令中明确指出那个固定的地址。

MOV    EAX,[1234134H]        ; loads EAX with value stored at location 12341234H

INC      DWORD PTR TEST2!_nCount ; increments DWORD variable nCount

注意,在symbolic信息可用的时候,debugger会去使用它。

 b.局部变量,参数

局部变量和参数存在于栈上,并且是通过EBP(有时候是ESP)来访问的。优化过的代码通常会清除掉对栈基指针(frame pointer)的依赖,在这样的情况下ESP寄存器被用来访问局部变量,而EBP可以被用来做一个额外的通用寄存器来使用。当你使用一个标准栈基指针的时候,指令看起来应该是这样的。

MOV   EAX,[EBP+8]   ; load EAX with argument

MOV   EAX,[EBP-4]    ; load EAX with local variable

 c.堆变量

堆变量存在于堆上,他们是通过指针来访问的。典型情况下需要不只一条指令来访问堆变量。

MOV   ESI, TEST2!_m_pFileList     ; load the pointer

MOV   EAX, [Esi+4]                         ; read second DWORD (pszName) in heap

另一个需要注意的是,大多数编译器会将经常访问的变量放到寄存器中,以便于提高访问速度。尤其是精简指令计算机。

 执行流控制

控制流命令要不就是有条件的(条件满足的时候),要不就是无条件的。这些语句支持函数调用,if-then-else,switch case等高级的语言成分。

无条件跳转指令

1. JMP命令

    这个命令简单的设置EIP寄存器为下一条指令的地址。没有任何数据会被存储到栈上,并且不会设置任何标志位。JMP被用在固定的指令分支上。大多数的if-then-else语句族至少需要一条JMP指令。

2. CALL命令

    这条指令先存储EIP的值到栈上,然后设置EIP为下一条指令的地址。将EIP压栈允许程序在结束了函数调用之后,回来继续执行CALL语句后面的语句。

对于JMP和CALL指令来说,操作数可以是固定的地址,寄存器的值,或者一个指向分支地址的指针。

3. RET命令

    RET指令将当前栈上的值赋给EIP寄存器。该命令用来为传递给栈的参数修复栈指针。

4. INT命令

    当INT命令的操作数是一个中断号的时候,该指令会引发一个软件中断。这个与CALL指令差不多,不同之处是EFLAGS寄存器被压入栈中。还有,如果是在user mode中被调用,在切换到kernel mode时也会发生将EFLAG寄存器压栈的操作。中断函数结束的时候,随着RETI指令的执行,EFLAGS寄存器和EIP都会从栈中恢复。

 

条件跳转指令

1, LOOP Adress

    LOOP指令被用来实现高级语言中的循环。直到ECX(计数器)的值为0的时候,它才会走向分支地址。如果ECX不是0,那么ECX会被减一,然后继续循环操作。

XOR     EAX,EAX    ; clear EAX register

MOV    ECX, 5        ; load loop count

START:

ADD     EAX,1         ; add one to eax

LOOP  START

2. JNX,JE等等

    根据条件来跳转的指令会去判断所指定的条件是否为真,若果是就执行跳转。比如,JNZ(jump not zero),操作数中指定的地址直到ZERO标志位被设置为1的时候才会被转过去。这些指令主要被用在if语句块中。

XOR    EAX,EAX   ; clear eax

MOV    ECX,5

START:

ADD    EAX,1       ; add one to EAX

DEC    ECX          ; decrement loop counter

JNZ     START

 

操作数的三种类型

  • 立即数
  • 寄存器
  • 存储器

 

寻址方式:

1.立即数寻址方式

2.寄存器寻址方式

3.存储器寻址方式

  • 直接寻址方式
  • 寄存器间接寻址方式
  • 寄存器相对寻址方式
  • 基址变址寻址方式
  • 相对基址变址寻址方式

数据传送指令

mov类指令:将源操作数的值复制到目的操作数中。源操作数指定的值是一个立即数,存储在寄存器中或存储器中。目的操作数制指定一个位置,要么是一个寄存器,要么是一个存储器。

  • movb 传送字节
  • movw 传送字
  • movl 传送双字
  • movs 符号位扩展
  • movz 零扩展

push:把数据压入栈中

pop:删除数据

  • 后进先出
  • 栈指针指向栈顶元素
  • 栈朝低地址方向增长

数据传送示例

  • c语言中的指针其实就是地址,间接引用指针就是将该指针放在一个寄存器中,然后在存储器引用中使用这个寄存器
  • 局部变量通常保存在寄存器中,而不是存储器

3.5 算术和逻辑操作

加载有效地址

加载有效地址指令leal实际上就是movl指令的变形。它的指令形式是从存储器读取数据到寄存器,但实际根本没引用存储器。

一元操作和二元操作

一元操作:只有一个操作数,既是源又是目的,可以是一个寄存器,或者存储器位置。

二元操作:源操作数 目的操作数

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

移位操作

  • SAL 算术左移
  • SHL 逻辑左移
  • SAR 算术右移(补符号位)
  • SHR 逻辑右移(补0)

 

3.6 控制

程序不可能一顺到底的执行,需要有一些分支流程控制的语法,对高级语言来讲,有分支循环等,对于汇编,有一个“跳”,或者选择性跳,跳转指令本身非常简单,仅仅一个jmp指令,类似于c语言的goto,语法为:

label:

       ...

       jmp label

跳转分为段跳转(小于128字节),远跳转(分段模式下跨段跳转),近跳转(其他),不过这些在AT&T里编译器会根据参数的 变化而选择性的生成机器码,但对于MASM,需要自己指定,jmp near ptr label, jmp far ptr label。

但本质上讲,倘若只有这样的jmp,那不论如何跳都将是个死循环,所以便有了条件跳转(Jcond),在一定条件下进行跳转,这里所谓的条件,仍然是eflags的不同标记位

 

条件码

  • CF:进位标志
  • ZF:零标志
  • SF:符号标志
  • OF:溢出标志 

访问条件码

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

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

执行比较指令,根据计算t=a-b设置条件码。

跳转指令及其编码

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

跳转指令有几种不同的编码,最常用的是PC(程序计数器)相关的。

jump分为:

  • 直接跳转:后面跟标号作为跳转目标
  • 间接跳转:*后面跟一个操作数指示符

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

翻译条件分支

将条件表达式和语句从c语言翻译成机器语言,最常用的方式就是结合有条件和无条件跳转

循环

汇编中没有do-while、while和for相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。大多数汇编器中都要先将其他形式的循环转换成do-while格式。

1.do-while循环

通用形式:

do

        body-statement

        while(test-expr);

循环体body-statement至少执行一次。

可以翻译成如下条件和goto语句:

loop:

        body-statement

        t = test-expr;

        if(t)

               goto loop;

每次循环,程序会执行循环体的语句,然后执行测试表达式。

2.while循环

通用形式:

while (test-expr)

        body-statement

GCC的方法是使用条件分支,在需要时省略循环体的第一次执行:

if(!test-expr)

        goto done;

do

               body-statement

               while(test-expr);

done:

接下来,这个代码可以直接翻译成goto代码:

t = test-expr;

if(!t)

        goto done:

loop:

        body-statement

        t = test-expr;

        if(t)

               goto loop;

done:

3.for循环

通用形式:

for(init-expr;test-expr;update-expr)
body-satament

do-while形式:

init-expr;
if(!test-expr)
goto done;
do{
  body-statement
  update-expr;
  }while(test-expr);
done:

翻译成goto代码:
init-expr;
t=test-expr;
if(!t)
   goto done;
loop:
   body-statement
update-expr;
t= test-expr;
if(t)
   goto-loop;
done:


条件传送指令

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

数据的条件转移是一种替代的策略。此方法先计算一个条件操作的两种结果,然后再根据条件是否满足从而选取一个。

只有在一些受限制的情况下,这种策略才可行,但如果可行,就可以用一条简单的条件传送指令来实现它。

条件传送指令更好地匹配了现代处理器的性能特性。

 

3.7 过程

过程可以理解为c中的函数,当调用者(caller)调用被调用者(be caller)的时候,系统会为被调用者在栈内分配空间,这个空间就称为栈帧。栈的结构大概如下:

 

  程序栈是向低地址生长的栈,与数据结构当中的栈结构类似,有后进先出的性质,寄存器%esp(stack pointer)保存栈顶指针的地址,寄存器%ebp(** pointer)保存帧指针的地址。 程序执行的时候,栈指针可以移动,以便增大或者缩小程序栈的空间,而帧指针是固定的,因为大多数程序栈中存储的数据都是相对于帧指针的(帧指针+偏移量)。

 

栈顶元素的地址是所有栈中元素地址中最低的

栈用来传递参数、存储返回信息、保存寄存器,以及本地存储。

栈帧:

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

最顶端的栈帧以两个指针界定:

  • 寄存器%ebp-帧指针
  • 寄存器%esp-栈指针

 

call

CALL指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。

返回地址是还在程序中紧跟在call后面的那条指令的地址。

ret

ret指从栈中弹出地址,并跳转到这个位置。

leave

这个指令使栈做好返回的准备

 寄存器使用惯例

程序寄存器组是唯一能被所有过程共享的资源。

 

 

 

 

posted @ 2015-10-11 18:02  黄伯伯  阅读(324)  评论(1编辑  收藏  举报