汇编学习笔记(九)
转移指令
转移指令是重要的汇编指令,一般有无条件转移指令和条件转移指令,无条件转移指令是直接转移到目的处执行,条件转移指令是根据条件转移,如前jcxz,当cx=0条件满足时才转移到标号处执行。
JMP指令
jmp是一种典型的无条件转移指令,jmp指令可以只修改IP,也可以同时修改CS和IP。
jmp 按照转移的距离分为:
1、段内短转移,对IP修改的范围为:-128-127,即存储jmp指令转移偏移地址使用1B单元存储,段内短转移可以用jmp short 标号
来明确。
2、段内近转移,对IP修改范围为:-32768 - 32767 偏移地址使用2B(word)单元存储,近转移可以用jmp near 标号
明确
3、段间转移,同时修改CS和IP,使用jmp far ptr 标号
jmp 可以有以下形式:
1、jmp 标号
转移到标号处执行。
2、jmp 寄存器
转移到寄存器中存储的偏移地址处执行,注意此处寄存器是16位寄存器,相当于IP=寄存器。
3、jmp 内存地址
转移到内存存储的偏移地址处执行。段内转移可以使用jmp word ptr [0]
,段间转移用jmp dword ptr [0]
其中IP=低位word,CS=高位word。
4、jmp 段地址:偏移地址
转移到段地址:偏移地址处执行指令,如下:
.....
jmp end
.....
end:
mov ax, 4c00H
int 21h
CALL和RET指令
CALL和RET指令也是转移指令的一种。
RET指令
RET指令包括两条指令ret和retf,两者之间有一定的区别,执行ret指令时,CPU相当于做以下动作:
- pop ip ;将栈顶元素弹出到IP寄存器中,以实现近转移
执行retf指令相当于以下动作:
- pop ip ;弹出第一个元素到IP寄存器中。
- pop cs ;弹出第二个元素到CS寄存器
可以看出ret和retf的区别主要在于是否修改CS段寄存器,CS:IP决定了指令执行的顺序,所以使用ret/retf指令可以实现程序的转移,如下所示,程序中转移到0000:7c00H处执行:
...... ; 程序中其他指令
mov ax, 0
push ax ; 段地址0压栈
mov ax, 7c00H
push ax ; 偏移地址压栈
retf
CALL指令
CALL指令如call 标号
相当于以下动作:
- push IP
- jmp 标号
当然实现段间转移,可以使用call far pr 标号
,相当于以下动作:
- push cs
- push ip
- jmp far ptr 标号
CALL指令如jmp指令也有call 寄存器
或者call 内存地址
形式,相关方式也类似,简略。
子程序
CALL指令和RET指令配合即可实现子程序,将自己的汇编程序按功能分成不同的子程序,有利于更好的组织。
1、call 子程序标号
即可转到子程序处执行。
2、子程序执行结束后通过ret,又可返回到主程序调用处后继续执行。
子程序示例(nasm片段):
org 0x1000
base_of_stack equ 0x7c00 ; 栈基地址
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, base_of_stack
; 调用子函数清屏
; ====================
call sub_clear_scr
mov si, msg_hello
call sub_print_str
loader_end: hlt
jmp short loader_end
; 以下静态数据
; ==========================
msg_hello db 'Hello world! ', 0x00
; 以下为子函数
; ===========================
sub_clear_scr:
; 调用BIOS 10h中断清屏
; ========================
push ax
push bx
push cx
push dx
mov ax, 0x0600 ; ah: 子功能号 06H - 向上滚屏, al: 滚动的行数,0-清除窗口
mov bx, 0x0700 ; bh: 空白区域的属性
mov cx, 0 ; ch:cl 窗口的左上角位置 行:列
mov dx, 0x184f ; dh:dl 出口的右下角位置 行:列, 屏幕默认是 25行×80列16色模式,即 19H*50H
int 10h
pop dx
pop cx
pop bx
pop ax
ret
sub_print_str:
; 在显存中写入数据来输出字符。
; ========================
; ds:si 指向字符串首地址, 字符串以0结尾
push ax
push si
push di
mov ax, es
push ax
mov ax, 0xb800
mov es, ax
mov di, 0
_loop_sps_read_char:
mov al, [si]
mov ah, 0x07
cmp al, 0
jz _label_sps_end
mov [es:di], ax
inc si
add di, 2
jmp _loop_sps_read_char
_label_sps_end:
pop ax
mov es, ax
pop di
pop si
pop ax
ret
编写子函数需要注意几个问题:
1、子程序中一般会变更寄存器,为了不影响调用程序的正常运行,一般在子程序内将要用到的寄存器先push到堆栈中,子程序结束时再pop出来。
2、子程序怎样传参和传递返回结果?一般通过寄存器来传递参数和返回结果,但是寄存器的数量有限,如果参数数量过多,如何传递?内存。怎样通过内存传递多个参数?
3、子程序中怎样分配临时变量?如果寄存器不够怎么办?内存。一般在栈中分配临时变量,怎样分配?
参考教材
[1]: 王爽老师的 汇编语言(第3版)
[2]: 李忠 / 王晓波 / 余洁 x86汇编语言-从实模式到保护模式