第三章 程序的机器级表示
3.1 历史观点
2.每个后继处理器的设计都是后向兼容的——较早版本上编译的代码可以在较新的处理器上运行
每个后继处理器的设计都是后向兼容的,可以保证较早版本上编译的代码在较新的处理器上运行。
3.X86 寻址方式经历三代:
DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
8086的分段模式
IA32的带保护模式的平坦模式
3.2 程序编码
几个处理器:
机器级代码
计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。
①、指令集体系结构(Instruction set architecture ISA)
它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
IA32将程序的行为描述成好像每条指令时按顺序执行的,一条指令结束后,下一条再开始。(实际上处理器并发地执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行完全一致)
②、机器级程序使用的存储器地址是虚拟地址
提供的存储器模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。
③、程序存储器(program memory)包含:程序的可执行机器代码、操作系统需要的一些信息、栈、堆。程序存储器用虚拟地址来寻址(此虚拟地址不是机器级虚拟地址)。操作系统负责管理虚拟地址空间(程序级虚拟地址),将虚拟地址翻译成实际处理器存储器中的物理地址(机器级虚拟地址)。
机器代码和它的反汇编表示的一些特性:
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.数据传送指令的三个变种:
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类指令:将源操作数的值复制到目的操作数中。源操作数指定的值是一个立即数,存储在寄存器中或存储器中。目的操作数制指定一个位置,要么是一个寄存器,要么是一个存储器。
push:把数据压入栈中
pop:删除数据
数据传送示例
3.5 算术和逻辑操作
加载有效地址
加载有效地址指令leal实际上就是movl指令的变形。它的指令形式是从存储器读取数据到寄存器,但实际根本没引用存储器。
一元操作和二元操作
一元操作:只有一个操作数,既是源又是目的,可以是一个寄存器,或者存储器位置。
二元操作:源操作数 目的操作数
移位操作
3.6 控制
程序不可能一顺到底的执行,需要有一些分支流程控制的语法,对高级语言来讲,有分支循环等,对于汇编,有一个“跳”,或者选择性跳,跳转指令本身非常简单,仅仅一个jmp指令,类似于c语言的goto,语法为:
label:
...
jmp label
跳转分为段跳转(小于128字节),远跳转(分段模式下跨段跳转),近跳转(其他),不过这些在AT&T里编译器会根据参数的 变化而选择性的生成机器码,但对于MASM,需要自己指定,jmp near ptr label, jmp far ptr label。
但本质上讲,倘若只有这样的jmp,那不论如何跳都将是个死循环,所以便有了条件跳转(Jcond),在一定条件下进行跳转,这里所谓的条件,仍然是eflags的不同标记位
条件码
访问条件码
条件码不会直接读取,常用的使用方法有三种:
执行比较指令,根据计算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)保存帧指针的地址。 程序执行的时候,栈指针可以移动,以便增大或者缩小程序栈的空间,而帧指针是固定的,因为大多数程序栈中存储的数据都是相对于帧指针的(帧指针+偏移量)。
栈顶元素的地址是所有栈中元素地址中最低的
栈用来传递参数、存储返回信息、保存寄存器,以及本地存储。
栈帧:
为单个过程分配的那部分栈称为栈帧。
最顶端的栈帧以两个指针界定:
call
CALL指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。
返回地址是还在程序中紧跟在call后面的那条指令的地址。
ret
ret指从栈中弹出地址,并跳转到这个位置。
leave
这个指令使栈做好返回的准备
寄存器使用惯例
程序寄存器组是唯一能被所有过程共享的资源。