
本章开始讨论汇编语言.前面已经说过汇编语言是介于机器语言和高级语言之间的一种语言.在计算机刚刚诞生的时候,人们编程使用的就是机器语言,由于机器语言只由0和1组成,因此极其难以编写和修改,例如高级语言中的一句int a=1,它的机器码可能是这样的:

写为16进制是:

除非你是电脑,不然你是不会想看这种代码的.
后来人们想了个办法,制订一套所谓的助记符系统,用助记符来代表某个机器指令,例如mov代表数据传送指令,于是上面的代码可以改写为:mov a,1,这就是一条简单的汇编语言指令.
当然实际上并没有这么简单,因为CPU并不认识a是什么东西.CPU能认识的只有内存地址和寄存器.寄存器是CPU内部一组用于临时存放数据的地方,由于寄存器就在CPU内部,因此访问寄存器比访问内存地址速度更快.现在的64位CPU拥有17个基本寄存器,分别是RAX,RBX,RCX,RDX,RSP,RBP,RSI,RDI,RIP,R8-R15.它们中的每个最多能存储64位也就是8个字节的数据.但对于32位程序而言,只需要使用其中的一半也就是32位就可以了,并且R8-R15也不需要使用.这样32位程序使用的寄存器就是:EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI,EIP.
在Ollydbg里面可以直接观察到程序当前使用的寄存器的情况:

其中EAX,EBX,ECX,EDX寄存器的低4位和低2位都可以单独使用,例如EAX的低4位叫做AX,AX的高2位为AH,低2位为AL,当EAX=0x12345678时,AX=0x5678,AH=0x56,AL=0x78.
理论上这些寄存器可以用于存放任意的数据,但EIP例外,EIP寄存器的全称为Extend Instruction Pointer,即扩展指令指针,它的值总是程序下一条要执行的指令的地址,因此该寄存器的值不能随意更改,一般程序也不可能用该寄存器当作临时的数据存放处.其他寄存器有时也有特殊的用途,之后会详细说明.
除了以上的通用寄存器外,CPU还有一组标志寄存器,用于记录最近一条指令执行后产生的结果的状态:

该标志有很多,但目前需要了解的有:
Z:Zero Flags,0标志,当最近一条指令执行后的结果为0时该标志为1,否则为0
S:Sign Flags,符号标志,当最近一条指令执行后结果为负时该标志为1,否则为0.
C:Carry Flags,进位或借位标志,当最近一条指令执行后结果超出32位无符号数可以表示的最大整数时被设置为1,或者发生小数减大数(无符号数)时被设置为1,否则为0.这是什么意思呢?32位寄存器可以存放的最大的无符号数为0xFFFFFFFF,也就是10进制的无符号数4294967295,这时如果把它+1,则会变成0x100000000,超过了32个位,此时CF位就会被设为1.
O: Over Flags,溢出标志.当最近一条指令执行后结果超出32位有符号数可以表示的最大整数时被设为1.判断运算结果是否溢出有一个简单的规则:当两个相同符号数相加,而运算结果的符号与原数据符号相反时,产生溢出.例如0x7FFFFFFF,其最高位为0,是正数,0x7FFFFFFF+1=0x80000000,0x80000000的最高位为1,是负数,此时就发生了溢出.


在Ollydbg中可以通过鼠标点击这些标志位使其改变.实际上这些标志位都是一个称为EFLAGS寄存器的不同的位,而不是独立存在的.

再来说说内存地址.常见的内存地址有两种,一种是绝对地址,一种是相对地址.
绝对地址的意思是,在指令中直接使用内存地址的值来进行寻址,例如:

这一条指令的意思是,把1赋值给内存地址0x00F551A1,长度为1个字节.在Ollydbg的提示窗口里面可以看到地址0x00F551A1的值:

在上面右键弹出菜单,选择在数据窗口中跟随地址,就可以在内存窗口中看到地址0x00F551A1:

执行完这一条指令后,0x00F551A1就变为了1:

相对地址的意思是,将某个地址作为基准,将数据传送给这个基准地址加若干个字节或减若干个字节的地址中.在汇编中常见的用法是把某个内存地址取到寄存器中,然后寄存器加或减若干个字节作为目标地址,例如:

这句话的意思是:把EDX寄存器的值作为一个内存地址,把这个内存地址+8个字节之后的内存地址中的值赋值给EAX,长度为1个DWORD.
在寄存器窗口中可以看见EDX现在的值:

Ollydbg的提示窗口里面可以看到EDX+0x8=0x00F51CDB,里面的数据是0x0D3BFFFF,于是可以知道,这条指令执行完后,EAX的值是0x0D3BFFFF.

如果用Ollydbg打开另外一个程序,会发现这个程序也存在0x00F51CDB这个地址,这是怎么回事呢?难道两个程序共用一个内存地址吗?这当然是不可能的,实际上在Ollydbg中看见的内存地址都是虚拟地址(Virtual Address)而不是物理地址(Physical Address).在Windows系统中每个32位进程都拥有从0x00000000-0xFFFFFFFF的虚拟地址范围,刚好4个GB,而不同进程的虚拟地址会被操作系统的虚拟内存管理器映射到不同的物理地址,也就是说,当CPU在不同的进程中访问同一个虚拟地址的时候,在物理上这两个地址是不一样的,物理地址才是CPU真正访问的地址.这样就实现了进程内存空间的隔离.在这4GB的地址中,0x00000000-0x7FFFFFFF这2GB是供用户态代码使用的,剩下的2GB是内核态的,用户态代码不能访问.虽然说0x00000000-0x7FFFFFFF供用户态使用,但并不代表代码中的指针就可以随便指向其中任意一个地址,比如0-0x100000地址就是不可指向的,否则会引发访问违规.
说完了寄存器和内存地址后,就可以正式介绍汇编指令了.
汇编指令大体上可以分为数据传送指令,数学运算指令,程序流程控制指令.
每一条指令都有相应的操作符和操作数,有的指令可以没有操作数.
操作数分为源操作数和目标操作数,例如MOV EAX,1这条指令就是一条数据传送指令,MOV是操作符,EAX是目标操作数,源操作数是1
数据传送指令有很多,常见的还有:
LEA取地址指令,该指令有两个操作数,目标操作数必须是寄存器,源操作数是一个内存地址.例如:

这条指令的意思是将ESI+1的值作为一个内存地址赋给ECX,此时ESI的值是:

那么ESI+1=0x01001E19+1=0x01001E1A,执行该指令后ECX将等于0x01001E1A.该指令和MOV的区别在于,如果是MOV指令,那么CPU将会从0x01001E1A地址中取从该地址开始的一个DWORD赋值给ECX,而0x01001E1A现在的内容是:

也就是ECX将会等于0x000002CF.
PUSH 压栈指令
这也是一条非常常见的数据传送指令.该指令有1个操作数,为要压入栈顶的值,可以是常量,寄存器或者某个内存地址中的值.什么是栈?栈是程序的内存空间中一块特殊的内存区域,主要用于存放局部变量和函数参数.它也是一种数据结构,遵循"先进后出"的规则.在栈中存放数据就如同叠一堆盘子一样,最后一个放进来的盘子总是在最上面.在Ollydbg的栈窗口中可以看到当前线程使用的栈情况:

注意,地址小的在上面,地址大的在下面,每个地址之间都相差4个字节.其中默认高亮的一行就是栈的顶部.PUSH指令会把栈顶减少4个字节并把操作数送到新的栈顶中,例如:

现在EDI的值为:0x00B61E19

按下F7执行这条指令后,堆栈是这样的:

可以看见栈顶减少了4个字节,且EDI寄存器的值被送入新的栈顶.
还有非常重要的一点是,ESP寄存器默认指向堆栈的顶部,EBP寄存器默认指向当前栈帧的底部:

每执行一次PUSH指令,ESP就会减少4.
与之相对的是POP指令,POP指令的格式与PUSH指令相同,执行的操作和PUSH指令相反,它将当前栈顶中的数据赋给操作数,并将栈顶下移4个字节,也就是增加4.例如当前栈情况是这样的:

有一条这样的POP指令:


当前栈顶的内存数据为1,执行完这条指令后:

ECX变成了1,ESP比原来增加了4.
XCHG指令:交换指令,该指令有2个操作数,可以是寄存器也可以是内存地址,但不能同时为内存地址,且交换的数据长度必须一致.例如:XCHG EAX,ECX指令将交换EAX和ECX的内容.不能写XCHG EAX,CL.
数学运算指令:
ADD: ADD指令(Addition)有两个操作数,它将两个操作数相加并把结果存在第一个操作数中,例如:EAX=1,ECX=2,那么执行了ADD EAX,ECX之后,EAX=3,ECX不变.
SUB: SUB指令(Subtraction)有两个操作数,它将两个操作数相减并把结果存在第一个操作数中,例如:EAX=2,ECX=1,那么执行了SUB EAX,ECX之后,EAX=1,ECX不变.
MUL:无符号数乘法指令(Multiplication),该指令有一个操作数,可以是寄存器或者内存地址.例如在执行该指令前寄存器的状态是:

如果执行了MUL ECX指令,那么该指令进行的操作是:将EAX和ECX相乘,然后将结果的高位放在EDX中,低位放在EAX中.上图中,EAX*ECX=7×3=21=0x15:

实际结果是0x0000000000000015,前面的8个0放在EDX中,后面的00000015放在EAX中.
IMUL:有符号数乘法指令,该指令可以有1个,2个或者3个操作数,当操作数为1个时,执行的操作跟MUL指令一样,不同的是它将操作数和EAX当作有符号数进行运算而不是无符号数.例如ECX=0xFFFFFFFD,MUL指令将把它作为10进制的4294967293进行运算,而IMUL指令将把它作为10进制的-3进行运算.
当操作数为2个时,IMUL指令将两个操作数相乘并将结果存放在第一个操作数中,例如IMUL ECX,EDX,ECX=3,EDX=4,那么结果ECX=12.当操作数为3个时,IMUL指令将后两个操作数相乘并将结果放在第一个操作数中,例如 IMUL ECX,EDX,3,EDX=5,那么结果
ECX=15.
DIV:无符号除法指令(Division).该指令有1个操作数.它执行的操作是:将EDX和EAX放在一起的一个64位的数作为被除数,操作数作为除数,执行除法,除法所得的商存在EAX中,余数存在EDX中,例如:

如果执行指令DIV ECX,则被除数为0x0000000000000017(23),除数为3,除法的结果是23÷3=7••••2

IDIV:有符号整数除法指令,该指令与DIV指令大体相同,不同之处在于把操作数作为有符号数而不是无符号数进行运算.
DEC:减1指令(Decrease)该指令有1个操作数,执行后将操作数的数-1,与之相对的是INC(Increase)指令,将操作数+1.
AND:按位与指令,该指令有2个操作数,它将两个操作数按位进行与运算,运算后的结果存放在第一个操作数中.与运算的规则是:0与0=0,0与1=0,1与1=1.
例如EAX=0x12345678,EBX=0x87654321,执行AND EAX,EBX,我们人来做这个运算的话,需要先将EAX和EBX转为2进制:


然后把这两个2进制数对应的位进行与运算,当然计算机直接就可以进行这种运算,运算后的结果为:

OR:按位或指令,其格式与AND运算一致,规则为
0 OR 1=1
1 OR 1=1
0 OR 0=0.
XOR 异或指令,格式同上,其规则为:
0 XOR 0=0
1 XOR 1=0
0 XOR 1=1
NOT指令:按位取反并-1指令,它有1个操作数,将操作数取反后减去1,例如 NOT EAX,EAX=-3,则运算结果EAX=2.
NEG指令与NOT指令格式操作基本一致,不同在于NEG指令只取反而不-1.
CMP指令:比较指令(Compare)比较两个操作数的大小并设置相关的标志位.CMP指令实际上是减法指令,但它不改变两个操作数的值.CMP指令经常和条件跳转指令结合使用控制程序的走向,在下面说到跳转指令时再详细说明.
TEST指令:TEST指令实际上是按位与指令,但它不改变两个操作数的值.该指令经常用于检测一个寄存器的值是否为0.例如
TEST EAX,EAX.如果EAX=0,那么执行之后Z标志位将会置为1,否则Z标志位为0.
程序流程控制指令:
JMP指令:跳转指令,操作数为1个,可以是寄存器也可以是某个地址.程序执行该指令后会跳转到操作数所代表的地址.例如:

这里的short是短距离的意思,当跳转要执行时,Ollydbg会用一条红色线标出跳转的目标,此时按Enter键可以查看跳转的目标地址(但不会执行跳转).
CALL指令:调用子函数指令,执行该指令后将会转移到另外一个函数内部执行该函数的代码.它和JMP指令的不同在于,JMP指令执行后通常不需要返回到JMP指令的下一条指令,而CALL指令在执行完毕后需要返回到原指令的下一条指令继续执行,这是通过和RETN指令配合实现的,具体:

这是一条CALL指令,按F7执行该指令后,程序将跳转到0x0124CF04这个地址继续执行:

注意栈的变化:

可以看到当前栈顶中的值就是刚刚的CALL指令的下一条指令的地址.并且Ollydbg自动给出了提示.
当该函数中的代码执行完毕后,在末尾将有一个RETN指令:

执行完RETN指令后,程序就回到了原来的地方:

也就是说,在CALL指令执行的同时,CALL指令的下一条指令的地址将会被压入栈顶,返回的时候需要这个地址才能正确地返回.
还有比较重要的一点是,我们都知道函数是会有返回值的,对于C函数而言其返回值默认是存放在EAX寄存器中的,所以执行完一个函数后,看EAX的值可以估计该函数的执行结果.
JE指令:(Jump If Equal)相等则跳转指令.更确切地说是当ZF=1时跳转.例如CMP EAX,1 如果EAX=1,则CMP指令执行后会将ZF设为1,那么这条跳转指令就会执行,否则这条指令将不会执行.
JNZ指令:(Jump If Not Equal To Zero)不相等则跳转指令.当ZF=0时跳转成立.例如CMP EAX,1,如果EAX≠1,则这条跳转指令将会执行.
JS指令:负跳转指令,当SF=1时跳转将会执行.与之相对的是JNS指令.例如CMP EAX,1,则当EAX的值>=1时JNS成立而JS不成立.
JG指令:(Jump If Greater)当ZF=0且OF和SF标志位相等时跳转执行.该指令前通常会有一个CMP指令,将两个操作数按有符号数进行比较,如果操作数1>操作数2,则JG指令将会执行.
JGE指令:与JG指令类似,但操作数1=操作数2时,跳转也会执行.
JA指令:高于则跳转指令,当CF=0且ZF=0时跳转成立.通俗来说,假设在该指令前有指令CMP EAX,0,且此时EAX=-1,而JA指令将两个操作数都当作无符号数看待,而-1转为无符号数是4294967295>0,因此JA跳转成立.JAE指令包含了相等的情况,JAE指令在Ollydbg里面显示为JNB指令.
JL指令:小于则跳转指令.当ZF=0且OF和SF不同时跳转执行,否则不执行.该指令执行的操作与JG指令相反.例如将两个操作数按有符号数进行比较,如果操作数1<操作数2,则JL指令将会执行.
JLE指令包含了两个操作数相等时也跳转的情况.
除了以上的指令外,还有一些特殊的指令,例如:
NOP指令:该指令不做任何操作,但不能将有NOP指令的地方用0填充,否则程序可能会崩溃.
INT 3指令:断点指令,当CPU执行到该指令时将会产生一个异常,如果没有处理该异常则程序将崩溃,如果处理了该异常则可以继续执行.
以上指令虽然不完全,但在Ollydbg中已经相当常见了.之后如果碰到没讲的指令就现场百度吧.
这一章就说到这里,下一章我们将讨论Windows编程的一些常识.