汇编语言(王爽)-CALL和RET指令


callret指令都是转移指令,都可修改IP,或同时修改CSIP,它们经常被共同用来实现子程序的设计


ret和retf

ret指令用栈中的数据,修改IP的内容,从而实现近转移

retf指令用栈中的数据,修改CSIP的内容,从而实现远转移

ret指令操作:

  1. (IP)=((ss)*16+(sp))

  2. (sp)=(sp)+2

retf指令操作:

  1. (IP)=((ss)*16+(sp))

  2. (sp)=(sp)+2

  3. (CS)=((ss)*16+(sp))

  4. (sp)=(sp)+2


call指令

call指令的两步操作:

  1. 将当前的IPCSIP压入栈中

  2. 转移

call指令不能实现短转移


依据位移进行转移的call指令

格式:call 标号(将当前IP压栈后,转到标号处执行指令)

call指令的操作:

  1. (sp)=(sp)-2

    ((ss)*16+(sp))=(IP)

  2. (IP)=(IP)+16位位移

16位位移=标号处的地址-call指令后的第一个字节的地址

16位位移的范围-32768–32767,以补码表示

16位位移由编译程序在编译时算出


转移的目的地址在指令中的call指令

格式:call far ptr 标号,实现段间转移

操作:

  1. (sp)=(sp)-2

    ((ss)*16+(sp))=(CS)

    (sp)=(sp)-2

    ((ss)*16+(sp))=(IP)

  2. (CS)=标号所在段的段地址

    (IP)=标号在段中的偏移地址


转移地址在寄存器中的call指令

格式:call 16位reg

操作:

  1. (sp)=(sp)-2

  2. ((ss)*16+(sp))=(IP)

  3. (IP)=(16位reg)


转移地址在内存中的call指令

格式:

  1. call word ptr 内存单元地址

    操作:

    1. (sp)=(sp)-2

    2. ((ss)*16+(sp))=(IP)

    3. (IP)=(内存单元地址)

    相当于进行:

    1
    2
    push IP
    jmp word ptr 内存单元地址
    1
    2
    3
    4
    5
    6
    mov sp,10h
    mov ax,0123h
    mov ds:[0],ax
    call word ptr ds:[0]

    执行后,(IP)=0123H,(sp)=0EH
  1. call dword ptr 内存单元地址

    操作:

    1. (sp)=(sp)-2

      ((ss)*16+(sp))=(CS)

      (sp)=(sp)-2

      ((ss)*16+(sp))=(IP)

    2. (CS)=(内存单元地址+2)

      (IP)=(内存单元地址)

    相当于进行:

    1
    2
    3
    push CS
    push IP
    jmp dword ptr 内存单元地址
    1
    2
    3
    4
    5
    6
    7
    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的配合使用

使用callret实现子程序的机制

程序返回前,bx的值?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:code
code segment

start: mov ax,1
mov cx,3
call s
mov bx,ax
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret

code ends
end start

分析:

  1. CPUcall s指令的机器码读入,此时IP指向call s后的指令mov bx,axCPU执行call s指令,将当前IP值(指令mov bx,ax的偏移地址)压栈,并将IP的值改变为标号s处的偏移地址

  2. CPU从标号s处执行指令,loop循环完成后,(ax)=8

  3. CPUret指令的机器码读入,此时IP指向ret指令后的内存单元。CPU执行ret指令,从栈中弹出一个值(即call s先前压入栈的mov bx,ax指令的偏移地址)送入IP中,则CS:IP指向指令mov bx,ax

  4. CPUmov bx,ax开始执行指令,直至完成

结论

可以写一个具有一定功能的程序段,称之为子程序

在需要执行子程序时,用call指令转去执行,此时,会将call指令后面的指令的地址存储在栈中。当子程序执行完成后,在子程序后面使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码处继续执行

子程序框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
assume cs:code
code segment
main:
...
call sub1 ;调用子程序sub1
...
mov ax,4c00h
int 21h

sub1:
...
call sub2 ;调用子程序sub2
...
ret ;子程序返回

sub2:
...
ret ;子程序返回

code ends
end main

mul指令

乘法指令

注意:

  1. 两个乘数:

    必须同为8位或同为16位

    若为8位,一个默认放在al中,另一个放在8位reg或内存字节单元中

    若为16位,一个默认放在ax中,另一个放在16位reg或内存字单元中

  2. 结果:

    若为8位乘法,结果默认在ax

    若为16位乘法,结果高位默认在dx中存放,低位在ax中存放

格式:

  1. mul reg

  2. mul 内存单元

    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位

例:

  1. 计算100*10

    分析:100和10小于255,可做8位乘法

    1
    2
    3
    mov al,100
    mov bl,10
    mul bl

    结果:(ax)=1000(03E8H)

  2. 计算100*10000

    分析:100小于255,但10000大于255,所以必须做16位乘法

    1
    2
    3
    mov ax,100
    mov bx,10000
    mul bx

    结果:(ax)=4240H,(dx)=000FHF4240H=1000000


模块化程序设计

利用callret指令,可以用简捷的方法,实现多个互相联系、功能独立的子程序来解决一个复杂的问题


参数和结果传递的问题

子程序一般都要根据提供的参数处理一定的
事务,处理后,将结果(返回值)提供给调用者

讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值

用寄存器来存储参数和结果是最常用的方法

对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作相反:调用者将参数送入参数寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入结果寄存器

计算data段中第一组数据的3次方,结果保存到后一组dword单元中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
assume cs:code
data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends

code segment
start: mov ax,data
mov ds,ax
mov si,0 ;ds:si指向第一组word单元
mov di,16 ;ds:di指向第二组dword单元

mov cx,8
s: mov bx,[si]
call cube ;调用子程序,进行3次方运算
mov [di],ax
mov [di].2,dx
add si,2 ;ds:si指向下一个word单元
add di,4 ;ds:di指向下一个dword单元
loop s

mov ax,4c00h
int 21h

cube: mov ax,bx
mul bx
mul bx
ret

code ends
end start

批量数据的传递

当进行传递的数据过多时,寄存器的数量有限,不可能简单的用寄存器来存放多个需要传递的数据。对于返回值,也有同样的问题

可以使用内存空间,将批量数据放到内存中,然后将数据所在的内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可使用同样的方法

编程,将字母字符串转换大写字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
assume cs:code

data segment
db 'conversation'
data ends

code segment
start: mov ax,data
mov ds,ax
mov si,0 ;ds:si指向字符串内存空间首地址
mov cx,12
call capital
mov ax,4c00h
int 21h

capital:and byte ptr [si],11011111b ;转换为大写字母
inc si ;ds:si指向下一个字母内存空间
loop capital
ret
code ends
end start

寄存器冲突的问题

编程,将一个全是字母,以0结尾的字符串,转换为大写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
assume cs:code
data segment
db 'word',0 ;以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个字符串,循环4次
s: mov si,bx
call capital ;进入子程序
add bx,5 ;切换至下一个字符串
loop s ;循环4次

mov ax,4x00h
int 21h

capital:mov cl,[si] ;开始对字符串进行循环
mov ch,0
jcxz ok ;若(cx)=0,跳转ok。若不为0,继续向下执行
and byte ptr [si],11011111b ;字母转换大写
inc si ;指向下一个字母
jmp short capital ;转到capital处继续执行
ok: ret ;子程序返回
code ends

end start

此程序中存在问题,在于cx,在主程序中使用了cx记录循环次数,可在子程序中也使用了cx,并且改变了cx的值,会使得主程序的循环出错

一般化问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突

设想:

  1. 编写要调用子程序的程序的时候不必关心子程序到底使用了哪些寄存器

  2. 编写子程序的时候不必关心调用者使用了哪些寄存器

  3. 不会发生寄存器冲突

解决方案:在子程序的开始将子程序中所用到的寄存器中的内容都保存起来,在子程序返回前再恢复。可以使用栈来保存寄存器中的内容

改进子程序capital

1
2
3
4
5
6
7
8
9
10
11
12
13
capital:push cx
push si

change: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change

ok: pop si
pop cx
ret

编写子程序的标准框架:

1
2
3
4
5
子程序开始:
子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret、retf)

编写子程序

解决除法溢出的问题

在使用div指令做除法的时候,会出现:结果的商过大,超出了寄存器所能存储的范围。这将引起CPU的一个内部错误,称为除法溢出

子程序描述

名称:divdw

功能:进行不会产生溢出的除法运算,被除数为dword型,除数为word型,结果为dword

参数:

  1. (ax)=dword型数据的低16位

  2. (dx)=dword型数据的高16位

  3. (cx)=除数

返回:

  1. (dx)=结果的高16位

  2. (ax)=结果的低16位

  3. (cx)=余数

提示

公式:

X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N

X:被除数,范围[0,FFFFFFFF]

N:除数,范围[0,FFFF]

HX高16位,范围[0,FFFF]

LX低16位,范围[0,FFFF]

int():描述性运算符,取商

rem():描述性运算符,取余数

此公式将可能产生溢出的除法运算X/N转变为多个不会产生溢出的除法运算

公式等号右边的所有除法运算都可以用div指令来做,肯定不会导致除法溢出

---------------The End---------------
0%