用汇编实现类似于C语言中的printf函数--学习笔记

来源:互联网 发布:http压力测试 windows 编辑:程序博客网 时间:2024/05/16 15:51

以下内容只是本人的一些学习心得,如有谬误,希望诸位大神不吝赐教,菜鸟在此拜过各位大神。

这几天刚刚学了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函数。

      

原创粉丝点击