汇编语言笔记10call和ret指令

汇编语言笔记10call和ret指令

0x00 ret 和retf

​ ret 指令功能就是pop ip 实现近转移(段内转移), retf 是pop ip 然后pop cs 实现远转移(段间转移)

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

QQ截图20200601212535

​ 可以看到执行ret指令后确实跳转到了ip=0 而且栈顶指针sp+2

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

QQ截图20200601213111

QQ截图20200601213139

​ 同样正确跳转,且sp+4(pop ip pop cs)

QQ截图20200601213306

QQ截图20200601213323

​ 很简单 因为ref是pop ip pop cs 那么ip后入栈 cs先如 所以分别是 1000h和0h

0x01 call指令

​ call指令 作用是 (1)将当前的ip或者 cs和ip压入栈中 (2)转移到指定位置 按照给出转移地址的方式不同,分为以下几种情况讨论:

相对位移地址的call指令

call 标号 进行操作 push ip 并且 jmp near ptr 标号 (所以对应的机器码中保存的是相对位移地址,并且该相对唯一地址时16位 -32768-32767)

QQ截图20200601214217

QQ截图20200601214233

​ 结果是6 原因 call s 会让push ip ip是多少呢?执行call s 时ip早已指向下一条指令inc ax 所以 ip是6 然后跳转到s :pop ax那ax 就是6了

绝对地址的call指令

call far ptr 标号 实现的是段间的转移 该指令的作用是 push cs,push ip 然后jmp far ptr 标号

QQ截图20200601214854

​ ax数值是1010h 原因是 call far ptr s 那么push cs push ip 所以栈顶sp内容是8,所以 pop ax ax=8,且栈顶是1000, add ax ax ,ax是10h pop bx ,bx是1000, add ax,bx ax是1010h

绝对地址在寄存器

call 16位reg (1)push ip (2)jmp 16位reg

QQ截图20200601215743

​ ax 是0bh 因为 call ax 让(ip)=5 (括号寄存器表示寄存器的值)进栈,然后来到6执行 mov bp,sp bp就指向栈顶, add ax,[bp] [bp]的默认段寄存器又是ss 所以[bp]直接指向栈顶元素 5 所以 add ax,5 ax=11 /0bh

绝对地址在内存中的call

​ (1)call word ptr 内存地址 相当于 push ip,jmp word ptr 内存地址

​ (2)call dword ptr 内存地址 相当于 push cs push ip jmp dword ptr内存地址

QQ截图20200602081519

​ ax中存放的是03h,原因是call word ptr ds:[0eh] 之前栈空 sp指向10h,然后执行该call 那么 push ip 而(ip)指向的是第一条inc ax的偏移地址 那么栈中ss:[0eh]是 该条指令的偏移地址,而ds和ss相同,所以jmp ds:[0eh] 跳转到第一条inc ax 所以三条inc ax全部被执行 ax 为03h (不考虑程序返回的mov ax,4c00h明显不是题目要考的)

QQ截图20200602083355

QQ截图20200602083420

​ ax的值是1 bx的值是0,原因在于,call dword ptr ss:[0],等效于push cs push ip 所以ss:[0eh]保存着cs段地址,而ss:[0ch]保存着call 时的ip指向的偏移地址,由于执行到call时ip已经指向了下一条语句,所以ss:[0ch]保存着nop的偏移地址,最终ax=offset s- nop的偏移地址=1,同理bx=0

0x02 call和ret的配合使用

QQ截图20200602084756

​ bx的值是8 call s 让ip也就是mov bx,ax的偏移地址进栈,跳到s 处 s处根据cx的值循环三次,所以ax=8,然后ret pop ip 让指令回到mov bx,ax 所以bx=ax=8

​ 利用栈的特性 我们可以实现子程序的嵌套调用 或者说函数的嵌套调用

assume cs:code,ss:stack
stack segment
		dw 8 dup (0)
stack ends
code segment
main:		...
			call sub1; 让main函数的ip进栈
			...
			...
			mov ax,4c00h
			int 21h
			
sub1:		...
			...
			call sub2; 让sub1的ip也进栈
			...
			ret;    让最上面的ip出栈 也就是回到 sub1原先的指令处
			
sub2:		...
			...
			ret;    让最上面的ip出栈 也就是回到 sub1原先的指令处

code ends
end main

0x03 mul指令

​ mul是乘法指令 跟除法类似,根据位数不同有两种

​ (1) 两个8位数相乘 ,一个数默认放在AL 另一个放在8位reg或者内存字节单元 结果保存在AX中

​ (2)两个16位数相乘,一个数默认放在AX,另一个放在16reg或者内存字单元 结果高位放在DX 低位放在AX

​ 计算100*10

mov al,100
mov bl,10;都要为8位寄存器
mul bl

​ 结果如下

QQ截图20200602090211

​ 计算100*1000=100000= 186a0h 由于1000>255所以都要使用 16位

mov ax,100
mov bx,1000
mul bx

QQ截图20200602090549

0x04 模块化程序设计

参数的传递

​ 上面利用call 和 ret实现了子程序,但是子程序一般要传递参数和返回值传递,我们可以利用某一个寄存器来传递参数,同样的方法也可以用来传递返回值

​ 比如下面是计算立方的程序:

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
		
		mov ax,4c00h
		int 21h
cube:	mov ax,bx
		mul bx
		mul bx
		ret
code ends
end start

​ 上面这个程序的立方部分计算是有问题的(是书上的实例) 计算bx的立方,mov ax,bx mul bx 这样计算bx的平方是对的 两个16位相乘 结果低位保存在AX 高位保存在DX 但是再mul bx 就有问题了 只让结果的低位AX与bx相乘,这里是默认了平方的结果高位DX=0 也就是bx小于256了 ,不然结果是错误的

批量数据的传递

​ 上面的cube函数只有一个参数bx 而且返回结果只有两个参数(高16位低16位),如果参数更多该怎么办呢?

​ (1)如果是类似数组类型的数据 比如字符串 可以将数据批量放到内存中,然后用首地址和长度加上循环的方式 传递参数

​ (2)通用方法,用栈,可见《附录四》(下午或者明天写附录四的笔记)

QQ截图20200602094159

​ 两个参数 字符串首地址和长度(循环次数) 长度直接放在cx中 大写函数:

capital:and byte ptr [si],11011111b
		inc si
		loop capital
		ret

​ 完整代码

assume cs:code,ds:data
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

寄存器冲突的问题

​ 在使用子程序时,有些寄存器很可能在调用之前有特殊的作用,这时候在子程序里再使用就很可能产生冲突,解决方法有(1)使用别的寄存器,但是8086cpu就那么几个寄存器,而且比如cx这种 loop 和 jcxz都要检查的换不了

(2)在子程序开头把所有将要使用的寄存器的值push进栈保存起来,最后退出之前pop出栈恢复

​ 举个例子

QQ截图20200602095320

要求不同 capital函数也要改:
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 'conversation',0
data ends
code segment
start:	mov ax,data
		mov ds,ax
		mov si,0
		call capital
		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

QQ截图20200602103304

assume cs:code,ds:data,ss:stack
data segment
		db 'word',0
		db 'unix',0
		db 'wind',0
		db 'good',0
data ends
stack segment
		db 16 dup (0)
stack ends
code segment
start:		mov ax,data
		mov ds,ax
		mov ax,stack
		mov ss,ax
		mov sp,10h
		mov bx,0
		mov cx,4
s:		mov si,0
		call capital
		add bx,5
		loop s
		mov ax,4c00h
		int 21h

capital:		push cx
s1:		mov cl,[si+bx]
		mov ch,0
		jcxz ok
		and byte ptr [si+bx],11011111b
		inc si
		jmp short s1
oK:		pop cx
		ret
		
code ends
end start

QQ截图20200602105758

​ 结果正确,主要是cx被循环和jcxz都要使用,冲突就用栈解决