美文网首页
流程转移与子程序

流程转移与子程序

作者: freemanIT | 来源:发表于2024-03-03 15:48 被阅读0次

    综述

    一般情况下, 指令顺序执行, 实际中, 常需要改变程序的执行流程

    这就用到转移指令

    • 可以控制cpu 执行内存中某处代码的指令
    • 可以修改ip, 或同时修改cs 和ip 的指令

    分类

    按行为分类

    • 段内转移: 只修改ip, 如jmp ax
    • 段间转移: 同时修改cs 和ip, 如jmp 1000:0

    根据指令对ip 修改范围不同分类

    • 段内短转移: ip 修改范围-128~127
    • 段内近转移: ip 修改范围-32768~32767

    按转移指令分类

    • 无条件转移, jmp
    • 条件转移指令, jcxz
    • 循环指令, loop
    • 过程
    • 中断

    操作符offset

    用offset 取得标号的偏移地址

    格式: offset 标号

    assume cs:code
    code segment
    start:
      mov ax,offset start ;相当于mov ax,0
      s: movax,offset s ;相当于, mov ax,3
     code ends
     end start
    

    jmp 指令

    无条件转移, 可以只修改ip, 也可以同时修改cs和ip

    jmp 指令要给出两种信息

    • 转移目的地址
    • 转移距离
      • 段间转移(远转移), jmp 2000:1000
      • 段内短转移: jmp short 标号, ip 修改范围-128~127, 8 位的位移
      • 段内近转移: jmp near ptr, ip 修改范围-32768~32767, 16 位的位移

    jmp 指令是依据位移进行转移的

    jmp short 指令中, 包含的是跳转到指令的相对位置, 而不是转移的目标地址

    assume cs:code
    code segment
      start:
        mov ax,0
        jmp short s
        add ax,1
        nop
        nop
      s:inc ax
    code ends
    end
    
    jmp根据位移转移

    执行顺序

    1. (ip)=0003, cs:ip 指向EB 05, jmp 的机器码
    2. 读取指令码EB 05 进入指令缓冲器
    3. (ip)=(ip)+多读取指令长度=(ip)+2=0005, cs:ip 指向add ax,0001
    4. cpu 指向指令缓冲器里的指令EB 05
    5. 指令EB 05 执行后, (ip)=(ip)+05=000AH, cs:ip 指向inc ax

    两种段内转移

    短转移

    jmp short 标号

    功能: (ip)=(ip)+8 位位移

    原理:

    1. 8 位位移="标号" 处的地址-jmp指令后的第一个字节的地址
    2. short 指明此处的位移为8 位位移
    3. 8 位位移的范围-128~127, 用补码表示
    4. 8 位位移由编译程序在编译时算出

    近转移

    jmp near ptr 标号

    功能: (ip)=(ip)+16位位移

    原理:

    1. 16 位位移="标号" 处的地址-jmp指令后的第一个字节的地址
    2. near ptr 指明此处的位移为16 位位移, 进行的是段内近转移
    3. 16 位位移的范围-32768~32767, 用补码表示
    4. 16 位位移由编译程序在编译时算出

    远转移

    远转移jmp far ptr 标号 近转移jmp near ptr 标号
    far ptr 指明了跳转的目的地址, 包含了标号的段地址cs 和偏移地址ip near ptr 指明了相对于当前ip 的转移位移, 而不是转移的目的地址
    far ptr转移 jmp转移地址 地址010B指令 near ptr转移 jmp 位移地址 位移转移

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

    格式 jmp 16 位寄存器

    功能: ip=(16位寄存器)

    例如: jmp ax

    jmp word ptr 内存单元地址 jmp dword ptr 内存单元地址
    从内存单元地址处开始存放着一个字, 是转移的目的偏移地址 从内存单元地址处开始存放两个字, 搞地质是转移地址的目的段地址, 低地址存放偏移地址
    一个字的偏移地址 两个字的偏移地址

    jmp 指令小结

    格式 示例
    jmp 标号 - 段间转移(远转移): jmp far ptr 标号
    - 段内短转移: jmp short 标号, 8 位的位移
    - 段内近转移: jmp near ptr 标号, 16 位的位移
    jmp 寄存器 jmp bx; 16 位的位移
    jmp 内存单元 段内转移: jmp word ptr 内存单元地址; jmp word ptr [bx]
    段间转移: jmp dword ptr 内存单元地址; jmp dword ptr [bx]

    其他转移指令

    jcxz 指令

    格式: jcxz 标号

    功能:

    如果(cx)=0, 则转移到标号处执行

    当(cx)≠0, 程序向下执行

    jcxz 是有条件的转移指令

    所有的有条件转移指令都是短转移

    对ip 的修改范围都为-128~127

    在对应的机器码中包含转移的位移, 而不是目的地址

    loop 指令

    格式: loop 标号

    操作:

    1. (cx)=(cx)-1

    2. 当(cx)≠0, 转移到标号处执行

      (cx)=0, 程序向下执行

    如果(cx)≠0, (ip)=(ip)+8位位移

    • 8 位位移="标号" 处的地址 - loop 指令后的第一个字节地址
    • 8 位范围为-128~127, 补码表示
    • 8 位位移由编译程序在编译时算出

    call 指令和ret 指令

    调研子程序: call 指令

    返回: ret 指令

    mov ax,0
    call s
    mov ax,4c00h
    int 21h
    
    s:add ax,1
    ret
    

    实质: 流程转移指令, 它们都修改ip, 或同时修改cs 和ip

    call 指令

    实质: 流程转移, 实现转移的方法和jmp 指令的原理类似

    过程:

    1. 将当前的ip 或cs 和ip 压入栈中
    2. 转移到标号处执行指令

    call 标号

    • 16 位位移="标号" 处地址 - call 指令后的第一个字节地址
    • 16 位范围-32768~32767, 补码表示
    • 位移由编译程序编译时计算

    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 标号
    

    "call 标号" 类似于"jmp near ptr 标号", 对应的机器指令中为相对于当前ip 的转移位移, 而不是转移的目的地址, 实现段内转移. "call far ptr 标号" 实现的是段间转移

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

    格式: call 16位寄存器

    功能:

    • (sp)=(sp)-2
    • ((ss)*16+(sp))=(ip)
    • (ip)=(16位寄存器)

    相当于

    • push ip
    • jmp 16位寄存器

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

    call word ptr 内存单元地址

    相当于:

    • push ip
    • jmp word ptr 内存单元地址
    mov sp,10h
    mov ax,0123h
    mov ds:[0],ax
    call word ptr ds:[0]
    执行后, (ip)=0123h, (sp)=0Eh
    

    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
    

    返回指令, ret 和retf

    ret retf
    功能 用栈中的数据, 修改ip 内容, 从而实现近转移 用栈中的数据, 修改cs 和ip 内容, 从而实现远转移
    相当于 pop ip pop ip
    pop cs
    举例 ret指令 retf指令

    举例:

    assume cs:code,ss:stack
    stack segment
      db 8 dup(0)
      db 8 dup(0)
    stack ends
    
    code segment
      start:
        mov ax,stack
        mov ss,ax
        mov sp,16
        mov ax,1000
        call s
        mov ax,4c00h
        int 21h
      s:
        add ax,ax
        ret
    code ends
    end start
    
    执行过程1 执行过程2

    mul 乘法指令

    格式:

    mul 寄存器

    mul 内存单元

    8位乘法 16位乘法
    被乘数(默认) al ax
    乘数 8位寄存器或内存字节单元 16位寄存器或内存单元
    结果 ax dx高位和ax低位
    mul bl; --(ax)=(al)(bl)
    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位

    模块化程序设计

    • 调用子程序: call 指令
    • 返回: ret 指令
    • 子程序: 根据提供的参数处理一定的事务, 处理后, 将结果(返回值) 提供给调用者

    参数和结果传递的问题

    例如, 计算N 的3 次方

    参数N 存放位置, 计算结果放在哪里?

    方案

    1. 寄存器传递参数
    2. 内存单元传递参数
    3. 用栈传递参数

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

    • 参数存放到bx 中, (bx)=N
    • 子程序中用多个mul 指令计算N 的3 次方
    • 将结果放到dx 和ax 中, (dx:ax)={N^3}

    遇到的问题, 如果传递更多参数, 寄存器不够了怎么办?

    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
      mov di,16
      
      mov cx,8
    s:
      mov bx,[si]
      call cube
      mov [di],ax
      mov [di].2,dx
      add si,2
      add di,4
      loop s
    cube:
      mov ax,bx
      mul bx
      mul bx
      ret
    
      mov ax,4c00h
      int 21h
    code ends
    end start
    

    内存单元批量传递数据

    • 将普亮数据放到内存中, 然后将他们坐在内存空间的首地址放在寄存器中, 传递给子程序
    • 对于具有批量数据的返回结果, 也可以用同样的方法
    assume cs:code
    data segment
      db 'conversation'
    data ends
    
    code segment
    start:
      mov ax,data
      mov ds,ax
      mov si,0
      
      mov cx,12
      call capital
      mov ax,4c00h
      int 21h
    capital:
      and byte ptr [si],11011111b
      inc si
      loop capital
      ret
      
    code ends
    end start
    

    用栈传递参数

    原理: 由调用者将徐亚传递给子程序的参数压入栈中, 子程序从栈中取得参数

    任务: 计算(a-b)^3, 将a 和b 为word 型数据

    • 进入子程序前, 参数a, b 入栈
    • 调用子程序, 将使栈顶存放ip
    • 结果(dx:ax)=(a-b)^3
    用栈传递参数

    计算(a-b)^3

    用栈传递参数执行过程

    计算器冲突问题

    conversation 转换为大写, 需要将循环次数设置为cx,12, 每次不固定, 可以写为db 'conversation',0

    做如下循环

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

    有例子如下将下面字符串装换为大写

    assume cs:code
    data segment
      db 'word',0
      db 'unix',0
      db 'wind',0
      db 'goog',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:
      push cx
      push si
    change:
      mov cl,[si]
      mov ch,0
      jcxz ok
      and byte ptr [si],11011111b
      inc si
      jmp short change
      ret
    ok:
      pop si
      pop cx
      ret
    code ends
    end start
    

    在以上代码中使用了栈来存储参数, 另一种方式是使用cx 来直接存储循环次数, 部分代码如下

    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
      ret
    ok:
      ret
    

    在内循环中也同样使用cx, 会造成冲突, 重复使用cx 一个寄存器

    而使用栈来存储, 就不用关心子程序使用了哪些寄存器, 也不会造成寄存器冲突

    子程序标准框架如下

    子程序开始:
        子程序中使用的寄存器入栈
        子程序内容
        子程序使用的寄存器出栈
        返回
    

    在子程序的开始, 将要用到的所有寄存器中的内容都保存起来, 在子程序返回前再恢复

    标志寄存器

    PSW/FLAGS 别称, 程序状态字

    结构:

    • flag 寄存器是按位起作用的, 就是说, 它的每一位都有专门的含义, 记录特定信息
    • 8086 中没有使用flag 的1, 3, 5, 12~15 位, 这些位不具有任何含义

    作用:

    • 用来存储相关指令的某些执行结果
    • 为cpu 执行相关指令提供行为依据
    • 控制cpu 相关工作方式

    直接访问标志寄存器的方法

    • pushf: 将标志寄存器的值压栈
    • popf: 栈中弹出数据, 送入到标志寄存器
    标志寄存器说明

    ZF 零标志(zero flag)

    标记相关指令计算结果是否为0

    • zf=1, 表示结果为0, 1表示逻辑真
    • zf=0, 表示结果不是0, 0 表示逻辑假
    指令 结果
    mov ax,1
    and ax,0
    zf=1, 表示结果是0
    mov ax,1
    or ax,0
    zf=0, 结果非0
    • 在8086 指令集中, 有的指令执行是影响标志寄存器的, 比如: and, sub, mul, div, inc, or, add, 大都是运算指令, 进行逻辑或算术运算
    • 有的指令执行对标志寄存器没有影响, 比如, mov, push, pop, 大都是传送指令
    • 使用一条指令的时候, 要注意指令的全部功能, 其中敖阔执行结果对标志寄存器的哪些标志位造成影响

    PF 奇偶标志(parity flag)

    指令执行后, 结果的所有二进制中1 的个数

    • 1 的个数为偶数, pf=1
    • 1 的个数为奇数, pf=0
    指令 结果
    mov al,1
    add al,10
    结果为0000 1011B = 0000 0001B + 0000 1010B; 其中有3(奇数)个1, 则PF=0
    mov al,1
    or al,2
    结果为00000011B = 0000 0001B or 0000 0010B; 其中有2(偶数)个1,则PF=1

    SF 符号标志(sign flag)

    指令执行后, 将结果视为有符号数

    • 结果为负, sf=1
    • 结果为非负, sf=0
    指令 结果
    mov al,10000001B
    add al,1
    al 为10000010B; 为负数,则SF=1
    sub ax,ax ax为0,为非负数, sf=0

    该标志是cpu 对有符号数运算结果的一种记录, 将数据当做有符号数来运算的时候, 通过sf 可知结果的正负, 将数据当做无符号数来运算, sf 的值则没有意义, 虽然相关的指令影响了它的值

    CF 进位标志(carry flag)

    进行无符号数运算的时候, cf 记录了运算结果的最高有效位向更高位的进位值, 或从更高位的借位值

    执行指令后

    • 有进位或借位, cf=1
    • 无进位或借位, cf=0
    指令 结果
    mov al,98H
    add al,al
    (al)=30H, CF=1, CF记录了最高有效位向更高位的进位值
    add al,al (al)=60H,CF=0, CF记录了最高有效位向更高位的进位值
    sub al,98H (al)=C8H, CF=1, CF记录了向更高位的借位值

    OF 溢出标志(overflow flag)

    进行有符号数运算的时候, 如结果超过了机器所能表示的范围称为溢出

    指令执行后

    • 有溢出, of=1
    • 无溢出, of=0
    指令 结果
    mov al,98
    add al,99
    (al)=197,超出了8位有符号数的范围(-128~127), OF=1
    mov al,0F0H
    add al,88H
    (al)=(-16)+(-120)=-136, 有溢出, OF=1

    cf 和of 区别

    • cf 是对无符号数运算有意义的进/借位标志位
    • of 是对有符号数运算有意义的溢出标志位

    带进(借) 位加减法

    adc 带进位加法指令

    adc 利用cf 位上记录的进位值

    格式: adc 操作对象1,操作对象2

    功能: 操作对象1=操作对象1+操作对象2+cf

    例如: adc ax,bx 实现的功能, (ax)=(ax)+(bx)+cf

    指令 mov al, 98h
    add al,al
    adc al,3
    mov ax,1
    add ax,ax
    adc ax,3
    mov ax,2
    mov bx,1
    sub bx,ax
    adc ax,1
    结果 (ax)=34h (ax)=5 (ax)=4
    解释 adc 执行后, 计算为(ax)+3+cf=30h+3+1=34h (ax)+3+cf=2+3+0=4 (ax)+1+cf=2+1+1=4)

    adc 大数相加

    adc大数相加例子

    两个128 位数据相加, 逆序相加

    data segment
    dw 0A452H,0A8F5H,78E6H,0A8EH,8B7AH,54F6H,0F04H,671EH
    dw 0E71EH,0EF04H,54F6H,8B7AH,0A8EH,78E6H,58F5H,0452H
    data ends
    
    128位数据相加

    其中使用了sub ax,ax, 因为可以将标志寄存器的值置为默认, inc di 不影响进位标志位

    sbb 带借位减法指令

    格式: sbb 操作对象1,操作对象2

    功能: 操作对象1=操作对象1-操作对象2-cf

    例如: sbb ax,bx; (ax)=(ax)-(bx)-cf

    cmp 指令

    格式: cmp 操作对象1,操作对象2

    功能: 计算操作对象1–操作对象2

    应用: 其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果

    指令 cmp ax,ax mov ax,8
    mov bx,3
    vmp ax,bx
    功能 做(ax)–(ax)的运算, 结果为0, 但并不在ax中保存, 仅影响flag的相关各位 (ax)=8,(bx)=3
    标志寄存器 ZF=1 PF=1 SF=0 CF=0 OF=0 ZF=0 PF=1 SF=0 CF=0 OF=0

    无符号数比较与标志位取值

    通过cmp 指令执行后相关标志位的值, 可以看出比较结果

    cmp ax,bx

    比较关系和标志寄存器结果

    有符号数比较与标志位取值

    cmp ah,bh

    有符号数比较和标志位取值

    条件转移指令

    条件转移指令表

    举例

    jxxx 系列指令和cmp 指令配合, 构造条件转移指令

    • 不必再考虑cmp 指令对相关标志位的影响和jxxx 指令对相关标志位的检测
    • 可以直接考虑cmp 和jxxx 配合使用表现出来的逻辑含义

    配合使用实现高级语言中的if 语句功能

    1 如果(ah)=(bh), 则(ah)=(ah)+(ah), 否则(ah)=(ah)+(bh)

    cmp ah,bh
    je s
    add ah,bh
    jmp short ok
    s:
    add ah,ah
    ok:
    ret
    ====================
    if(a==b){
    a=a+a
    }
    else{
    a=a+b
    }
    

    2 如果(ax)=0, 则(ax)=(ax)+1

    ;ax 获得值
    add ax,0
    jnz s
    inc ax
    s:...
    ====================
    if(a==0){
    a++;
    }
    

    条件转移指令应用

    指令: jxxx--je/jna/jae..

    可以根据某种条件, 决定是否转移重新执行流程, "转移"=修改ip

    如何检测

    • 通过检测标志位, 由标志位体现条件
    • 条件转移指令通常都和cmp 相配合使用, cmp 指令改变标志位

    示例

    data segment
    db 8,11,8,1,8,5,63,38
    data ends
    

    统计数值为8 的字节个数

    思路:

    初始设置(ax)=0, 然后使用循环依次比较每个字节的值, 找到一个和8 相等的数将ax 的值加1

    assume cs:code
    data segment
      db 8,11,8,1,8,5,63,38
    data ends
    
    code segment
    start:
      mov ax,data
      mov ds,ax
      mov ax,0
      mov bx,0
      mov cx,8
      
    s:
      cmp byte ptr [bx],8
      jne next
      inc ax
    
    next:
      inc bx
      loop s
      
      mov ax,4c00h
      int 21h
    code ends
    end start
    

    DF 标志和传送指令

    方向标志位(Direction Flag)

    功能

    • 在串处理指令中, 控制每次操作后si, di的增减
    • DF = 0: 每次操作后si, di递增
    • DF = 1: 每次操作后si, di递减

    对df 位进行设置的指令

    • cld指令: 将标志寄存器的DF位设为0(clear)

    • std指令: 将标志寄存器的DF位设为1(setup)

    转送指令1: movsb

    以字节为单位传送

    1. ((es)×16 + (di)) = ((ds) ×16 + (si))

    2. 如果DF = 0则:

      (si) = (si) + 1

      (di) = (di) + 1

      如果DF = 1则:

      (si) = (si) - 1

      (di) = (di) - 1

    传送指令2: movsw

    功能, 以字为单位传送

    1. ((es)×16 + (di)) = ((ds) ×16 + (si))

    2. 如果DF = 0则:

      (si) = (si) + 2

      (di) = (di) + 2

      如果DF = 1则:

      (si) = (si) - 2

      (di) = (di) - 2

    data segment
      db 'Welcome to masm!'
      db 16 dup(0)
    start:
      mov ax,data
      mov ds,ax
      mov si,0
      mov es,ax
      mov di,16
      cld
      mov cx,16
     s:
      movsb
      loop s
      mov ax,4c00h
      int 21h
     code ends
     end start
    

    rep指令

    rep 指令常和串传送指令搭配使用

    功能: 根据cx 的值, 重复执行后面的指令

    rep movsb == s: movsb, loop s

    rep movsw==s: movsw, loop s

    例如

    用串传送指令, 将F000H 段中的最后16 个字符复制到data 段中, 最后一个字符位置: F000:FFFF

    assume cs:code,ds:data
    data segment
      db 16 dup(0)
    data ends
    
    code segment
    start:
      mov ax,0f000h
      mov ds,ax
      mov si,0ffffh
      mov ax,data
      mov es,ax
      mov di,15
      mov cx,16
      std
      rep movsb
      
      mov ax,4c00h
      int 21h
    code ends
    end start
    
    移动最后16位字符到data段

    相关文章

      网友评论

          本文标题:流程转移与子程序

          本文链接:https://www.haomeiwen.com/subject/cjnkzdtx.html