汇编基础之三 -- 汇编指令
指令
详细的指令介绍:
常用的汇编指令有:
-
mov 指令,在内存中写入以一个数,将寄存器的值写入内存,将内存中的值写入寄存器。寄存器使用名称指定,内存使用一个内存地址编号指定。
指令 宽度 指定内存地址 指定值 MOV DWORD PTR DS:[地址编号], 值 // 值写入内存 MOV DWORD PTR DS:[0x1840FF93], EAX // eax寄存器中的值写入内存 MOV EAX DWORD PTR DS:[0x0012FF45] // 内存值写入EAX寄存器
内存地址可以有以下 的几种书写方式
PTR DS:[0x10ff93dd] // 直接指定地址 PTR DS:[reg] // reg为8个寄存器的名称, reg中储存的是一个内存地址 PTR DS:[reg + 立即数] // reg值 + 一个数字得到内存地址,实现地址偏移 PTR DS:[reg + reg*{1,2,4,8}] // 数组地址的快速寻址 PTR DS:[reg + reg*{1,2,4,8} + 立即数] // 利用地址的偏移,这是内存可以快速寻址原因
- MOVS指令:默认将ESI寄存器中储存的内存地址处的数据,拷贝到EDI寄存器中保存的内存地址上去,每次复制后,ESI和EDI中储存的地址值自动偏移复制的数据宽度值,可以向大地址和小地址偏移,由EFL标志寄存器上的DF 标志位决定。
将ESI储存的内存地址的值,复制到EDI寄存器中保存的内存地址上去。并且ESI和EDI的值会自动加1,也就是内存地址向后移动了一位。
MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]
ESI和EDI是可以省略的,默认都是从这两个寄存器中存放的内存地址拷贝,更具每次拷贝的数据宽度,有MOVSB, MOVSW, MOVSD三个快捷的指令,表示移动一个字节,两个字节和四个字节数据宽度。
- STOS指令:将AI,AX,EAX寄存器的值存储到EDI指定的内存单元中。LODS是STOS的逆过程
STOS BYTE PTR ES:[EDI] // byte对应AI,word对应AX,dword对应EAX STOS WOED PTR ES:[EDI] // AX => EDI 指定 的地址处 STOS DWORD PTR ES:[EDI] // EAX => EDI指定的地址处
- REP指令:按计数寄存器(ECX)中指定的重复次数执行指定的指令。
MOV ECX 12 // 指定重复次数 REP MOVSD // 重复18次,12是16进制,每次执行后,ECX-1 REP STOSD // d代表数据宽度为 dword
-
ADD DWORD EAX, EBX // 将EAX寄存器中的值与EBX中的值相加,结果保存到EAX中
AND DWORD EAX, PTR DS:[0x10ff93dd] // 将EAX寄存器中的值与内存地址为0x10ff93dd的值位于,结果存EAX中 - CPM:和SUB相同对两个值相减, SUB eax eax 会将计算结果存入eax,然后更改EFL标志寄存器中的ZF标志位,而CPM只会更具结果更改ZF标志位,不保存该结果。通常用于比较两个值是否相同。如果相同,则CMP(两数之差)结果为0,ZF标志位将会1,否则ZF位将位0,通过获取ZF为值得到是否相等。
- TEST:AND相同, 对两个值进行为与运算,但不保存结果,只会更改ZF标志位为0或1。
JCC指令组
上面使用CPM和TEST进行两值得比较,并通过标志位得结果,得到比较得结果,而JCC指令组也是同样得原理。
J是指jump。CC是条件,该条件与EFL标志寄存器中的标志位息息相关。通过获取EFL标志位的值,判断是否满足条件而是否执行JUMP操作。对应了高级语言中的比较运算。
中文含义 | 检查符号位 | 典型C应用 | |
---|---|---|---|
JZ/JE | 若为0则跳转;若相等则跳转 | ZF=1 | if (i == j);if (i == 0); |
JNZ/JNE | 若不为0则跳转;若不相等则跳转 | ZF=0 | if (i != j);if (i != 0); |
JS | 若为负则跳转 | SF=1 | if (i < 0); |
JNS | 若为正则跳转 | SF=0 | if (i > 0); |
JP/JPE | 若1出现次数为偶数则跳转 | PF=1 | (null) |
JNP/JPO | 若1出现次数为奇数则跳转 | PF=0 | (null) |
JO | 若溢出则跳转 | OF=1 | (null) |
JNO | 若无溢出则跳转 | OF=0 | (null) |
JC/JB/JNAE | 若进位则跳转;若低于则跳转;若不高于等于则跳转 | CF=1 | if (i < j); |
JNC/JNB/JAE | 若无进位则跳转;若不低于则跳转;若高于等于则跳转; | CF=0 | if (i >= j); |
JBE/JNA | 若低于等于则跳转;若不高于则跳转 | ZF=1或CF=1 | if (i <= j); |
JNBE/JA | 若不低于等于则跳转;若高于则跳转 | ZF=0或CF=0 | if (i > j); |
JL/JNGE | 若小于则跳转;若不大于等于则跳转 | SF != OF | if (si < sj); |
JNL/JGE | 若不小于则跳转;若大于等于则跳转; | SF = OF | if (si >= sj); |
JLE/JNG | 若小于等于则跳转;若不大于则跳转 | ZF != OF 或 ZF=1 | if (si <= sj); |
JNLE/JG | 若不小于等于则跳转;若大于则跳转 | SF=0F 且 ZF=0 | if(si>sj)ag |
堆栈指令
ESP:栈顶指针寄存器,记录当前使用的地址,栈的内存空间是存大地址开始使用的
我们可以使用MOV指令,将数据MOV到ESP寄存器指定的位置,然后使用SUB或者ADD指令,将ESP寄存器的值偏移对应的数据宽度值,实现数据的入栈和出栈操作。基于这样的原理,汇编中提供了PUSH和POP命令
MOV BYTE PTR DS:[ESP], 0xFF // 向栈中写入一个字节 SUB ESP, 1 // 栈顶指针偏移一字节 以上两个操作等同于一个push操作 PUSH 0xFF // 将0xFF写入到ESP保存的地址处,同时ESP偏移一个数据宽度 PUSH EAX // EAX中的值入栈 PUSH DWORD PTR DS:[地址] // 该地址处的值入栈 出栈同理 POP DWORD EAX // 将栈顶的元素取出放入EAX寄存器中保存,同时ESP偏移一个DWORD数据宽度
-
JMP:将程序跳转到指定的地址执行,通过改变EIP中的值实现
-
CALL:将程序跳转到指定的地址,跳转前将下一次执行的地址保存到栈中,用来记录跳转前的位置。然后改变EIP中的值使得cpu去执行其他地址的指令。当执行结束需要返回原来的地址时,从栈中取出跳转签到的地址,赋值到EIP寄存器中即可回到跳转前的状态。
-
RETN:从栈中取出保存的地址,赋值给EIP,用作下次执行
使用这两个指令需要理解EIP寄存器的作用。当一个程序被编译完成之后,程序的执行方法即已经确定,即A=>B=>C的顺序进行执行,且他们使用的内存空间说连续的,计算机执行一行指令后,下一次要执行的内容都保存在EIP寄存器保存的地址处。所以EIP寄存器指向的内容,就是程序下一次执行的指令。也就是说,A执行时,EIP寄存器中保存的是B的地址,这样执行A后将会获取B的信息并执行。如果A时一个JMP或者CALL指令,执行依次JMP EIP D
,这条指令表示将EIP中的值改为D的地址,所以下一次执行的即为D指令。由此实现了跳转。这样的方式当程序跳转到D指令后,程序无法返回B指令了,因为我们找不到B的地址,想要重新回到B指令,需要将B的地址保存,使用时取回即可,通常是将其入栈。这样就和CALL指令的使用方式相同了。
JMP EIP, 地址 // 修改EIP寄存器中的值。 CALL 地址 // 跳转到指定地址,执行该地址的指令,跳转前,将下一行地址入栈,
CALL指令的详细使用在下一章函数中讲解,同时会涉及到堆栈提升与堆栈平衡。