指令标记法
Prefix Instruction [operand 1], [operand 2] ; comment
注意,不能让内存同时作为目的操作数和源操作数。
操作数
一般指令的操作数可以有0~2个。
隐式操作数- Implicit Operands
隐式操作数是由指令本身指定的。CLI和STI就是这样的不需要指出操作数的指令。CLI和STI指令设置和清除在EFlags寄存器中的中断标志位。CLI和STI永远只操作那个bit位。
寄存器寻址- Register Operands
也就是指令使用寄存器中的值作为源或目的操作数。
CALL EDI ; EDI contains address to call
MOV EAX, EBX ; EAX<-EBX EAX = destination EBX = source
INC EAX ; EAX=EAX+1 – EAX=source and destination
立即数- Immediate Operands
立即数是指令的一部分,直接指定了操作数的值。立即数允许用户设置一个常量到变量中,或者引用一个固定的内存地址。
MOV EAX,177 ; load EAX with 177
MOV CL,0FFH ; load CL with 255
CALL 12341234H ; call routine at location 12341234H
IO操作数- I/O Operands
IO操作数不在用户态模式程序中使用。IO指令被用来引用IO map中的设备。这些包括绝大多数的硬件设备比如PICS,串行口,并行口,磁盘控制器等等。
IN AL, 04H ; read port location 4 into AL register
OUT 04H, AL ; write AL register to port location 4
内存引用操作数
内存引用操作数是指通过许多种内存寻址方式来获取操作数的值的方式。
1. 绝对寻址- Direct Addressing
指的是地址本身用作操作数。这被用来指出程序占用的固定地址,比如说实例的全局变量。
MOV AL, [12341234H]
INC DWORD PTR [12341234H]
2. 间接寻址- Based Addressing
指使用寄存器来存放地址来作为操作数的方式。这经常被用来解析指针所指向的变量。
MOV AL,[ECX]
DEC DWORD PTR [ESI]
3. 带有偏移量的间接寻址- Base Plus Displacement Addressing
跟间接寻址差不多,不同之处就是要在寄存器的地址上加上一个额外的偏移量。这种方法用来访问一个结构之中的变量。寄存器中的指针指向这个结构的起始地址,位移能够允许引用到正确的结构元素。这种方式比不得不用指针直接指向每一个要被访问的元素好,因为经常出现一段代码中访问一个结构体的多个元素的情况。这样,寄存器中的指针值只需设置一次,只有偏移量改改就好了。
LEA EDX,[ESP+0x4]
MOV ECX, [EBP-0x10]
4. 基址+偏移量寻址- Index Plus Displacement Addressing
这种寻址方式与带偏移量的间接寻址一样,区别就是寄存器与固定的偏移量的角色互换了。现在基址是固定的位置,加上一个在寄存器中的偏移量。这种方式主要用于访问静态内存中的数组中的元素。
MOV EAX,1234124H[ESI]
5. 变址寻址- Base Plus Displacement Plus Index Addressing
这是由多种寻址方式混合成的寻址方式。它综合了前面的两种方式。用来访问声明在栈上或者动态分配在堆上的地址。寄存器指向基址,另一个寄存器保存偏移量,还有一个固定的偏移量可以被包含进来。
MOV EAX,[EBP+8][ESI]
INC WORD PTR [EBX+EAX*2]
过程调用的进入和退出
在进入和离开一个程序的时候,代表性的栈框架(stack frame)会被建立,从而使程序能够方便的访问参数和局部变量。Intel处理器使用EBP和ESP两个寄存器来完成这项工作。下面的信息就show出了调用子函数的时候,一个栈框架是如何建立的例子。
过程调用部分
MOV EAX, Argument 1 ; load EAX with value of Argument 1
PUSH EAX ; push first argument onto stack
CALL sub1
在调用函数之前,参数被压栈。分解PUSH指令,它做了下面的事情:
ESP <- ESP – 4 ; decrement stack pointer
SS:[ESP] <- operand ; load operand into location pointed to by ESP
分解CALL指令,下面是call的指令:
PUSH EIP ; push return address on stack
EIP <- destination ; EIP set to first instruction on the sub1 routine
这时栈看起来应该是这样子的:
???? |
||
EIP(也就是返回值) |
<- |
ESP |
Argument 1 |
||
???? |
过程入口部分
典型的过程入口看起来应该是这样子的:
PUSH EBP ; save the base pointer
MOV EBP,ESP ; setup new stack frame
SUB ESP,24 ; allocate local variables
发生了三件事,第一,我们保存了EBP寄存器的值;多数的编译器假设EBP寄存器不会被调用的过程破坏。第二,我们建立了栈框架,工作拷贝ESP到EBP寄存器,我们为过程做到了这一点。第三,我们为局部变量创造了空间,这些空间是分配在栈上的。现在栈看上去应该是这样的:
???? |
||
Local Var n |
<- |
ESP |
Local Var 1 |
||
Saved EBP |
<- |
EBP |
EIP(返回值) |
||
Argument 1 |
||
???? |
访问参数和局部变量
要访问参数,可以在过程的任何地方使用如下的指令:
MOV EAX,[EBP+8] ; read argument one into EAX register
要访问局部变量,使用如下的指令即可:
MOV EAX,[EBP-4] ; read first local variable
过程退出
典型的退出部分看起来像下面这样:
MOV ESP,EBP ; restore stack pointer
POP EBP ; restore base pointer
RET 4 ; return
退出一个过程先要恢复ESP,恢复的过程是把它设置成为与EBP的值相同即可。然后恢复EBP到它刚刚进入这个过程调用时的样子,该值被早些时候压入了栈中。最后的指令是RET,分析RET指令如下:
EIP <- POP() ; reset our instruction pointer
ESP <- ESP + count ; fixup stack for arguments