body, table{font-family: 微软雅黑; font-size: 13.5pt} table{border-collapse: collapse; border: solid gray; border-width: 2px 0 2px 0;} th{border: 1px solid gray; padding: 4px; background-color: #DDD;} td{border: 1px solid gray; padding: 4px;} tr:nth-child(2n){background-color: #f8f8f8;}
call 和 ret 指令都是转移指令,他们都修改 IP , 或同时修改 CS 和 IP 。经常被用来实现子程序设计。
ret 指令用栈中的数据( 弹出一个数据 ),修改 IP 内容实现近转移
retf 指令用栈中数据( 弹出两个数据 ),修 改 CS 和 IP 的内容,实现远转移
; CPU 执行 ret 指令 1、(IP)=((SS)*16+(SP)) 2、(sp)=(sp)+2 相当于: pop IP |
; CPU 执行 retf 指令 1、(IP)=((SS)*16+(SP)) 2、(SP)=(SP)+2 3、(CS)=((SS)*16+(SP)) 4、(SP)=(SP)+2 相当于: pop IP pop CS |
;程序执行 ret 指令后, (IP)=0,CS:IP指向代码段第一条指令
assume cs:code
stack segment
db 16 dup (0)
stack ends
code segment
mov ax , 4c00h
int 21h
start: mov ax , stack
mov ss , ax
mov sp , 16
mov ax , 0
push ax
mov bx , 0
ret ; ip 被设置为0,指令转移到mov ax , 4c00
code ends
end start
assume cs:code
code segment
mov ax , 4c00h
int 21h
start:
mov ax , 0
push ax
mov bx , 0
ret
code ends
end start ; 一样的效果
|
;程序执行 reft 后,CS:IP 指向代码段第一条指令
assume cs:code
stack segment
db 16 dup (0)
stack ends
code segment
mov ax , 4c00h
int 21h
start: mov ax , stack
mov ss , ax
mov sp , 16
mov ax , 0
push cs
push ax
mov bx , 0
retf
code ends
end start
assume cs:code
code segment
mov ax , 4c00h
int 21h
start:
mov ax , 0
push cs
push ax
mov bx , 0
retf
code ends
end start
|
call 指令
1、将当前的 IP 或 CS 和 IP 压入栈中;
2、转移
call 指令不能实现短转移,除此之外,call指令实现转移的方法和 jmp 指令的原理相同。
依据位移进行转移的 call 指令
指令:call 标号 (将当前 IP 压栈后,转到标号处执行指令)
CPU 执行这种指令进行如下操作
1、(SP)=(SP)-2 ((SS)*16+(SP))=(IP)
2、(IP)=(IP)+16 位位移
CPU 执行 “call 标号”时,相当于: push IP jmp near ptr 标号 |
16位位移=“标号”处的地址-call 指令后的第一个字节的地址;范围 -32768~32767 ;16位位移是编译程序在编译的时候算出来的。
内存地址 | 机器码 | 汇编指令 |
076A:0 076A:3 076A:6 076A:7 |
B8 00 00 E8 01 00 40 58 |
mov ax , 0 call s ; 执行时 call 0007 inc ax s: pop ax |
当 IP=3的时候指向 call s ;执行这条指令的时候 IP 偏移到 6,也就是下一条指令;call s == push IP jmp near ptr 标号 此时栈中只有一个数据6,最后pop ax , 所以ax=6; |
转移的目的地址在指令中的 call 指令
“call far ptr 标号” 实现的是段间转移。 1、(SP)=(SP)-2 ((SS)*16+(SP))=(CS) (SP)=(SP)-2 ((SS)*16+(SP))=(IP) |
2、 (CS)=标号所在段的段地址 (IP)=标号在段中的偏移地址 相当于进行: push CS push IP jmp far ptr 标号 |
内存地址 | 机器码 | 汇编指令 |
076A:0 076A:3 076A:8 076A:9 076A:A |
B8 00 00 9A 09 00 6A 07 40 58 5B |
mov ax , 0
call far ptr s ; call 076A:0009
inc ax
s: pop ax
pop bx
|
执行call far ptr s 的时候,cpu把下一条指令的位置 CS:IP =076A:0008 压入栈中保存,然后计算到标号 s 的偏移,转到标号处执行;此时出栈 ax=IP=0008,bx=CS=076A; |
转移地址在寄存器中的 call 指令
指令格式:call 16 位寄存器 功能:(SP)=(SP)-2 ((SS)*16+(SP))=(IP) (IP)=(16位寄存器) |
相当于进行: push IP jmp 16位寄存器 |
内存地址 | 机器码 | 汇编指令 |
076A:0 076A:3 076A:5 076A:6 076A:8 076A : B |
B8 00 00 FF D0 40 8B EC 03 46 00 58 |
mov ax , 6
call ax
inc ax
s: mov bp , sp
add ax , [bp] ; ax=000B , 这里取的是ss:bp内存里的值,因为sp==bp,所以是堆栈顶数据5,5+6=B
pop ax ; ax=0005
|
call ax ; 指令执行时当前 IP 偏移到 inc ax 即:IP=5;进栈;指令跳转到IP=6处执行,ax=6+栈顶元素=000B;出栈,ax =5 |
转移地址在内存中的 call 指令
1、call word ptr 内存单元地址 相当于: push IP jmp word ptr 内存单元地址 mov sp , 10h
mov ax , 0123h
mov ds:[0] , ax
call word ptr ds:[0] ;执行这条指令,IP进栈,栈中存放000D
执行后:(IP)=0123H,(SP)=0EH
|
2、call dword ptr 内存单元地址 相当于: push CS push IP jmp dword ptr 内存单元地址 mov sp , 10h
mov ax , 0123h
mov ds:[0] , ax
mov word ptr ds:[2] , 0
call dword ptr ds:[0]
执行后:(CS)=0 , (IP)=0123h,(sp)=0Ch |
call 和 ret 的配合使用
assume cs:codesg
codesg segment
start: mov ax , 1
mov cx , 3
call s ; 栈中进栈(IP),执行到这行指令的时候,IP指向mov bx , ax
mov bx , ax ;(bx)=8Z
mov ax , 4c00h
int 21h
s: add ax , ax
loop s
ret ;从栈中取一个数当做ip(刚好指向mov bx , ax)
codesg ends
end
|
mul 指令
乘法指令
1、两个相乘的数:两个数相乘要么都是8位,要么都是16位。如果都是8位,一个默认放在 AL 中,另外一个放在 8 位寄存器或内存字节单元中;如果是16位,一个默认放在 AX 中,另外一个放在16位寄存器或内存字单元中。 | 2、结果:如果是8位乘法,结果默认放在 AX 中;如果是16位乘法,结果默认高位放在 DX ,低位在 AX 中存放。 |
指令格式: mul reg mul 内存单元 |
内存单元可以用不同的寻址方式给出,eg: mul byte ptr ds:[0] 含义:(ax)=(al)*((ds)*16+0) mul word ptr [bx+si+8] 含义:(ax)=(ax)((ds)*16+(bx)+(si)+8) ; 结果的低16位 (dx)=(ax)((ds)*16+(bx)+(si)+8) ; 结果的高16位 |
; 计算 100*10 ; 两个数都小于255,可以做8位乘法 mov al , 100 mov bl , 10 mul bl 结果:(ax)=1000(03E8H) |
; 计算 100*10000
; 10000大于255,必须做16位乘法
mov ax , 100
mov bx , 10000
mul bx
结果:(ax)=4240H , (dx)=000FH ; F4240H=1000000
|
模块化程序设计
call 和 ret 指令支持汇编语言编程中的模块化设计。
参数和结果传递的问题
子程序一般要根据提供的参数处理一定的事务,处理后将结果(返回值)提供给调用者。即:如何存储子程序需要的参数和产生的返回值。
assume cs:codesg , ds:datasg
datasg segment
dw 1,2,3,4,5,6,7,8 ;(16个字节(00))
dd 0,0,0,0,0,0,0,0 ;(32个字节(00) )
datasg ends
codesg segment
start: mov ax , datasg
mov ds , ax
mov si , 0 ;偏移0指向第一个数1
mov di , 16 ;偏移16指向存放结果内存单元的首地址
mov cx , 8 ; 8个数,进行8次循环
s: mov bx , [si]
call cube
mov [di] , ax ;把结果的低16位放到低位两个字节(00 01)
mov [di].2 , dx ;把结果的高16位放到高位的两个字节(02 03)
add si , 2 ;偏移2个单位取到下一个数2
; ds:si 指向下一个 word 单元
add di , 4 ;偏移到下一个存储结果的内存地址(04 05 06 07)
; ds:di 指向下一个 dword 单元
loop s
mov ax , 4c00h
int 21h
cube: mov ax , bx ; 对拿到的数进行立方运算
mul bx
mul bx
ret
codesg ends
end start
|
// 程序初始状态
// 程序运行两次,也就是得出1的三次方(00 00 00 01)和2的三次方(00 00 00 08) // 改写程序的初始第一个值为9 9的三次方等于 2D9 , 低16位存放2D9 (D9 02), 高16位存放0(00 00) //第一个数是9运行一次循环的内存情况 //第一个数是100(64H),100^3=F4240H,低位4240H放在低16位(40 42),F放在高16位(0F 00) |
批量数据的传递
如果子程序要传递多个参数,寄存器有限?
可以将批量数据放到内存中,然后将他们的内存空间首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可以用同样的方法。
assume cs:codesg , ds:datasg
datasg segment
db 'conversation'
datasg ends
codesg segment
start: mov ax , datasg
mov ds , ax
mov si , 0 ; ds:si指向字符串(批量数据)所在空间的首地址
mov cx , 12 ; cx 存放字符串的长度
call capital
mov ax , 4c00h
int 21h
capital:and byte ptr [si] , 11011111b
inc si
loop capital
ret
codesg ends
end start
|
除了用寄存器传递参数外,还有一种通用的方法就是用栈来传递参数。 |
寄存器冲突的问题
;改进上面的程序,字符串后面加一个0表示结束,就可以不用cx assume cs:codesg , ds:datasg
datasg segment
db 'conversation' , 0
datasg ends
codesg segment
start: mov ax , datasg
mov ds , ax
mov si , 0
call capital
mov ax , 4c00h
int 21h
capital:mov cl , [si] ; 当读到最后一个的时候为 0
mov ch , 0
jcxz ok ; 读到最后 (cx)=0
and byte ptr [si] , 11011111b
inc si
jmp short capital
ok: ret ; 出栈,设置cs:ip指向 mov ax , 4c00h
codesg ends
end start
|
|
◆编写调用子程序的程序的时候不必关心子程序到底使用了哪些寄存器;
◆编写子程序的时候不必关心调用者使用了哪些寄存器
◆不会发生寄存器冲突
;编写程序将 data 段中的字符串全部转化为大写 assume cs:code , ds:data
data segment
db 'word' , 0
db 'unix' , 0
db 'wind' , 0
db 'good' , 0
data ends
code segment
start: mov ax, data
mov ds , ax
mov bx , 0
mov cx , 4 ; 设置循环次数4
s: mov si , bx
call capital ; 调用子程序处理字符串
add bx , 5 ; 偏移量加 5 ,处理下一个字符串
loop s
mov ax , 4c00h
int 21h
capital: push cx ; 执行子程序防止有冲突寄存器,先压栈保存主程序寄存器值
push si
change: mov cl , [si]
mov ch , 0
jcxz ok ; 判断(cx)是否等于0,如果等于0表示读完一个字符串
and byte ptr [si] , 11011111b
inc si
jmp short change
ok: pop si ; 结束了一次子程序的调用,恢复主程序的相应寄存器的值
pop cx
ret ; 返回call下一行指令,执行下一次循环
code ends
end start
|