以下内容只是本人的一些学习心得,如有谬误,希望诸位大神不吝赐教,菜鸟在此拜过各位大神。
这几天刚刚学了call和ret指令,call指令和ret指令是用来实现程序的跳转的,汇编程序中,主程序和子程序之间实现的跳转,是通过对cs和ip寄存器的值进行压栈或出栈实现的,本章中,我们的目标是实现类似于c语言中的printf函数的功能,不过在开始前,我们先来总结一下call和ret指令。
我们使用“()”将寄存器括起来的形式,表示该寄存器存储的值,比如(IP)表示偏移地址寄存器存储的值。
一、ret和retf指令:
1、ret指令用栈中的数据,修改ip的内容,实现近转移,下面是执行操作:
(1)(IP) = ((ss)*16 + (sp))
(2) (sp) = (sp) + 2
其实质等同于:pop IP
2、retf指令用栈中的数据,修改ip寄存器的内容,实现远转移,下面是执行操作:
(1) (IP) = ((ss)*16+(sp))
(2) (sp)= (sp)+2
(3) (cs)=((ss)*16+(sp))
(4) (sp)= (sp)+2
其实质等同于:pop IP
pop CS
二、call指令(注意:call指令不能实现段内短转移)
1、这里要对跳转指令进行必要的说明,所谓的跳转指令就是指jmp指令,下面我们来对jmp指令进行简要的说明:
如上图所示,囊括了jmp指令的大多数情况。
要了解以上列举的一系列内容,我们首先要知道jmp偏移位移的计算公式,什么是偏移位移,就是指IP寄存器值要加上的数值,就是偏移位移,计算公式如下图所示:
上图中的s和s0就是标号,现在我们就以jmp s0 和jmp s来进行说明,这两条指令都是段内短转移,短转移就是修改ip的值,短转移的修改范围为-128~127,也就是说以当前的jmp指令为基准,ip可以向后退128个字节或向前进127个字节,这个ip偏移位移是如何计算出来的呢,其实就是:
标号处地址-jmp指令后的第一个字节的偏移地址,比如mov bx,3(jmp s0后的第一条指令)的偏移地址为0001,而标号s0处的地址为0006,那么此时的偏移位移就是6-3=3,就是这么简单,注意,这里进行的是通过偏移位移进行跳转,而非通过目标地址进行跳转,这样做的好处是可以使程序更加灵活。
现在我们以jmp short 标号为例,进行一些简要的概括:
(1)jmp short 标号 -》(IP) = (IP) + 8位位移
(2)8位位移=标号处地址-jmp指令后的第一个字节的偏移地址
(3)short指令知名此处的位移为8位位移
(4)8位位移的范围为-128~127,用补码表示(这里的空间容量正好为2^8)
(5)8位位移由编译器在程序编译时算出。
由此可以类推jmp near ptr + 标号指令,这里near指的是jmp实现段内近转移,near表示ip的范围为-32768~32767,其他内容和jmp short 标号一致。
接下来我们来看看jmp + 16位寄存器形式:
jmp + 16位寄存器实质上就等同于(IP)= (16位寄存器),比如:
mov ax, 2000h
jmp ax
这里就等同于(IP) = (ax)
jmp word ptr + 内存单元地址:
这种形式就是指,将内存地址单元开始的一字内存空间的值,赋值给IP寄存器,和jmp + 16位寄存器非常相似,例如:
mov ax, 123h
mov ds:[0], ax
jmp word ptr ds:[0]
执行后(IP)=123h
下面来看看jmp far ptr + 标号指令进行讨论:
jmp far ptr + 标号 实现的是段间转移(远转移)
执行该指令后,cs=标号所在的段地址,ip=标号所在的偏移地址,例如:
上面的代码中,jmp直接越过了256个字节的空间,直接跳转到了标号s处,实现了一次大的转移。
关于jmp指令,这里我们讨论最后一个内容,就是jmp dword ptr + 内存单元地址(段间转移)
关于这个指令,其实和jmp far ptr + 标号十分相像,即从内存单元地址开始的两字空间中,低16位值赋给IP寄存器,高16位的值赋给CS寄存器,比如:
mov ax, 123h
mov ds:[0], ax
mov word ptr ds:[2], 0
jmp dword ptr ds:[0]
执行后:(CS)=0,(IP)=123好,cs:ip指向0000:0123处
2、有条件跳转指令:
刚才我们讨论的jmp指令是无条件跳转指令,现在我们来看看有条件跳转指令,jcxz指令,这个指令很特别,当cx寄存器存储的值等于0时,jcxz执行跳转命令,否则不执行,并且执行该指令后的第一条指令,其格式为jcxz + 标号,等同于:
if ((cx)==0)jmp short + 标号
3、循环跳转指令loop
这里,循环跳转指令和jcxz指令正好相反,它是当cx不等于0时,执行跳转指令,它等同于:
(CX)--
if ((cs)!=0) jmp short + 标号
4、讨论完跳转指令,我们现在可以来继续讨论call指令了,call指令实现的功能如下:
将当前的IP或CS和IP的值压入栈中,实现转移
依据位移进行转移的call指令:
(1)call + 标号(当前的ip压栈后,转到标号处执行指令)
即:(sp) = (sp) - 2
((ss)* 16 + (sp)) = (IP)
(IP)= (IP) + 16位位移
等同于:
push IP
jmp near ptr 标号
(2)转移的目的地址在指令中的call指令
call far ptr 标号 :实现段间转移,操作如下:
(sp) = (sp) -2
((ss)*16 + (sp)) = (cs)
(sp) = (sp) -2
((ss)*16 + (sp)) = (ip)
等同于:
push cs
push ip
jmp far ptr 标号
执行结束后,(CS)=标号所在的段地址,(IP)=标号所在的偏移地址
(3)转移地址在寄存器中的call指令:
指令格式:call 16为寄存器
功能:
(sp) = (sp) -2
((ss)*16 + (sp)) = (ip)
(IP)=16为寄存器的值等同于:
push IP
jmp 16位位移
(4)转移地址中的call指令:
主要通过内存单元来执行跳转,分为两种形式
I、call word ptr 内存单元地址
II、call dword ptr 内存单元地址
等同于:
push cs
push ip
jmp dword ptr 内存单元地址
在对call和ret指令有了比较充分的了解以后,我们可以通过它们来实现子程序调用,如下所示:
assume cs:code
code segment
main: .
.
.
call sub1 ;调用子程序sub1
.
.
.
mov ax, 4c00h
int 21h
sub1: .
.
.
call sub2 ;调用子程序2
.
.
.
ret
sub2: .
.
.
ret
code ends
end main
在完成了如何进行子程序调用以后,我们可以来完成我们想做的事情,写一段子程序,使得其能够被复用,只需要通过输入参数就可以实现字体在指定的屏幕位置显示,这里我们这样定义该程序段,dh指定在第几行打印字体,dl指定在第几列打印字体,cl指定字体的颜色属性,ds:si指定数据段的位置,如何在屏幕上打印字体呢?在控制台模式下,我们使用的是80*25打印模式,我们只需要在B8000~BFFFF之间的32KB内存中写入任意的数据,都能够在控制台上显示出来,每行我们可以打印80个字符,每个字符两个字节(一个用于存储字符数据,另一个用于存储字符属性),比如我们要在第8行第3列写入数据,只需要令偏移地址段地址=(8-1)*160 + 3*2(字符数据存储在偶数地址上),下面是实现的代码:
assume cs:code, ds:data
data segment
db 'welcome to masm!',0
data ends
code segment
start: mov ax, data ;获得段地址
mov ds, ax
mov si, 0 ;字符串索引
mov dh, 8 ;在第8行显示数据
mov dl, 3 ;在第三列显示数据
mov cl, 00000111b;字体为黑底白色字体
call show_str ;调用显示段
mov ax, 4c00h
int 21h
show_str:;参数dh:行,dl:列,cl:字体颜色信息,ds:si字段信息
push ax ;保存信息
push bx
push cx
push dx
push di
push si
mov ax, 0 ;清空ax寄存器
;确定列号
mov al, 2 ;将列号始终变为偶数,因为显示字体的索引为偶数
mul dl
mov dl, al
mov ax, 0
;确定行号
mov al, 160 ;每一行所占的字节数
mul dh
mov dh, 0
add dx, ax ;确定输出字体的位置;
mov bx, dx
;确定输出80*25模式字体的段地址以及索引号
mov ax, 1011100000000000b;显示附加段区域
mov es, ax
mov di, 0 ;显示段索引
mov ax, 0 ;确定字体颜色及字体背景信息
mov al, cl
s: mov cl, ds:[si] ;将字符移入cl寄存器
mov ch, 0
jcxz OK
mov dl, ds:[si]
mov byte ptr es:[bx][di], dl ;将字体写入B800为段地址的地方
mov byte ptr es:[bx][di].1, al
inc si
add di, 2
jmp short s ;返回s处
OK: pop si
pop di
pop dx
pop cx
pop bx
pop ax
ret
code ends
end start
效果如上图所示,如果我们输入的行数(dh)为20,列数(dl)为50,字体显示为绿色,则如下图所示:
由此可见,我们的子段可以实现代码复用,这里我们就已经实现了类似于C语言中,printf函数的功能了。
上面所显示的程序中,show_str段就是类似于C语言的printf函数。