概述
1.小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
2.MD5加密:单向算法,只能从字符串计算出MD5值,不能逆向计算出源代码(因为并非一一对应的,可能有多个源码对应一个MD5)。MD5的碰撞。应用:RAR密码
3.静态的数据(RAR,图片等)加密后一般无法破解。
4.破解的方法:①.找出加密原理 ②.绕过判断机制(低级)
5.RSA加密:双密钥算法(公钥和私钥,不能互推),涉及素数,取模计算(将两个大素数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥)。公钥只用来加密(一般是公开的),私钥只用来解密(关键,不公开),只有知道私钥才能解密,否则即使是加密者也无法解密。应用(广泛):数字签名,email加密。
6.注册码的生成方法之一:EXE内只有公钥及N,作者本人有私钥,用户的mac地址作为机器特征码,
m' =RSA(mac,公钥)发送给作者,作者先收取mac地址 = RSA(m',私钥),计算注册码 sn =RSA(mac,私钥)返回给用户,软件再对sn进行计算y = RSA(sn,公钥),if( y==mac ) Success!
破解思路:替换EXE内的公钥为自己知道的一组密钥,以此计算出一组注册码给修改过的软件校验。
7.机器语言不能直接删除,否则会使后面的地址全部往上填充进来,这样会影响后面的涉及地址跳转的操作。方法:①把需要修改的操作机器码都改成 90 (nop—No Operation)
②改成jump+ 地址,跳转到验证完的地址
8.修改exe文件:010Editor(只能查看16进制),QuickView(可以查看16进制和汇编代码)
QuickViewF7搜索,F2在16位汇编和32位汇编切换,F3撤销,
Tab在机器码与汇编码间切换,Alt+F9保存,F1帮助
9.寄存器ds只能赋值为另一个寄存器的值而不能直接赋值为常数
10.data代表字符串abc的段地址,而offset abc是abc的偏移地址,段地址:偏移地址 ->组成一个完整的地址
11.Build == compile +link
assemble == compile : asm-->obj
link : obj-->exe
12.表示一个十六进制常数时,必须用数字开头(如果是字母就添0)
13.C语言数字类型的对应关系:
unsignedchar = byte(8位) db(definebyte)
unsignedshort int = word(16位) dw
unsignedlong int = double word(32位) dd
14.定义一个char的变量a:
a db 12h ; unsigned char a = 0x12
15.小端模式(Little Endian):与书写习惯相反,比如存放一个变量long long a = 0x12345678h
变量的地址为1000,则连续地址存放的数据: 1000:78h 1001:56h 1002:34h 1003:12h
意义:a仍为long long,定义char b,若执行b =a,则a会自动强制转换为8位的char,小端模式使其只取首地址抛弃之后多余的地址的值
即只将地址1000里的78h赋值给b,亦即把最低位开始的值赋值给b,符合常规的思维习惯
16.char->unsignedint ,假定unsigned int 是16位(如TC环境)
若原先的8是符号数,则需要进行符号扩充,规则:左边补上的位值 = 原数的最高位(即正数补0,负数补1)(正数不变)
1111 1111 -> 11111111 1111 1111
char 的-1 -> unsigned int 的 -1
符号扩充只跟被扩充数的类型有关(即只要它是符号数就必须进行符号扩充),与扩充以后的目标类型无关(即不管它是有符号还是非符号)
17.unsiged char ->int / unsigned int
规则:左侧一律补0
18.小数在内存中的保存:如 float x = 127.375(四个字节)
01000010 1111111011000000 00000000
0 1000010 1 1111110 1100000000000000
符号 偏置指数(8位) 尾数(23位)
偏置指数 = 133
则实际指数 = 133-127 = 6
对尾数的处理:先在前面添加一个1凑够24位,并且在这个1后添加一个小数点 1.1111110 11000000 00000000
将小数点向后移动6位,变成 1111111.0 11000000 00000000
可得实际数据 127.375 (小数点后第n位的权值为2^-n)
19.console application可以单步往下跟
windows application里面有消息循环,所以通常用单步跟踪无法到达注册码判断的位置
所以必须要有切入点,即找出注册码判断时可能调用的判断函数
一般是MessageBoxA(弹出消息框),CreateWindow(),ShowWindow(),【DestroyWindow()】(一般离判断最近)
20.常用拦截API:CreateFileA()与打开文件有关(之后又ReadFile()读取文件内容)
RegCreateKey(),RegOpenKey()
21.filemon可以监视打开并写入到什么文件;regmon可以监视注册表的写入; autoruns 可以观察开机运行程序
22.8086(16位)共有14个寄存器,每个寄存器都是16位(决定地址位数和int位数(宽度)):
AX,BX,CX,DX,(通用寄存器,算数运算,位运算)
SP,BP,SI,DP(用来存放偏移地址)
CS,DS,ES,SS(用来存放段地址),IP(instruction pointer,当前指令的偏移地址),FL(flag,标志寄存器)
23.所有类型的指针大小都等于寄存器大小,因为指针变量需要用一个寄存器来表示。
比如,在16位CPU中,si,di,bx,bp均可以表示一个指针;
32位CPU中,eax,ebx,ecx,edx,esi,edi,ebp,esp均可以表示指针
64位CPU中,rax,rbx,rcx,rdx,rsi,rdi,rbp,rsp均可以表示指针
24.32位CPU的地址最大值(寻址范围)为2^32-1 B,即3.多GB(即32位系统所支持的最大内存)
25.地址 = 段(首)地址:偏移地址(把一个物理地址用两个16位寄存器分别存储)
如一个物理地址 12398h=1234:0058=1235:0048=1236:0038=1230:0098
26.段首地址最后一位必须等于0
27.32位内存也需要分段,一是便于管理,二是段具有自己的属性,能够对某一段内存设置属性(如只读)
28.GDT表:
gdtr = gdt的首地址
29.AX由AH及AL构成,其中AH是高8位,AL是低8位。BX,CX,DX同。
mov ah,98h
mov al,0DEh
30.ds:[esi] 表示在ds指向的段中,取出esi所指的对象
地址 值
31.byteptr 相当于 (char*)强制类型转换(byte一个字节,相当于char), 2000:1000 34h
例如: 2000:1001 12h
movax,2000h 2000:1002 56h
movds,ax 2000:1003 78h
movsi,1000h
mov al,byte ptr ds:[si] ;把ds:[si]指向的数据转换为byte类型后存储到al中,AL=34h(间接寻址)
mov ax,word ptrds:[si] ;AX =1234h
mov eax,dword ptrds:[si] ;eax = 78561234h
其中mov的目的地为寄存器时,byte/word/dword ptr 这三个语句可以省略
因为编译器会进行类型的检查,当检测到mov左边为寄存器时,会自动把右边的数值强制转换为与寄存器的位数对应的类型(al-8位,ax-16位)
32.在程序中引用某个变量,段地址必须用段地址寄存器;而偏移地址可以用寄存器(间接寻址),也可以用常数(直接寻址),或两者的组合(加减法)
33.若两个值相加超过了寄存器的位数,则抛弃高位只取低位
34.一段源程序:
datasegment
abc db1,2,3 ;char[3] = {1,2,3};
xyz dw1234h, 5678h, 9999h
data ends
codesegment
assume cs:code ds:data
mov ah,ds:abc 将abc这样写,该句编译后变成 movah,ds:[0000] ;(相当于把abc加上方括号,即取其偏移地址)
mov ah,ds:[abc] abc本来是变量的名字,用方括号括起来就变成了它的偏移地址,编译后与上一句相同
mov bl,ds:[abc+1] 编译后变成movbl,ds:[0001]
mov bl,ds:abc[1] 编译后变成movbl,ds:[0001] 这里abc[1]并不是数组abc的第一个元素,而是相对于abc的地
址+1的地址
; mov bl,ds:abc+1 虽然编译后同上一句,但是防止混淆应避免这样的写法
movcs,ds:xyz cs = 1234h
movdx,ds:[xyz] cs = 1234h
movsi,ds:xyz[2] si = 5678h
mov di
movah,4Ch
code ends
end
35.控制寄存器:IP,FL
IP: instruction pointer 指令指针,与CS配合起来指向将要执行的下一条指令
即 CS:IP 指向下一条将要执行的指令(32位当然是ECS:EIP)
FL:Flag
状态标志:CF,ZF,SF,OF,AF,PF(后两个较少用)
控制标志:DF,IF,TF
以上9个位都存在于标志寄存器FL中
寄存器FL的长度取决于CPU位数
36.CF:进位标志(carry flag):随时反映当前计算结果是否有进位(或借位)
这里的进位(借位) 指的是对当前 寄存器最高位的更高位进位(或借位)
比如 al是的宽度为8位, 只有计算结果需要用到第九位才叫进位(或借位)
moval,0FFh al = 1111 1111
add al,2 al = 00000001 (FF与2相加产生了进位,CF的状态会立即变为1)
有时也把CF=1称为非符号数的溢出标志(ff+2!=1,相加过程中有一位溢出)
相关指令(用途):
moval,oFFh
add al,2
jc has_carry(段名) (jump if has carry,有进位则跳)
...
has_carry:
....
注:CF也可表示借位标志,当做减法运算产生借位时,也会变成1
37.OF(Overflow Flag)溢出标志(以下情形均为符号数)
溢出有两种情况:两个正数相加的结果是负数,两个负数相加的结果是正数就溢出(一正一负相加不会溢出)
moval,oFFh
add al,2
执行完后 OF=1,CF=1
相关指令jo(jump ifoverflow),jno(jump if not overflow)
38.SF(Sign Flag) = 【运算结果的最高位】
反映当前的运算结果是正的(SF=0)还是负的(SF=1)
相关的指令:js(SF=1时,即有符号则跳),jns(SF=0,无符号则跳)
39.ZF(Zero Flag)零标志
运算结果为0时,ZF=1;否则ZF=0 (应该把ZF的值理解为真和假)
subax,ax ;AX=0,ZF=1
addax,1 ;AX=1,ZF=0
指令:jz(jump ifzero)==je
jnz(jump if not zero)==jne(机器码完全相同,两个指令等价)
原因:
cmpax,bx
jethey_are_equal
实际上cmp指令做的是减法运算,因此ax==bx即差等于零时,ZF=1,je和jz都会执行
但与减法运算sub相比,cmp指令不会改变左侧变量的值
40.AF(Auxiliary Carry Flag):辅助进位标志
记录运算时第五位(二进制的第n位都是右数,且最右边的为第0位)产生的进位或借位值
第五位有进位或借位时,AF=1;否则AF=0
moval,23h
addal,1Fh
41.PF(Parity Flag) 奇偶标志
当运算结果的低八位(即用16进制表示的2位)中"1"的个数为偶数时,PF=1,否则PF=0.
指令:
jp==jpe(jumpif parity even)
jnp==jpo(jumpif parity odd)
42.DF(DirectionFlag):
标志位DF控制字符串操作的方向,当DF=0时为正方向,
DF=1时为反方向.
若源首地址<目标首地址,则复制按反方向
(如右图,左边为源地址,右边为目标地址)
若源首地址>目标首地址,复制按正方向
用指令cld使DF=0,用指令std使DF=1
cld(clear direction,使DF=0,表示正方向)
std(set direction,使DF=1,表示反方向)
43.IF(Interrupt Flag)中断标志:
当IF=1时,允许中断;否则关闭中断.
cli指令使 IF=0 表示关/禁止硬件中断
sti指令使 IF=1 表示开/允许硬件中断
键盘缓冲区 是一个队列(可以理解为一个数组)
charkey[100]
char *p =&key[10]
(时钟中断)
44.
TF(Trace/Trap Flag) 跟踪/陷阱标志
用于程序调试时进行单步的方式工作
当TF=1,cpu在执行每一条指令后会自动调用int 1h中断.TF=0是正常状态
int1h实际上才一个函数(程序)等待输入f8时才返回
45.端口
端口地址的分配独立于内存地址的分配
并且端口地址范围为[0000h,0FFFFh]
共65536个端口,对端口操作使用指令 in 或out 实现
通过60h号端口,CPU与键盘之间可以建立通讯
指令一:
in al, 60h ;从端口60h读取一个字节并存放到AL中.(60H并不是一个数据,应该当做一个内存地址)
该指令执行后,AL中的值为当前键盘上输入的键对应的编码
指令二:
in al,61h
or al, 80h ;使得最高位置1
out 61h, al ;把al的值输出到61端口
and al, 7Fh ;最高位置0
out 61h, al ;再次把al的值输出到61端口
这一系列代码是为了告诉CPU当前键盘输入的键已经被内存读取了,实现键盘与CPU的同步
指令三:
movax,0013h
int 10h ;切换显卡到320*200*256色图形模式,
int 10h 是一个BIOS中断,本身是一个函数,内部有很多in,out指令来实现CPU对设备的控制(简化工作量)
各个操作的层次顺序 : (底层)in/out ->bios 中断调用 ->dos中断调用 (上层,封装)
对应指令:
①in al,60h 端口层操作(使用户更直接有效地控制硬件,能读到的键最多,编程最麻烦)
②mov ah,0
int 16h ;bios层操作
③mov ah,1
int 21h ;dos层操作 (操作系统内核提供的函数,能读到的键最少,编程最简单)
(在WINDOWS中则把这两句中断调用换成API调用)
(dos不同于WINDOWS提供的函数有规范的命名,dos只给出函数的编号)
46.70h与71h端口与cmos内部的时钟有关
mov al,4
out70h,al ;index hour
inal,71h ;AL = hour(e.g. 19h means 19pm)
CMOS采用这种特殊时间表示方式(即BCD码)的原因:若用13h表示19点,则需要把十位和个位分离,需要用到除法
若用19h表示19点,则仅通过移位就可以把十位和个位分离(便于转化成字符串输出),操作:
(19h: 0001 1001 向右移4位即 0000 0001 即为该19h的十位数
47.汇编语言定义函数(仅标号+代码即可)如
convert:
push cx
mov ah,al
and ah,0Fh
mov cl,4
shr al,cl ; shr(shift right)右移4位 (CPU不允许把大于等于2的次数用常数表示,要先放在al中)
add ah,'0'
add al,'0'
pop cx
ret
48.指令 = 操作码 + 操作数(有的指令无操作数)
49.操作数寻址方式:
①.立即数方式(常数): add bx,1234h
②.
③.直接寻址
④.间接寻址(只有这四个寄存器能被方括号括起来当做地址)
[bx] [bp] [si] [di]
[bx+2] [bp+si] [bx+di] [bx+si+1] [bp+di-4] (这里不能随便选两个寄存器相加)
(形如[bx+si+1]这种用法可以表示结构数组中的某个成员)
(32位支持间接寻址的寄存器除了这几个外还有eax,ebx,edx,esp),开头加一个e即可)
50.复杂间接寻址的运用——结构数组的成员引用
structST{
char name[8];
int score;
}
struct STst[10];
假定要引用st[i].score
假设bx已经取到 &st[0]; bx是数组首地址
SI=sizeof(st[0])*i; i表示第i个元素,SI则是第i个元素相对于数组首地址的偏移量
则
mov ax,[bx+si+8] ; 等同于 ax = st[i].score ; +8是为了跳过8个字节的name
51.386及以上CPU的间接寻址方式:
(1)除了段寄存器,EIP,EFL外,几乎所有寄存器都可以放到[]中表示间接地址
[EBX][EBP] [ESI] [EDI] ....
(2) []里面可以出现*号
其中一个寄存器可以乘以2,4,8这三个常数中的一个(取决于数据类型的宽度)
long inta[10], i=3, x
mov eax, [ebx+esi*4] ; 等效于x = a[i] ( a-EBX, i-ESI )
52.数据传送指令
通用数据传送指令 :mov,push,pop,
注意:
mov byte ptr ds:[1234h],byte ptr ds:[100h] ;错误,两个操作数不能均为内存变量
mov cs,ax ;错误,因为cs不能通过mov改变(但其他段寄存器ds,es,ss都可以通过mov改)
同理,IP也不能通过mov改变,且IP不能直接访问(读取) (CS:IP指向将要执行的下一条指令)
53.堆栈相关寄存器
SS:SP 指向堆栈的顶端的元素,其中SS表示堆栈(Stack Segment)段地址寄存器,SP表示堆栈指针(StackPoint)寄存器
注意:在8086中每次只能压入或弹出16位,不能压入/弹出16位,因此 push ah是错误的
在32位机器中,每次压入弹出32位(4个地址值)
54.静态变量和全局变量存储于数据段Data Segment中,动态变量存储于堆栈段中
55.if( *pasc & 1<<(7-x) 语句转换成汇编语言
设 x = *pasc
mov cx,8 ; 循环变量
next:
shl x,1 ;在汇编语言中,左移一位移出去的最高位自动存放到CF(进位标志)中
jchas_dot
no_dot:
...
has_dot:
...画点
sub cx,1
jnz next
56.汇编指令:xchg ax,bx
能够把ax,bx的值相互交换(exchange)
特别的,指令 xchg ax,ax是无效果的,可以认为是nop指令(no operation)
57.破解过程中不能删除某一行(否则会把下面的代码提上来,改变代码的地址)
正确的方法应该填充nop
58.地址传送指令: LEA,LDS,LES
- lea dest,src
LEA: 载入当前地址,理解为取变量的偏移地址
lea dx,ds[1000h]
等效于 mov dx,1000h
leadx,[abc]
等效于 movdx,offset abc
特别的,要想dx=bx+si+3
不能写 movdx,bx+si+3(mov 逗号右边的值只能是常数或5*6等常数组成的表达式,因此要先分成三步相加再mov)
或者 lea dx,[bx+si+3]
因此 这里lea不可用mov来代替,可以简化步骤,进行计算后的赋值(与bx+si+3所指地址存放的值无关)
- lds/les dest,src
把远指针(既有段地址又含偏移地址的地址) 近指针(只包含偏移地址的地址,通常意义上的指针)
装到DS和dest里
远指针只是为了在16位环境下在段与段之间切换而已,在32位中通常不用切换,因此通常指用近指针
59.标志寄存器堆栈操作
pushf,popf 分别把标志寄存器FL的值分别压入,弹出
例如
pushf
popax ;ax=fl
or ax, 0100h 使AX中的第8位=1
pushax
popf ;FL=AX,FL中的第8位即TF位变为1
在32位中,对应的指令使pushfd,popfd
60.转换指令:
- 符号扩充指令: CBW,CWD
CBW:convert byte to word
CWD:convert word to double word
CDQ: convert double word to quadruple word (32位扩充为64位)
注意:cbw操作的只能是AL,即把AL放大成AX,前面补上去的8个位取决于AL的正负(正补0,负补1)
用例: mov al,7fh
cbw ; ax = 007fh
同样cwd操作的只能是AX(扩充的数字保存在dx中),cdq操作的只能是eax(扩充的数字保存在edx中)
此外,还可以用movzx(move by zero extention,零扩充)来扩充,但是源和目标无严格要求,比上面的指令要灵活
movsx(move by sign extention) 符号扩充
意义:符号扩展了才有足够的位数,因为有些指令要求两个操作数的位数是相同的
- 换码指令: XLAT (translate)也称查表指令
转换10进制位16进制:
chart[] = "0123456789ABCDE"
i = t[i]
设ds = 数组t的段地址
mov bx, offset t ;bx=表t的首地址
mov al,10 ; al为下标
xlat ;结果AL = 'A'
;实际AL =DS:[BX+AL]
xlat指令要求DS:BX指向数组,AL=数组下标
执行指令后,AL=数组元素
- 循环移位: rol,ror
ROL: 循环左移 (rotate left)
逗号右边的参数只能是寄存器cl
例子(十六进制转换为字符串输出)
(datasegment)
xyz dw0ABCDh,
abc dw"0000",0Ah,0Dh,"$"
t db"0123456789ABCDE"
要把xyz拆成ABCD输出
movbx,offset t
movdi,offset abc
push cx (因为下面要改变cl,所以要用堆栈保护cx)
mov cl,4
rol xyz.4
pop cx
movax,xyz
and ax,000fh ; 只要低4位
xlat ;al = ds:[bx+al]
movds:[abc],al
add di,1
sub cx,1
jmp...(循环)
61.算术指令
1)加法指令:add,inc,adc(带进位加)
inc ax(自加1)与add ax,1的区别: inc ax的执行速度更快,且产生的机器码更短
而且【inc ax】指令不影响进位标志cf(但影响zf),而add会影响cf
如add bx,ax
inc ax
jnc next
这里jnc是取决于add的结果而非inc的结果
adc (add with carry)主要用于大于0ffffh(16位)的大数相加
如要计算0f365h +0e024h (得数超过16位)
movax,0f365h
movdx,0002h
addax,0e024h
adc dx.5 ;两数的高16位相加再加上低16位相加产生的进位dx = 2 +5 + 1 = 8
dx:ax = 0008d389h
2)减法指令:sub,dec,sbb(subtract with borrow),
如求127546h减109428h
mov
3)neg(求相反数,negate) op
mov ah,2
negah ; ah = -2 = -2 0FEh
注意neg 与 not(逐位求反)差别与联系
neg x = (not x) + 1 C语言表述就是-x = ~x +1
4)cmp比较
如比较ax = 0ffffh,b = 00001h
cmp ax,bx
jg ax_greater_than_bx ;符号数的大于(这里b>a)
com ax,bx
ja ax_above_bx ;非符号数的大于(这里a>b)
cmp的结果决定于ax,bx是否是有符号的
同理,非符号的小于是jb(jump if below),小于等于是jbe,
有符号的小于是jl(jumpif less),小于等于是jle(就是在原指令后加上即可)
判断非符号数和有符号数的相等都是je,不相等都是jne
ja指令的依据是:cf=0(相减没有借位)且zf=0(不等于0)
jb指令实际上等价于jc指令(可换),因为它们的依据都仅仅是cf=1
jl跳转的依据是 SF!=OF(溢出标志)
jg跳转的依据是SF(符号标志,数值上等于结果的最高位)==OF
例如al=1,bl=2
cmp al,bl ;在内部进行al-bl后,若 SF=1,OF=0(未发生溢出,表示结论是错误的,相当于对SF的值进行肯定) -->al<bl
如al = 80h(负数), bl=7Fh(正数)
cmp al,bl ;若SF=0,OF=1(发生溢出,) --> al<bl
- 乘法指令 mul + 寄存器(乘数) (非符号数相乘)
几种模式: 8位*8位->16位 ; 16位*16位->32位 ; 32位*32位->64位
被乘数固定为al,乘积也固定为ax,即模式为AX = AL*src (src位数必须与al相同)
moval,10h
movbl,12h
mulbl ;ax = al*bl = 10h*12h = 0120h
其中mul后面的指令不允许是一个常数,只能是一个寄存器或变量(必须是db类型定义的变量)
16位乘法: dx:ax = ax * src ; 【注意这里的冒号:并不是段地址和偏移地址的标志,而是一个分隔符,高16位在dx,低16位在ax】
32位乘法: edx:eax = eax * src; (结果会自动存放到等号左边的两个寄存器edx和eax中)
原本imul是对符号数的相乘,386以上CPU对imul功能进行了扩充
imuleax,ebx,1234h ;eax = ebx * 1234h
imuleax,ebx ;eax = eax * ebx
imuleax,dword ptr [ebx+2], 1234h
- 除法指令:DIV,IDIV
div op 非符号数除法,商和余数用两个寄存器表示
16位被除数(规定除数必为8位,规定被除数必须存放在ax):
ax/8位 = 8位al(商) ... 8位ah(余数)
movax,123h
movbl,10h
divbl ; ax/bl = 123h/10h = 12h...3
; al = 12h, ah = 03h
32位被除数:
假定求5678h/1234h --> 00005678h/1234h
DX:AX / 16位 = AX商 .... DX余数
mov dx,0 ;这一步不能少,保证被除数的高16位 = 0;
movbx,100h
divbx; 00005678h/1234h = 3h....
除法溢出(不一定是除0)
movax,123h
mov bh,1
divbh ;123h/1 = 123h ... 0
此时由于al无法容纳16位的商,于是发生除法溢出
当CPU检测到运行过程中除法溢出时,会在该条除法指令之前插入一条int 00h中断指令并执行中断指令,不再往下执行
该条指令执行的效果取决于int00h中的函数是如何定义的(如dos环境下就会返回语句divide by zero)
62.je指令等价于jz指令(都取决于zf的值)
63.小数运算指令: fadd,fsub,fmul,fdiv
小数的定义:
pi dd(double word-32位,可用来定义float或longint) 3.14
r dq(quadruple word-64位,可用来定义double) 3.14159
CPU中有八个小数寄存器,分别为st(0),st(1)....st(7),每个寄存器宽度均达到8位(long double)
其中st(0)简称st,这些寄存器均可以在编程中使用
硬件本身只能实现整数运算,只能用整数计算,模拟小数运算
fld(float load) [abc]把小数的值载入到小数堆栈(即st0~7,但是一定会放到st(0)中,原来st中的元素依次往后移,类似于堆栈)
fstp(float store and pop) [result] 把最上面的一个小数(st(0)弹出到result中)
若写成fst则只会读取st(0)到result中而不会清除堆栈中的小数
64.逻辑运算(位运算)指令:and or xor not test
mov ah,3
and ah,6 ;逐位求与
not ah ;逐位求反
mov ah,3
test ah,1 ;test在内部把3和1做位运算与,但结果并不保存到ah中,但会影响标志位zf
test ah,80h ;检验ah的最高位是否等于1
test al,al (在C语言中很常用) 检测al是否为0
shl,shr是逻辑左移和右移(非符号数),补入的都是0
sal,sar是对符号数的逻辑左移和右移,最高位不会变
其中sal==shl???,因为都是往右边补0
sar分两种情况:正数补0,负数补1
注意:-1(1111 1111)用sar右移,还是-1(补进的是1)
rol循环左移(rotate left), ror循环右移
movah,1011 0110
rol ah,1 ;ah=0110 1101 同时CF=1
rcl 带进位循环左移, rcr带进位循环右移
movah,1011 0110B
clc ;cf = 0 移位前 0 1011 0100
rcl ah,1 移位后 1 0110 1000
相当于把cf看成是ah的最高位,把这个9位数做循环移位
movah,1011 0110B
stc ; cf=1 移位前 1 1011 0110 (stc->set up carry flag )
rcr ah,1 移位后 1 0110 1101
65.c语言中
>>是算术右移还是逻辑右移,取决于对象是有符号数还是无符号数(而且会先扩展成int)
如 unsigned chara = 0xFE
则 a >>=1 ;编译成shr
逻辑左移则都是在右边补0
此外,_rotl() ,_rotr() 两个函数可以实现循环左移和右移
66.查看C语言程序编译成的asm:
方法一:用调试器跟踪
为了在调试时快速定位main()函数,可以在main内部嵌入一条指令:
TC中写成 asm int3 ;debug程序会视其为一个断点
VC中写成 __asm int 3 双下划线
int 3机器码是CC(为了使得编译器能够方便替换源代码为中断指令),不同于其他任何int中断指令都是2个字节(如int 21 - CD03)
方法二:用编译器把.c编译成.asm: TCC -s hello.c
67.反调试:为了防止他人跟踪程序
方法: 读取某一段指令首字节(通常是核心代码)的机器码,检测其是否被改成CC(被设置了断点)
如果被改成CC则绕过断点提前结束程序
应对方法:在同一处设置连续的多个断点
反-应对方法:对一段核心代码的机器码求和(甚至MD5),可以检测该段代码是否有设置断点或被修改
应对反-应对方法:硬件断点(因为int 3产生的属于软件断点,很容易被反调试,)
原理 : 把某一段想要设置断点的地址取出来保存在寄存器
设置方法: ollydbg右击指令-断点-硬件执行(最多设置4个,是由CPU决定的)
反-应对反-应对方法:因为设置硬件断点保存的地址都被存在当前线程的Context.Dr0~Dr3里
利用windowsAPI的GetThreadContext(hThread,&context)获取DRx的地址:
CONTEXTcontext;
HANDLEhThread = GetCurrentThread();
context.ContextFlags= CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread,&context);
context.Dr0 = 0xFF ; //校验值
context.Dr1= 0xFE ;
context.Dr2= 0xFD ;
context.Dr3= 0xFC ;
SetThreadContext(hThread,context);
进行上述初始化之后,在程序中调用函数isDebuged()检测当前线程的context.Dr0~Dr3是否还是原来的值,
即可判断当前有无硬件断点
此外,还可以对变量地址设置硬件断点,当变量被读写都可以中断
可以先通过cheatEngine寻找出变量的地址,
在调试器中寻址到变量地址处,右键-断点-硬件写入-DWORD
这样就能在修改这个变量的指令处停下
此外,还可以对变量地址用硬件断点来检测数组越界
68.关于省略段地址寄存器:
一般缺省设置均把段地址视为ds
但只有[]内含有bp这个寄存器时,段地址才被默认视为 ss
左边宽度已知的情况下,可省略强制转换(dword ptr)等指令
69.字符串操作指令:
movs 字符串传送(mov string)
cmps 字符串比较
scas 字符串扫描
stos 存入字符串
lods 从字符串读取
70.字符串传送指令 movsb movsw movsd(只是单位不同)
[rep] movsb ; [rep](repeat)表示循环执行指令n次,n=cx
每一次传送的源地址:ds:si 目标地址:es:di
方向标志df=0设置源地址/目标地址的移动方向(字符串扫描方向)
单独的movsb所做的操作如下:
byte ptres:[di] = byte ptr ds:[si]
if( df==0){ si++; di++; }
else { si--; di--; } 该指令会自动更新目标和源地址
[rep] movsw 的操作过程:
again:
if( cx==0) goto done;
word ptres:[di] = word ptr ds:[si]
if( df==0) { si+=2; di+=2; }
else {si-=2; di-=2; }
cx--; //自动改变循环变量
done:
如果是反方向的movsb:
movax,1000h
mov ds,ax
mov cx,4
mov si,0
mov di,0
std ( setdirection = 1 )
[rep]movsb
71.字符串比较指令:cmpsb, cmpsw,, cmps
单独的指令比较ds:[si]和es:[di]的某一位是否相等,同时改变si和di
同样的,也可以在指令前加 repe(repeat if equal) 或者repne(repeat if not equal) 来进行循环比较 (最大循环次数由cx决定)
应用:
mov cx,6
mov df,0
repecmpsb
je equal ;两个串全部相等(只需要判断最后一次比较是否相等)
dec si
dec di ;返回到不相等的那一位
equal:
...
72.字符串扫描指令:scasb scasw
scasb(内部执行):
cmp al,es:[di] ;scasb操作的字符串地址必须由es:[di]指向,
di++ ;这里由当前df的值决定,当df=1时则为di--(方向标志位)
repe scasb:本次扫描相等的情况下继续下一次扫描, repne scasb则相反
73.字符串保存指令:stosb stosw(把AX的值填入dword ptres:[edi]中) stosd(把EAX的值填入dword ptres:[edi]中)
rep stosb的过程:(重复地把al的值填入es:[di])
again:
if( cx==0 ) goto done;
es:[di] = al;
di++;
cx--;
goto again;
done:
汇编语言实现memset(a,0,sizeof(a))假设a[102]
movecx,102
push ecx
shr ecx,2 ;ecx/=4,ecx=25(余下两个元素之后填
moveax,0
rep stosd
popecx
and ecx,3 ;实际上这个语句等于ecx%=4(能不用就不用除法,效率太低)
rep stosb ;填充剩余未填的两个字节
74.lodsb 从DS:SI所指向的内存单元中取出
当DF==0即为正方向,否则为负方向
没有repe lodsb的用法(无意义)
实例:设ds:si->"##AB#12#XY"
且es:di指向一个空的数组.通过编程读出其中的字母存入数组中
again:
lodsb ; al = es:[di]
cmp al,'#'
je next
stosb ;es:[di] =al,di++
next:
dec cx
jnz again
75.关于assume
assume只是对编译器编译程序起作用,并不能对段寄存器进行实际的赋值。
assume的目的是为了在编译时确实变量的段地址。
比如你在data段里定义了一个变量叫abc,然后你在
code段里引用了此变量,比如:
mov ah, [abc]
那么在编译的时候,编译器首先计算出abc的偏移地址假定为0,
然后还要确定abc的段地址data与哪个段寄存器对应。如果你
assumeds:data,那么,编译出来的结果是:
mov ah, ds:[0000]
如果你assume es:data,那么编译出来的结果是:
mov ah, es:[0000]
如果你assume ds:data, es:data,这里就有个优先的问题,ds的优先级高于es,所以编译出来的结果仍旧是:
movah, ds:[00]
assume不对段寄存器进行赋值的,只是帮助编译器确定变量的段地址。假定变量abc的偏移地址是0,段地址是data,那么在编译 mov ah,[abc]
这句话时,变量名abc肯定要被替换成实际的段地址及偏移地址 mov ah, data:[0000] 但是,我们知道,段地址data本身不能用来表示指令中的段
地址,必须用一个段寄存器来代替才行。所以,此时编译器需要选择ds、es、ss、cs中的其中一个来代替data。它的选择决定于你是怎么assume
的,而完全不管程序执行的时候这些段寄存器的值是否等于data(事实上编译的时候程序还没有运行,编译器怎么可能知道4个段寄存器的值?)。
因此,把 mov ah, [abc]编译成 movah, ds:[0000] 是因为你前面assume ds:data。如果你没有assume的话,编译会出错,因为变量所在段虽然是data,但是它
不知道该用哪个段寄存器来代替data。注意这里编译出来的 movah, ds:[0000] 中,ds这个段寄存器的值本身不会自动被赋值为data的。
76.控制转移指令:jmp
1)短跳(编译器自动根据距离决定类型)(jmpshort ptr)
jmp 0108h
若这条指令的偏移地址为0100h,则指令对应的机器码则为EB06,
因为机器码记录的是跳转的地址与jmp下一条指令的地址的差值
(原因:是为了使得一段代码复制到另外一段空间时仍可像原来一样工作)
且06是一个符号数,当其小于等于0x7Fh时则为正数,往下跳,大于等于0x80h则为负数,往上跳
EBFE则是jmp到当前这条指令,会形成死循环
补充:向前引用(在C语言中体现为:若一个函数要定义在主函数位置之后,则需在前面加函数的声明)
短跳编译时后面会产生一个nop,原因是编译器默认为近跳,分配给这条指令两个字节的机器码来存储跳跃距离
但是短跳的跳跃距离只需要一个字节的机器码存储,因此多余的那个机器码会写入90(nop)以保持分配的正确
2)近跳(jmp near ptr)
jmp 2000h
仍假设这条指令的偏移地址为0100h,则指令对应的机器码为E9FD1E
因为 2000h-0102h =1EFD
补充:近跳除了 jmp 标号 这种格式外,还可以使用以下两种格式
3)远跳(跨段跳跃)(jmp far ptr)
jmp1234h:5678h
机器码直接编码: EA78563412 段地址 偏移地址
这里机器码中的地址又称为远指针(既包含偏移地址又包含偏移地址,作为一个32位数保存在内存中)
注意:jc,jnc,jz,jnz,js,jns,jo,jno等条件跳转指令均为短跳
若这些指令后面跟的地址与当前指令相差太远,如
jb below
...(100h)
below:
编译会报错 jump out of range
解决方法:
jae above (jae: above and equal)
jmp below
above:
....(100h)
below:
唯一的以寄存器为条件的跳转语句jcxz dest 如果cx==0则跳转到dst
77.汇编文件的读取
movah,3Dh ;
mov al,0 ; 打开类型:read-only
int 21h
movah,3Eh
movbx,handle
int 21h ;关闭文件
78.文件的读写相关(C语言)
一般只要文件的字节的取值范围超过了ASCII码的范围,则属于二进制文件
用"rb"的方式读取文件,则把文件的每一个字节原封不动的读取,若用"r"读取,则会转换成字符串读取入,例如
源文件包含'A','B','C',0x0D,0x0A,用"r"读取只会读进'A','B','C',0x0A 用"rb"则会完整地读取
因此文本文件也可以用rb打开(其中的特殊字节可以自己处理)
获取文件的长度:利用函数fseek,lseek移动文件指针
有三种移动方式:(由第三个参数决定)
以0位 参照点(SEEK_SET=0),以EOF(文件最后一个字节的下一个位置)(SEEK_END=2),以文件当前的指针位置作为参照点(SEEK_CUR=1)
移动的距离可以是正负数(负数表示左移),其返回值为当前文件指针的位置
接下去调用ftell可以获得文件长度,fread(buf(存储的数组),sizeof(type),NumElems,fp)
79. 循环指令loop (还有loopz,loope,loopnz,loopne等等) 简化指令
1) loop dest循环
相当于两步操作的合成
dec cx
jnz again
注意,若循环前cx=0
则loop会执行0xffff+1次
因此通常要在loop前加 jcxz done
80. 子程序调用与返回指令: call retn(近返回) retf(远返回)
被调用的一段程序叫过程(procedure),类似于C语言中的函数,有时也叫子程序
其中retn表示近返回,可简写成ret,retf表示远返回
call: push 下一条指令的偏移地址(如果是远调用则需要先push段地址)
jmp 调用的函数的地址
ret: pop 堆栈中的首元素(即刚才存的偏移地址)到ip(如果是retf则需要再pop一个元素到cs)
81.C语言函数调用参数传递规则:从右到左压入堆栈, 由调用者负责清理参数
对于c = f(a,b);
对应的汇编代码:
push b
push a
call func
add sp,4 //函数调用完成后,传进 f 的参数已无用,由调用函数清理参数
右侧C程序对应的汇编代码(堆栈框架,Stack Frame)
push 20
push 10
call f
(pushoffset here)
here:
add sp,4
mov y,ax
f中:([bp+4]即为函数的首个形参)
push bp (使得堆栈又多了一层)
mov bp,sp
mov ax,[bp+4] (sp不可直接放进[],而bp与sp默认段地址均为ss,因此用bp代替sp)
addax,[bp+6]
mov sp,bp
pop bp
ret
C语言传递参数必须从右到左压栈的原因:
由于printf这个函数(可变参数函数)的存在, 必须要从右边开始压栈,使得栈顶元素是 char *format, 并由此来读取可变参数表中的参数
补充: pascal 的编译器采用从左到右传入参数的顺序, 而且由被调用者清理形参
windows api 采用 stdcall 的调用方式, 顺序与C语言一样,只是由被调用者清理形式参数
intprintf(char * format, ...){
double y = 0;
char *p;
int i = 0;
p = (char*)&format + sizeof(format);
while ( )
}
82. [bp-2] 表示一个函数的局部变量(存储在堆栈的上面)
intfunction(){
int c;
//这条语句等于 sub sp,4(4=sizeof(int) )
即相当于把当前栈顶上面一个(栈外的)元素作为c (因此c的地址一定是bp-xx)
这也就是C语言中局部变量未赋初值时, 变量会随机被赋值的原因
并且可以看出局部变量时存在于堆栈中的
因此汇编语言操作局部变量都是使用 mov [bp-4],ax sub [bp-8],1 等语句
而操作全局变量则是使用 mov 地址,ax sub 地址,1 等语句
}
相对的,静态变量或者全局变量存放在与data segment中定义的数据存放处,是程序开始就建立存储的,不是动态生成的
83.汇编语言递归函数
f:
push bp
mov bp,sp
mov ax,[bp+4] ax=首个元素
cmp ax,1
dec ax
push ax ;传递参数
call f
add sp,2 ;C语言由调用者清理参数
add ax,[bp+4] ; 即ax = f(n-1) + n
done:
pop bp
ret
注意: 栈顶的地址最小,
栈底(第一个压进去的元素)地址最大
....
1000:1FF0 here(16位地址,两个字节)
1000:1FF2 n-2
1000:1FF4 oldbp
1000:1FF6 here
1000:1FF8 n-1
1000:1FFA oldbp
1000:1FFC here(调用语句的下一句)
1000:1FFE n
84.中断向量表:
0:000 ~ 0:3ff 这一段地址存储每一个中断函数的地址(每一个中断的中断向量占4个字节)
如 第n号中断存储在 偏移地址4*n(十进制) 的内存中
int 21h, int 16h, int 10h等属于软件中断(由编程者写入程序而调用的), 相当于一个内核函数
int 8h时间中断, int 9h硬件中断(来自硬件的事件请求而调用的,在内存中看不到,即便写入程序也无意义), 相当于操作系统根据事件执行的一条指令
85.修改中断向量
cli
mov wordptr es:[bx],offset int8h
mov es:[bx+2],cs (这里cs表示int8h这个函数的段地址, 因此这里逗号右边也可以写成seg int8h 或 code
sti
86.中断函数与call调用的一般函数不同,必须把它用到的所有函数都保护起来(push),包括ax
因为中断函数的调用是无规律的, 可能在主函数执行到任何位置被调用,
87.中断调用完毕后, 需要发送一个信息到中断控制器, 使其知道本次中断已处理完, 下一次中断可以产生
88.关于溢出: 寄存器的范围指的是有符号数的范围,(例如al的范围是 -128~127)
如果将 计算得到的结果128用al来保存, 就会发生溢出( OF会变成1 ) , al就变成了-128, 显然不是正确的结果
这里还要注意CF与OF的区别, CF表示的进位(或借位), 是只针对无符号数运算的, 对有符号数的计算结果无意义, (有时也把CF称为无符号数的溢出标志)
同样, OF表示的溢出, 是只针对有符号数运算的, 用于无符号数运算的过程中无意义( 并且它们之间没有任何关系)
CPU在执行add指令的时候, 无所谓有符号数的计算还是无符号数的计算(计算结果是绝对的), 关键在于某个程序需要哪一种结果
最后
以上就是认真黑猫为你收集整理的<汇编语言程序设计> 课堂笔记的全部内容,希望文章能够帮你解决<汇编语言程序设计> 课堂笔记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复