汇编一些有用的知识 (笔记)

发布时间:2010-12-6 10:35
分类名称:Debug_Crack


注释:里面的一些解释来自《使用OllyDbg从零开始系列》教程。
===============================================
RAX, EAX, AX, AL, AH 分别对应64bit, 32bit, 16bit(低),16bit(高)
===============================================

NOP(无操作) (对应的16进制编码为90)

运行这条指令不会对寄存器,内存以及堆栈造成任何影响,英文单词的意思是无操作”,也就是说,它没有特殊的用途。例如,你用一个短指令来替换一个长指令的话,如果处理器没有错误,多余的空间将会被NOP填充。

===============================================
Push,Pop
这本来很简单的两条指令,但如果不理解清楚,可能在反汇编的时候,会经常被Esp,Ebp搞得稀里糊涂。一开始我一直以为,Push 1,及相当与将1压栈,然后esp指向1紧邻的下一个单元,后来才发现这种理解是错误的。Push 1,相当于esp先下移(减小一个指针单元),然后将1存入esp指向的单元里。对应的,Pop先指向的值存入目标内存,然后再上移。

PUSHAD

PUSHAD指令把所有通用寄存器的内容按一定顺序压入到堆栈中,PUSHAD也就相当于’PUSH EAX, PUSH ECX, PUSH EDX, PUSH EBX, PUSH ESP, PUSH EBP, PUSH ESI, PUSH EDI’

POPAD

该指令与PUSHAD正好相反,它从堆栈中取值,并将它们放到相应的寄存器中。POPAD等价于“POP EDI,POP ESI,POP ESP,POP ESP,POP EBX,POP EDX,POP ECX,POP EAX”。

这俩条指令,通常在被加壳的程序里常见(如UPX等)。


PUSHA 等价于 'PUSH AX, CX, DX, BX, SP, BP, SI, DI'

POPA 等价于 'pop DI, SI, BP, SP, BX, DX, CX, AX'

PUSHA,POPAPUSHAD,POPAD就像姐妹一样,只不过它们在16位程序中使用

===============================================
mov 只要记住俩个只能赋值的情况,其余的情况都能随意赋值。
段寄存器只能通过普通寄存器来被赋值。 如:MOV DS,EAX
内存单元只能通过寄存器来被赋值。       如:MOV [12345678],EAX

MOVSX (带符号(S)扩展(X)的传送指令)

第二个操作数可能一个寄存器也可能是内存单元第一个操作数的位数比第二个操作数多第二个操作数的符号位填充第一个操作数剩余部分。 如MOVSX EAX, BX, 其中BX值为F000,EAX中为全0,结果为EAX剩余的高位,用BX的最高位填充,如果是一个负数,最高位为1,全用1填充,EAX为FFFFF000.如果BX为1234,则EAX为00001234.


MOVZX (0扩展的传送指令)

MOVZX类似于前面的语句,但是这种情况下,剩余的部分不根据第二个操作数的正负来进行填充。我们这里不提供范例,因为和上面是相似的剩余的部分总是被填充为0,不判断符号位。相当于无符号操作。

===============================================
[] 中放的地址,用来寻找地址对应内存中的值,只能是常数,bx,si,di,bp
正确用法
[bx],[bp],[si],[di],[bx+si],[bx+di],[bp+si],[bp+di],[bx+si+idata],[bx+di+idata],[bp+si+idta],[bp+di+idata]

错误用法
[bx+bp], [si+di] 
SI DI 不能被分成8位寄存器来使用。
===============================================

INCDEC
INC EAX 、INC DWORD/WORD/BYTE PTR [00452234] 自加一
INC EAX 、INC DWORD/WORD/BYTE PTR [00452234] 自减一

ADD
ADD指令有两个操作数,相加后的结果存放到第一个操作数中。ADD EAX,1等价于INC EAX

ADC(
带进位的加法)
在这种情况下,两个操作数的和加上进位标志的值,结果存放到第一个操作数中。如:
ADC ECX,3  ;此时ECX为 21, 进位C 为1, ECX = ECX + 3 + 1 = 25

SUB
这个指令与ADD刚好相反-它将第一个操作数减去第二个操作数的值存放到第一个操作数中。

SBB
该指令跟ADC正好相反,它计算两个操作数的差值,并且还要减去进位标志,结果存放到第一个操作数中。

MUL(无符号数的乘法)
有两种乘法,第一个种是MUL,这种是无符号数乘法,只有一个操作数,另一个操作数是EAX,结果存放到EDX:EAX中。例如:
MUL ECX
这里是无符号数EAX, ECX相乘结果存放到EDX:EAX中。

IMUL(有符号数的乘法)
IMUL指令用法类似于MUL
IMUL ECX
该指令将有有符号数ECX乘以EAX,结果存放到EDX:EAX中。
除了上面一条指令外,IMUL还允许使用多个操作数,这是与MUL不同的地方。

尽管在默认情况下是使用EAXEDX寄存器,但是我们还可以指定其他的数据源以及目标多达三个操作数。第一个操作数,用来乘以两个数,并且保存相乘的结果,结果必须保存在寄存器中。下面我们将看到两个和三个操作数的例子。

F7EB  IMUL EBX                           EAX x EBX -> EDX:EAX

这是第一个例子,类似于MUL,但是是有符号的乘法。

696E74020080FF   IMUL EBP, DWORD PTR [ESI+74]  ; FF800002 [ESI+74] x FF800002 -> EBP

这是第二个例子,其中有三个操作数:  ESI+74内存单元的值乘以FF800002,并且将结果存储到EBP中。我们可以在OD中执行看看。相乘的结果,如果很大,最高位部分将被舍弃。(溢出了)。所以,如果想做大数运算,你得自己实现,溢出部分的处理。用大数库即可。

第三个例子,只有两个操作数,两者相乘,并将结果存放到第一个操作数中。

0FAF55E8              IMUL EDX, DWORD PTR [EBP-18]       EDX x [EBP-18] -> EDX


正如你所看到的,这些方法中,最好的方式就是一个操作数的方式,结果存放到EDX:EAX,这意味着拥有两倍的大小,通过两个操作数或者三个操作数无法完成的操作可以通过一个操作数的方式来完成。

DIV(无符号除法)/IDIV(有符号除法)

这两个指令反别与MULIMUL相反。

DIV只有一个操作数,该操作数必须是无符号数,结果存放到EDX:EAX中。

IDIV指令经常被使用。如果是一个操作数的话,那么它和DIV类似,只不过操作数是有符号的,结果依然保存在EDX:EAX中。两个操作数的情况,第一个操作数除以第二个操作数,结果存放到第一个操作数中。三个操作数的情况,第二个操作数除以第三个操作数,结果存放到第一个操作数中。

XADD(
交换并相加)
正如你所猜想的一样,这个指令其实就是XCHGADD两个简单指令的组合。相当于:
XCHG EAX, EBX
ADD    EAX, EBX

NEG
该指令的目的是将操作数的符号取反,即如果我们有一个32位的16进制数,NEG操作以后,结果就会取反。例如:
NEG EAX ; EAX值为0x32,结果为-32 (补码表示:FFFFFFCE)

逻辑指令
逻辑指令有两个操作数,两操作数按位运算,并将结果存放到第一个操作数中。

AND
只有两个二进制位都为1的时候结果才为1,其他情况,结果都为0
1 and 1 = 1
1 and 0 = 0
0 and 1 = 0
0 and 0 = 0

 OR
该指令AND的不同之处在于,两位中只要有一位为1,结果就取1
1 or 1 = 1
1 or 0 = 1
0 or 1 = 1
0 or 0 = 0 

XOR
该指令时异或运算,当两位不同时取1,相同时取0
1 xor 1 = 0
1 xor 0 = 1
0 xor 1 = 1
0 xor 0 = 0 

NOT
该指令是简单的按位取反。
not 1 = 0
not 0 = 1
例如:not 0110 = 1001 

===============================================

XCHG (交换 寄存器/内存单元 寄存器)
该指令交换两个操作数的值,例如:
XCHG EAX,ECX

===============================================
xor eax,eax 相当于 mov eax,0
lea edi,[epb-0ch] 相当于 mov edi, epb-0ch,但后面的mov指令是错的。epb-0ch不能这么写。但可以将其放到[]中。

===============================================
X ptr 指明内存单元的长度。X可以为(word, byte)

===============================================
db 重复次数 dup (重复的字节型数据)
dw 重复次数 dup (重复的字节型数据)
dd 重复次数 dup (重复的字节型数据)

===============================================
无条件转移指令
段内转移
短转移(-128~127)
jmp short 标号(8bit)
近转移(-32768~32767)
jmp near 标号(16bit)
jmp reg(16bit)
jmp word ptr 内存地址(16bit)
段间转移
jmp far ptr 标号
jmp dword ptr 内存地址

条件转移指令 短转移
jcxz, cx=0,就跳

===============================================
过程
ret  用于栈中的数据,修改IP的内容,实现近转移
相当于 pop IP

retf 用于栈中的数据,修改CS和IP的内容,实现远转移
相当于 pop IP,pop CS

call 将当前IP或CS和IP压栈,转移(不能实现短转移)

近转移
call reg(16bit)
call word ptr 内存地址(16bit)
call 标号
相当于:
push IP
jmp near ptr 标号

远转移
call dword ptr 内存地址
call far ptr 标号 段间转移
相当于:
push CS
push IP
jmp far ptr 标号
===============================================
中断

CPU 内部中断:
除法错误
单步执行
执行into指令
执行int指令

iret相当于:
pop IP
pop CS
popf

int n


===============================================
cmp opr1,opr2
opr1-opr2,不保存结果,影响标志位。
opr1 == opr2, zf = 1
opr1 != opr2, zf = 0
opr1 < opr2, cf = 1
opr1 >= opr2, cf = 0
opr1 > opr2, cf = 0 && zf = 0
opr1 <= opr2, cf = 1 || zf = 1
大部分情况下,只判断zf即可。对应的跳转指令有:jz/je

JMP 跳转

JE, JZ 结果为零则跳转

JNE, JNZ 结果不为零则跳转

JS 结果为负则跳转

JNS 结果不为负则跳转

JP, JPE 结果中1的个数为偶数则跳转

JNP, JNPE 结果为1的个数为奇数则跳转

JO 结果溢出了则跳转

JNO 结果没有溢出则跳转

JB, JNAE 小于则跳转 (无符号数)

JNB, JAE 大于等于则跳转 (无符号数)

JBE, JNA 小于等于则跳转 (无符号数)

JNBE, JA 大于则跳转(无符号数)

JL, JNGE   小于则跳转 (有符号数)

JNL, JGE 大于等于则跳转 (有符号数)

JLE, JNG 小于等于则跳转 (有符号数)

JNLE, JG 大于则跳转(有符号数)


常和条件转移一起使用:
je/jz ==, zf == 0
jne/jnz !=, zf != 0
jb/jnae <, cf == 1
jnb/jae >=, cf == 0
ja/jnbe >, cf == 0 && zf == 0
jna/jbe <=, cf == 1 || zf == 1

JL/JNGE <, sf == 1
JNL/JGE  >=, cf == 0
JLE/JNG >,sf == 0 && zf == 0
JNLE/JG <=, sf == 1 || zf == 1


写程序不必分析标志位,只要分析逻辑方式就可以。

===============================================
标志位:
ZF, zero flag 结果为0,则ZF = 1
PF, parity flag 结果1个个数是否为偶数,如果是偶数,则PF = 1
SF,sign flag, 结果为负,则SF = 1
CF,carry flag,进位
OF,overflow flag,溢出
DF,direction flag,方向标识 针对串处理,(SI,DI),DF == 0,SI,DI每次递增,反之递减。

===============================================
串处理

MOVS
该指令是从一个地址向另一个地址复制数据。源地址保存在ESI寄存器中,目的地址保存在EDI寄存器中。我们不需要显示地指定参数。 

MOVSB 传送一个字节
MOVSW传送一个字 
MOVSD传送一个双字

MOVSB 传送一个字节
汇编描述:
mov es:[di], byte ptr ds:[si] ;8086不支持,只是用来描述
if df == 0:
inc si
inc di
if df == 1:
dec si
dec di

movsw 传送一个字
mov es:[di], word ptr ds:[si] ;8086不支持,只是用来描述
if df == 0:
add si,2
add di,2
if df == 1:
sub si,2
sub di,2

movsd 传送一个
mov es:[di], dword ptr ds:[si] ;8086不支持,只是用来描述
if df == 0:
add si,4
add di,4
if df == 1:
sub si,4
sub di,4

REP

该指令可做为前面介绍的一些指令的前缀,尤其是MOVS指令。该前缀表示当前指令需要执行的次数ECX。每次循环计数器ECX的值递减1,和前面介绍的循环一样。因此,REP MOVS不一定拷贝是4个字节,它拷贝的大小为 每次拷贝的大小 * ECX, 源指针ESI和目的指针EDI每次递增4或者递减4(递增或递减取决于方向标志位D)

此外,REP前缀还可以衍生出REPE/REPZREPNE/REPNZ指令,这个时候我们就需要考虑零标志位Z了。但是REPE/REPZ或者REPNZ/REPNE连同MOVS指令一起使用就没有太大意义了。此外还有其他指令支持前缀,我们以后再讨论。

LODS
该指令从源地址(像之前一样,ESI)拷贝数据到EAX中。如:
LODS DWORD PTR DS:[ESI]; EAX = ESI指向的内存值
REP前缀也可以与LODS指令配合使用,他们会重复执行直到计数器ECX的值为0

也有一次拷贝两个字节和一个字节的LODSWLODSB指令

STOS
该指令是将EAX的值拷贝到EDI指向的内存单元中。
我们可以使用REP前缀,还有每次操作两个字节的STOSW指令和每次操作一个字节的STOSB指令。

例如:Debug版本的VC编译出的函数头部,总有一段类似的代码:

mov         ecx,30h 
mov         eax,0CCCCCCCCh 
rep stos    dword ptr es:[edi] 
es:[edi] 开始,一次填充4个byte,值为0xCCCCCCCC,等一个填充了30*4个0xCC,为何填充CC?CC对应的汇编指令为INT 3,如果调试器在调试,调试器就能中断下来,让调试器最早发现错误的地方。

串操作方向,可以通过如下指令指定:
cld, DF = 0
std, DF = 1

CMPS
该指令比较ESIEDI指向内存单元的内容。
指令影响的零标志位Z,所以你可以配合REPE/REPZ前缀指令使用,直到计数器ECX的值为0或者零标志位清0

===============================================
test属于逻辑运算指令
功能: 执行BIT与BIT之间的逻辑运算
测试(两操作数作与运算,仅修改标志位,不回送结果). 
Test对两个参数(目标,源)执行AND逻辑操作,并根据结果设置标志寄存器,结果本身不会保存。EST AX,BX 与 AND AX,BX 命令有相同效果

语法: TEST r/m,r/m/data
影响标志: C,O,P,Z,S(其中C与O两个标志会被设为0)

运用举例:
1.Test用来测试一个位,例如寄存器:

test eax, 100b; b后缀意为二进制
jnz ******; 如果eax右数第三个位为1,jnz将会跳转

我是这样想的,jnz跳转的条件是ZF=0,ZF=0意味着ZF(零标志)没被置位,即逻辑与结果为1.

2.Test的一个非常普遍的用法是用来测试一方寄存器是否为空:
test ecx, ecx
jz somewhere
如果ecx为零,设置ZF零标志为1,Jz跳转

===============================================

循环:

XOR ECX,ECX

MOV ECX,15h

将计数器初始化为循环次数15h。接下来就是循环体了:

Label:

DEC ECX

该计数器每次递减1

其实就是循环体了,循环体里面可以是任意指令。

最后,你需要添加一个判断计数器是否为0的指令以及条件跳转指令。

CMP ECX,0

JNE Label


循环指令(如loop)短转移

LOOP
LOOP指令可以帮我们完成前面例子中的事情- 将计数器ECX的值减1,判断ECX的值是否为0,如果为0就跳转到指定的地址-将像前面的例子一样。(可惜的是,大多数现代的处理器中该指令的效率不如前面模拟的例子。)


for循环:
mov <循环变量>,<初始值> ;给循环变量赋初值
jmp B                    ;跳到第一次循环处
A: (改变循环变量)    ;修改循环变量
    ... ...
B:  cmp <循环变量>,<限制变量>    ;检查循环调节
    jge 跳出循环
    (循环体)
    ... ...
    jmp A                ;跳回去修改循环变量

如:
    int i;
    for (i = 0; i < 50; i++)
013D2DC7  mov         dword ptr [i],0
013D2DCE  jmp         fun1+39h (13D2DD9h)
013D2DD0  mov         eax,dword ptr [i]
013D2DD3  add         eax,1
013D2DD6  mov         dword ptr [i],eax
013D2DD9  cmp         dword ptr [i],32h
013D2DDD  jge         fun1+4Ah (13D2DEAh)
    {
        c = c + i;
013D2DDF  mov         eax,dword ptr [c]
013D2DE2  add         eax,dword ptr [i]
013D2DE5  mov         dword ptr [c],eax
    }
013D2DE8  jmp         fun1+30h (13D2DD0h)

do循环:
cmp <循环变量>,<限制变量>
jl  <循环开始点>

while循环:
A:    cmp <循环变量>,<限制变量>
    jge B
    (循环体)
    ...
    jmp A
B: (循环结束)