• 汇编学习笔记(9) -- CALL和 RET指令


         子程序描述
         提示
     
     
    call 和 ret 指令都是转移指令,它们都修改IP,或同时修改CS 和 IP
    经常被用来实现子程序的设计
     
    ret 和 ret
    ret指令用栈中的数据,修改IP的内容,从而实现近转移
     
    retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移
     
     
    CPU执行ret指令时,进行下面两步操作:
    (1) (IP) = ( (ss) * 16 + (sp))
    (2) (sp) = (sp) + 2
    CPU执行retf指令时,进行下面4步操作:
    (1) (IP) = ( (ss)* 16 + (sp) )
    (2) (sp) = (sp) + 2
    (3) (CS) = ( (ss) * 16 + (sp) )
    (4) (sp) = (sp) + 2
    可以看出,如果我们用汇编语法来解释ret和retf指令,则:
    CPU执行ret指令时,相当于进行:
    pop IP
     
    CPU执行retf指令时,相当于进行:
    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
    code ends
    end start
    下面的程序中,retf 指令执行后,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
     
    问题:补全程序,实现从内存1000:0000处开始执行内存
    assume cs:code
    stack segment
        db 16 dup (0)
    stack ends
    
    code segment
        start:
        mov ax,stack
        mov ss,ax
        mov sp,16
        mov ax,____
        push ax
        mov ax,_____
        push ax
        retf
    code ends
    end start
     
    答案:
    assume cs:code
    stack segment
        db 16 dup (0)
    stack ends
    
    code segment
        start:
        mov ax,stack
        mov ss,ax
        mov sp,16
        mov ax,1000h
        push ax
        mov ax,0
        push ax
        retf
    code ends
    end start
     
    call指令
    cpu执行call指令时,进行两步操作
    (1) 将当前的IP 或 CS和IP 压入栈中
    (2)转移
     
    call指令不能实现短转移
    call实现转移的方法和原理和jmp指令的原理相同
     
     
     
    依据位移进行 转移的call指令
    call 标号(将当前的IP压栈后,转到标号处执行指令)
    CPU 执行此种格式的call指令时,进行如下操作:
    (1) (sp) = (sp) - 2
    ( (ss) * 16 + (sp) ) = (IP)
    (2) (IP) = (IP) +16位 位移
     
    16位 位移 = 标号处的地址 - call指令后的第一个字节的地址;
    16位 位移的范围为-32768~32767,用补码表示;
    16位 位移由编译程序在编译时算出。
     
    如果我们用汇编语法来解释此种格式的call指令 则:
    CPU执行“call 标号”时,相当于进行:
    push IP
    jmp near ptr 标号
     
    例题:
    执行后ax的数值为6
     
     
    转移的 目的地址在指令中的call指令
    上面的call指令,其对应的机器指令中没有转移的目的地址
    而是相对于当前IP的转移位移
     
    call far ptr 实现的是段间转移
     
    cpu执行的此种格式的call指令时,进行如下操作
    (1) (sp) = (sp) - 2
    ( (ss) * 16 + (sp) = (CS)
    (sp) = (sp) - 2
    ( (ss) * 16 + (sp) ) = (IP)
     
    (2) (CS) - 标号所在段的段地址
    (IP) - 标号在段中的偏移地址
     
    如果我们用汇编语法来解释此种格式的call指令 则:
    CPU执行 “call far ptr 标号” 时,相当于进行:
    push
    push IP
    jmp far ptr 标号
     
    例题:
    执行后ax的数值为1010h
     
     
     
     
     
     
    转移地址在寄存器中的call指令
    指令格式: call 16位 reg
    功能:
    (sp)=(sp)-2
    ((ss)* 16+(sp)=(IP)
    (IP)=(16位reg)
    用汇编语法来解释此种格式的call 指令,CPU执行“call 16位reg”时
    相当于进行:
    push IP
    jmp 16位reg
     
    问题:
    下列程序进行后,ax中的值为多少
    答案
    push IP
    jmp word ptr 内存单元地址
     
    转移地址在内存中的call指令
    转移地址在内存中的call指令有两种格式
    (1) call word 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]
    执行后
    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
     
     
    问题
    下面的程序执行后,ax中的数值为多少
    (用call指令原理分析,不要用debug执行,中断会导致结果不一致)
    assume cs:code
    stack segment
            dw 8 dup (0)
    stack ends
    
    code segment
            start:
            mov ax,stack
            mov ss,ax
        mov sp,16
        mov ds,ax
        mov ax,0
        call word ptr ds:[0eH]
        inc ax
        inc ax
        inc ax
        
        mov ax, 4c00h
        int 21h
    code ends
    end start
     
     
    答案
    ax = 3
     
     
     
     
    call 和 ret 的配合使用
     
    问题
    下面程序返回前,bx中的值为多少
    assume cx: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
     
    思考后看分析
     
     
     
     
    分析
    CPU执行这个程序的主要过程
    (1) CPU将call s指令的机器码读入,IP指向了call s后的指令mov bx,ax,
    然后CPU执行calls指令,将当前的IP值(指令movbx,ax的偏移地址)压栈
    并将IP的值改变为标号s处的偏移地址;
     
    (2) CPU 从标号s处开始执行指令,loop 循环完毕后,(ax)=8;
     
    (3) CPU 将ret指令的机器码读入,IP 指向了ret指令后的内存单元
    然后CPU执行ret指令,从栈中弹出一个值(即call s先前压入的mov bx,ax 指令的偏移地址)送入IP中
    则CS:IP指向指令mov bx,ax
     
    (4) CPU从mov bx,ax开始执行指令,直至完成
     
    程序返回前,(bx)=8
    可以看出,从标号s到ret的程序段的作用是计算2的N次方,计算前,N的值由cx提供
     
    我们再来看下面的程序:
    assume cs:code
    stack segment
        db 8 dup (0)            1000:0000  00 00 00 00 00 00 00 00
        db 8 dup (0)            1000:0008  00 00 00 00 00 00 00 00
    stack ends
    
    code segment
        start:
        mov ax,stack                1001:0000  B8 00 10
        mov ss,ax               1001:0003  8E D0
        mov sp,16                1001:0005  BC 10 00
        mov ax,1000                1001:0008  B8 E8 03
        call s                    1001:000B  E8 05 00
      
        mov ax, 4c00h           1001:000E  B8 00 4C
        int 21h                      1001:0011  CD 21
        
        s:
        add ax,ax                1001:0013  03 C0
        ret                        1001:0015  C3
    code ends
    end start
    看一下程序的主要执行过程
     
     
    (1) 前3条指令执行后,栈的情况如下:
     
    (2) call 指令读入后,(IP)=000EH,CPU指令缓冲器中的代码为: E8 05 00;
    然后,(IP) = (IP) + 0005 = 0013H
    0005是call的下一个指令的IP 与 标号中第一条指令所在IP 的距离
     
    (3) CPU 从cs:0013H处(即标号s处)开始执行
     
    (4) ret 指令读入后:
    (IP)=0016H,CPU指令缓冲器中的代码为: C3
    CPU执行C3,相当于进行pop IP,执行后,栈中的情况为:
     
    (5)CPU回到cs:000EH处(即call指令后面的指令处)继续执行
     
     
     
    从上面的讨论中我们发现,可以写一个具有一定功能的程序段,我们称其为子程序
     
    在需要的时候,用call 指令转去执行。
    可是执行完子程序后,如何让CPU接着call 指令向下执行?
    call 指令转去执行子程序之前,call 指令后面的指令的地址将存储在栈中
     
    所以可在子程序的后面使用ret 指令,用栈中的数据设置IP的值
    从而转到call指令后面的代码处继续执行
     
    这样,我们可以利用call 和ret来实现子程序的机制。子程序的框架如下。
     
    标号:
    指令
    ret
     
    具有子程序的源程序的框架如下:
    assume cs:code
    code segment
        main: 
        :
        call sub1      ;调用子程序sub1
        :
        :
        mov ax,4c00h
        int 21h
        
        
        sub1:          ;子程序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中存放
     
    格式如下
    mul reg
    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
    mov al,100
    mov bl,10
    mul bl
    (2) 计算 100*10000
    mov ax,100
    mov bx,10000
    mul bx
     
     
    参数和结果传递的问题
    子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者
    其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值
     
    比如,设计一个子程序,可以根据提供的N,来计算N的3次方
    这里面就有两个问题:
    (1) 将参数N存储在什么地方?
    (2)计算得到的数值,存储在什么地方?
     
     
    很显然,可以用寄存器来存储,可以将参数放到bx中;
    因为子程序中要计算N*N*N,可以使用多个mul指令,为了方便,可将结果放到dx和ax中
     
    子程序如下。
    cube:
        mov ax,bx
        mul bx
        mul bx
        ret
     
    用寄存器来存储参数和结果是最常使用的方法
    对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:
    调用者将参数送入参数寄存器,从结果寄存器中取到返回值
    子程序从参数寄存器中取到参数,将返回值送入结果寄存器
     
    编程,计算data段中第一组数据的3 次方,
    结果保存在后面一组dword单元中
    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
     
    批量数据的传递
    前面的例子中,子程序cube只有一个参数,放在bx中
    如果有两个参数,那么可以用两寄存器存放
    可是如果需要传递的数据有3个、4个或更多直至N个,该怎样存放呢?
     
    在这种时候,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序
     
    对于具有批量数据的返回结果,也可用同样的方法。
     
    下面看一个例子,设计一个子程序,功能:将一个全是字母的字符串转化为大写。
     
    这个子程序需要知道两件事,字符串的内容和字符串的长度。
    因为字符串中的字母可能很多,所以不便将整个字符串中的所有字母都直接传递给子程序。
    但是,可以将字符串在内存中的首地址放在寄存器中传递给子程序
    因为子程序中要用到循环,我们可以用loop指令,而循环的次数恰恰就是字符串的长度
    出于方便的考虑,可以将字符串的长度放到cx
     
     
    例子
    编程:将data段中的字符串转换为大写
    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      ;cx存放字符串的长度
     
        call cap
        
        mov ax,4c00h
        int 21h
        
        cap:
        and byte ptr [si],11011111b
        inc si
        loop cap
        ret
    code ends
    end start
     
     
    寄存器冲突问题
    设计一个子程序
    功能:将一个全是字母,以0结尾的字符串,转化为大写
     
     
    程序要处理的字符串以0作为结尾符,这个字符串可以如下定义:
    db ' conversation' ,0
     
     
    应用这个子程序,字符串的内容后面一定要有一个0,标记字符串的结束
    子程序可以依次读取每个字符进行检测,如果不是0,就进行大写的转化,如果是0,就结束处理
     
    由于可通过检测0而知道是否已经处理完整个字符串,所以子程序可以不需要字符串的长度作为参数
    可以用jcxz 来检测0
     
    capital:
        mov cl,[si]
        mov ch,0
        jcxz ok                        ;如果(cx) = 0,结束;如果不是0,处理
        and byte ptr [si],11011111b    ;将ds:si所指单元中的字母转化为大写
        inc si                         ;ds:si指向下一个单元
        jmp short capital
        
        ok:ret
     
    来看一下这个子程序的应用
    (1) 将data段中字符串转化为大写
    assume cs:code
    data segment
        db 'conversation' ,0
    data ends
     
    代码段中的相关程序段如下
    mov ax,data
    mov ds,ax
    mov si,0
    call capital
     
    (2)将data段中的字符串全部转化为大写。
    assume cs:code
    data segment
        db 'word',0
        db 'unix',0
        db 'wind',0
        db 'good',0
    data ends
     
    可以看到,所有字符串的长度都是5(算上结尾符0),使用循环,重复调用子程序capital,完成对4个字符串的处理
    完整的程序如下。
    assume cs:code
    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
        s:
        mov si,bx
        call capital
        add bx, 5
        loop s
        
        mov ax,4c00h
        int 21h
    
        capital:
        mov cl,[si]
        mov ch,0
        jcxz ok
        
        and byte ptr [si] , 11011111b
        inc si
        jmp short capital
        
        ok: ret
    code ends
    end start 
     
    这个程序思路是正确,但是细节上有错误
     
     
    问题在于cx的使用,主程序要使用cx记录循环次数
    可是子程序中也使用了cx,在执行子程序的时候,cx中保存的循环计数值被改变,使得主程序的循环出错
     
    从上面的问题中,实际上引出了一个一般化的问题:
    子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突
     
    那么如何来避免这种冲突呢?
    在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复
    可以用栈来保存寄存器中的内容
     
    以后,我们编写子程序的标准框架如下:
     
     
    重新改进下子程序capital的设计
    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
     
    实验10
    编写3个子程序
     
    显示字符串
    编写一个通用的子程序来实现这个功能
    我们应该提供灵活的调用接口,使调用者可以决定显示的位置(行、列)、内容和颜色
     
    子程序描述
    名称: show_ str
    功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串。
    参数: (dh) = 行号(取值范围0~24)
    (dI) = 列号(取值范围 0~79)
    (cl)=颜色,ds:si指 向字符串的首地址
    返回:无
    应用举例:在屏幕的8行3列,用绿色显示data段中的字符串
    assume cs:code
    data segment
        db 'Welcome to masm!',0
    data ends
    
    code segment
        start: 
        mov dh, 8
        mov dl,3
        mov cl, 2
        mov ax, data
        mov ds, ax
        mov si, 0
        call show_str
       
         mov ax, 4c00h
        int 21h
        
        show_str:
        :
        :
    code ends
    end start
    提示
    内存地址空间中,B8000H~BFFFFH 共32KB的空间,为80X25彩色字符模式的显示缓冲区
    偏移 000~09F 对应显示器上的第1行(80个字符占160个字节);
    偏移 0A0~13F 对应显示器上的第2行;
    偏移 140~1DF 对应显示器上的第3行;
     
    例:在显示器的0行0列显示黑低绿色的字符串 'ABCDEF'
    ('A'的ASCII码值为41H, 02H表示黑底绿色)
     
    显示缓冲区里的内容为:
     
     
    可以看出,在显示缓冲区中,偶地址存放字符,奇地址存放字符的颜色属性
     
     
     
    答案
    assume cs:code
    data segment
            db 'welcome to masm!',0
    data ends
    
    code segment
    start: 
        mov dh,12
                mov dl,13
                mov cl,1
                mov ax,data
                mov ds,ax
                mov si,0
                call show_str
                mov ax,4c00h
                int 21h
            
    show_str: 
        push dx
        push cx
        push si
                    
        mov di,0            ;显示缓存区中的偏移量
        mov bl,dh    
        dec bl            ; bl-1才是真正的行,因为行号从0开始计数
        mov al,160  
        mul bl            ; 每行160字节 用 行数*每行偏移量 得到目标行的偏移量
        mov bx,ax           ; mul bl之后,乘积存储在ax中,这里要转存入bx中
        mov al,2            ; 列的偏移量为2,两个字节代表一列!!!
        mul dl            ; 与行偏移量同理
        add bl,al            ;将列偏移量与行偏移量相加,得到指定位置的偏移量。
        
        mov ax,0b800h
        mov es,ax        ;指定显示缓存区的内存位置
        
        mov al,cl            ; 由于后面jcxz语句的判断要用到cx,所以我们要将
                    ; cl(颜色)先存下来。
    s:     
        mov ch,0
        mov cl,ds:[si]         ;首先将当前指向字符串的某个字符存入cx中
        jcxz ok            ; 如果cx为0,则转移到ok标号执行相应代码
        mov es:[bx+di],cl        ;将字符传入低地址
        mov es:[bx+di+1],al    ; 将颜色传入高地址
        add di,2            ; 列偏移量为2
        inc si            ; 字符串的偏移量为1
        loop s            ; 不为0,继续复制
    
    ok:     
        pop dx        
        pop cx
        pop si            ; 还原寄存器变量
        ret            ; 结束子程序调用
    code ends
    end start

    参考: 王爽 - 汇编语言 和 小甲鱼零基础汇编

    https://www.cnblogs.com/nojacky/p/9523904.html

  • 相关阅读:
    线程的资源释放(一)
    iOS开发完整项目
    iOS开发多线程技术方案
    Windows 7 Beta泄漏版存在安全问题 狼人:
    工信部:黑客入侵等是网络安全防护工作的重点 狼人:
    微软下周2将发布13个补丁 修复26个安全漏洞 狼人:
    调查显示互联网14%SSL认证不安全 狼人:
    09年恶意软件放缓 2010年共享最危险 狼人:
    IE曝新安全漏洞 千万网民隐私遭受威胁 狼人:
    安全关注:2009年信息安全八大预测 狼人:
  • 原文地址:https://www.cnblogs.com/ZhouJiaHao/p/13807946.html
Copyright © 2020-2023  润新知