汇编语言(十一)call和ret指令
call和ret指令
都是用来修改ip的或者同时修改cs和ip。
ret和retf指令作用如下:一个是近转移,一个是远转移。
执行ret指令相当于执行
1 | pop IP |
CPU执行retf指令时相当于执行
1 | pop IP |
举个例子我们来看看。下面的程序中ret指令执行之后,(ip)=0,cs:ip指向代码段第一条指令。
1 | assume cs:code |
那么下面retf执行之后,CS:IP指向代码段第一条指令
1 | assume cs:code |
来看一个小程序,补全,实现从内存1000:0000处开始执行指令。
1 | assume cs:code |
call指令
call和jmp原理类似,只是比jmp多了一步,也就是先将ip,或cs,ip压栈,之后再转移。
注意:不能实现短转移。
依据位移进行转移的call指令
1 | call 标号(将当前ip压栈后,转到标号处执行指令)转移过程中用的是16位位移。(ip)=(ip)+16位位移。 |
显然我们已经知道16位位移的范围是-32768~32767,用补码表示。
位移大小在编译器编译时算出,和之前一样。
执行call时,相当于执行
1 | push IP |
看一个例题:
下面程序执行后,ax中数值为多少。
显然第一步ax=0。
当cs:ip指向1000:3时,‘call s’指令的机器码进入指令缓冲器,ip指向下一条指令开始处(6);十分注意
第二步将0006入栈,然后跳转到s,计算0006到0007的位移,也就是0001(如机器码所示)。
然后就跳转到s开始执行了,出栈6到ax,结束。
转移的目的地址在指令中的call指令
前面讲的call指令在执行的时候机器指令中并没有目的地址,只有相对于当前ip的位移。
call far ptr 标号实现段间转移。
执行call far ptr标号的时候,相当于执行
1 | push cs |
下面看例题
这里将cs1000入栈然后再将ip0008入栈然后跳转到s,再出栈0008到ax,然后ax+ax=0010,然后出栈1000到bx然后ax+bx=1010h
转移地址在寄存器中的call指令
格式:call 16位reg。也是先入栈,再位移,只不过只有ip
相当于执行
1 | push ip |
看一个例题
首先ax=6,然后将0005入栈,之后跳转到0006的位置执行mov bp,sp。此时sp指针指向的内容为0005,所以[bp]=0005,之后ax+[bp]也就是0006+0005=000bh。
转移地址在内存中的call指令
转移地址在内存中的call指令有两种格式
(1) call word ptr 内存单元地址
相当于执行
1 | push ip |
比如下面的指令
1 | mov sp,10h |
执行后,(ip)=0123h,(sp)=0Eh(也就是10-2=0E),注意调用call的时候sp先-2,所以到这里变成10-2=0E。
(2) call dword ptr 内存单元地址
看见dword就想到了把cs,ip一起改变,正好四个字节。
相当于执行
1 | push cs |
比如下面的指令:
1 | mov sp,10h |
执行后(cs)=0(后出栈),(ip)=0123h(先出栈),(sp)=0Ch(这里也就是10-04=0c显然!)
来看一个例题
显然一开始定义了8个字也就是16个字节都为0。
显然执行的时候应该把下一条ip先入栈,然后再jmp到ds:[0E]的位置也就是数据段的最后两个字节(因为数据段和栈段在一起,所以是一个东西)!用来表示新的ip,其实也就是原来的ip。
程序中一开始就定义好了栈段和数据段,都是那16个字节的0。
ax=0,ip入栈,sp = sp - 2 = 16 - 2 = 14然后新的ip跳转到数据段0E的位置也就是原来的ip。那么位移也就为0.
然后显然就继续执行了,ax自增3次变为3结束。
再来看第二个例题
同样地定义了16个字节的0作为初始数据段。然后把它设置为栈段。并且初始化栈底指针。
我们看到ss:[0]栈顶,被置为s的偏移地址。然后把cs地址(两个字节)赋值给ss:[2]的两个字节。
然后调用call,先让sp = sp - 2然后把下一条ip也就是nop所在ip和cs放入栈底,此时栈指针推向0ch,之后ip跳转到ss:[0]也就是s的偏移地址处,然后[2]和[3]的数据作为cs,那么也就和原来的cs相同不变。
所以下一步直接跳转到s的位置。
执行s位置后ax=s的偏移地址。
然后用ax也就是s的偏移地址 - nop的偏移地址,结果显然为1。所以ax = 1此时。
而后面的ss:[0e]存放的是cs的值,bx也=cs,所以两者相减为0。
综上所述ax=1,bx=0
注意!这里cs和ip的入栈是有顺序的!cs后入栈在高位,把ip推向低位!书上写的解释没有注意顺序!但是必须要分清先后!
call和ret的配合使用(入栈出栈来回用)
问题10.1
首先ax=1,cx=3然后将call s的下一句的ip入栈,之后ip跳转到s执行ax+ax=2然后cx = cx - 1 = 2,然后ax+ax=4,cx = cx - 1 = 1,然后ax+ax = 8,cx=cx - 1 - 0然后跳出循环,执行ret,也就是出栈将mov bx,ax的ip给当前ip,这样跳转到mov bx,ax此时bx = 8结束!
我们再来看一个稍微难一些的程序
首先定义栈为16个字节然后开始程序。定义栈底指针等等,然后ax = 1000,然后把mov ax,4c00也就是结束符的地址先入栈,然后ip跳转到s执行,ax + ax = 2000,然后执行ret也就是跳转到结束,那么最终ax = 2000
我们可以发现call指令可以用来执行子程序,但是子程序执行完毕后如何接着原来call的下面继续执行呢,这时正好call把下一条指令的ip放入栈中,而我们在执行完子程序之后只需要执行一个ret就可以出栈ip并且跳转过来!
我们可以用如下方法
1 | 标号: |
具有子程序的源程序如下
1 | assume cs:code |
mul指令
mul是乘法指令。
注意!
要么是8位✖️8位,要么是16位✖️16位。
8位✖️8位:一个默认放在al中另一个在8位reg或一个内存单元中
16位✖️16位:一个默认放在ax中另一个在16位reg或内存字单元中
结果:如果是8位乘法,默认结果放ax,如果是16位乘法,结果高位默认在dx中存放,低位在ax。
内存单元的寻址方式
1 | mul byte ptr ds:[0]//(ax)=(al)*((ds)*16 + 0) |
或者16位乘法
1 | mul word ptr [bx+si+8] |
计算100*10
1 | //显然都是8位 |
显然都是低位✖️低位
计算100*10000
100是8位,然后10000需要16位,所以都用16位。
1 | mov ax,100 |
结果(ax)=4240,(dx)=000f。正好f4240h=1000000
模块化程序设计
利用call和ret指令,用简洁的方法实现多个相互联系,功能独立的子程序来解决一个复杂的问题!
参数和结果传递的问题
实际上就是研究如何存储子程序需要的参数和产生的返回值。
这里有两个问题
1 | 将参数n存储在什么地方 |
显然可以这样
参数:(bx)=N
结果:(dx:ax)=N^3
1 | cube:mov ax,bx |
用寄存器来调用存储参数和结果是最常使用的办法。
调用者将参数送进参数寄存器,从结果寄存器中取得返回值;子程序则相反。
编程计算data段中第一组数据的3次方,结果保存在后面一组dword单元中。(十分重要的程序!好好练习)
1 | assume cs:code |
下面看我先自己编写一下。
1 | code segment |
批量数据的传递
上面的程序中cube只有一个参数放在了bx中,如果有多个,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中。传递给需要的子程序。
设计一个子程序,将一个全是字母的字符串转换为大写。
显然我们需要一个一个将其进行与运算11011111b然后得到大写。
我们可以将字符串在内存中的首地址放在寄存器中传递给子程序,让子程序循环读取。循环的次数就是字符串长度!
1 | capital:and byte ptr [si],11011111b |
我们看标准程序
1 | assume cs:code |
除了用寄存器传递参数,还可以用栈传递参数!
寄存器冲突问题
设计一个子程序,功能:将一个全是字母,以0结尾的字符串转化为大写。
1 | db 'conversation',0 |
其实可以不通过处理字符串长度,只要用jcxz判断最后一位是0就可以知道处理完了。
子程序如下:
1 | capital:mov cl,[si]//cx是16位寄存器,所以用半个来放一个字节的字符串就行。 |
(1)将data段中字符串为大写
1 | assume cs:code |
代码段中程序应该如下:
1 | mov ax,data |
(2)将下面数据段中字符串转换为大写
显然主程序应该如下:
1 | start:mov ax,data |
这里其实又犯了小错误,也就是我们的循环次数用cx表示,但是子程序中同样修改cx用于判断字符串是否到最后一位。
所以这就是寄存器冲突!
所以我们在编写主程序的时候就要检查子程序是否用到cx,bx等等。
所以我们希望
(1)调用子程序的时候,不必关心子程序到底使用了哪些寄存器
(2)编写子程序时不关心调用者使用了哪些寄存器
(3)不会发生寄存器冲突
寄存器冲突的解决办法是在子程序开始前将子程序中所有用到寄存器的内容保存起来(入栈!)
改进一下capital
1 | capital:push cx |
要注意出栈入栈的顺序。
实验10 编写子程序
下期再来补这个。