我是靠谱客的博主 认真黑猫,最近开发中收集的这篇文章主要介绍<汇编语言程序设计> 课堂笔记,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

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  (FF2相加产生了进位,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,jejz都会执行

但与减法运算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.70h71h端口与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

  1. 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所指地址存放的值无关)

  1. lds/les  dest,src

把远指针(既有段地址又含偏移地址的地址) 近指针(只包含偏移地址的地址,通常意义上的指针)

装到DSdest

远指针只是为了在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.转换指令:

  1. 符号扩充指令: 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) 符号扩充

 

意义:符号扩展了才有足够的位数,因为有些指令要求两个操作数的位数是相同的

 

 

  1. 换码指令: 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=数组元素

 

 

  1. 循环移位: 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),

如求127546h109428h

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

 

  1. 乘法指令 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;    (结果会自动存放到等号左边的两个寄存器edxeax)

 

原本imul是对符号数的相乘,386以上CPUimul功能进行了扩充

imuleax,ebx,1234h    ;eax = ebx * 1234h

imuleax,ebx                ;eax = eax * ebx

imuleax,dword ptr [ebx+2], 1234h 

 

  1. 除法指令:DIV,IDIV

div op 非符号数除法,商和余数用两个寄存器表示

16位被除数(规定除数必为8,规定被除数必须存放在ax):

ax/8  =  8al() ... 8ah(余数)

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,可用来定义floatlongint)  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在内部把31做位运算与,但结果并不保存到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

利用windowsAPIGetThreadContext(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]的某一位是否相等,同时改变sidi

同样的,也可以在指令前加 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本身不能用来表示指令中的段

地址,必须用一个段寄存器来代替才行。所以,此时编译器需要选择dsessscs中的其中一个来代替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表示近返回,可简写成retretf表示远返回

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不可直接放进[],bpsp默认段地址均为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)

如果将 计算得到的结果128al来保存, 就会发生溢出( OF会变成1 ) , al就变成了-128, 显然不是正确的结果

这里还要注意CFOF的区别, CF表示的进位(或借位),  是只针对无符号数运算的, 对有符号数的计算结果无意义, (有时也把CF称为无符号数的溢出标志)

同样, OF表示的溢出, 是只针对有符号数运算的, 用于无符号数运算的过程中无意义( 并且它们之间没有任何关系)

CPU在执行add指令的时候, 无所谓有符号数的计算还是无符号数的计算(计算结果是绝对的), 关键在于某个程序需要哪一种结果

最后

以上就是认真黑猫为你收集整理的<汇编语言程序设计> 课堂笔记的全部内容,希望文章能够帮你解决<汇编语言程序设计> 课堂笔记所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(36)

评论列表共有 0 条评论

立即
投稿
返回
顶部