在栈的那里已经简单提到了子程序的调用的实质,就是修改CS:IP,在这里就详细介绍一下子程序调用的过程。
1.call,ret,retf指令
call,ret和retf是汇编为子程序调用而专门设计的汇编指令,CPU在执行ret和retf指令时,利用的栈中的数据,ret只修改IP的值,实现近转移(段内),而retf修改CS和IP的值(默认CS存在栈中的高位地址),实现远转移(段间)。
CPU在执行ret指令时,进行下面操作,用汇编语法解释就是:
pop IP ;将栈顶元素的值赋值给IP,把栈顶向下移动两个字节(删除原栈顶)
而CPU在执行retf指令时,进行下面操作,用汇编语法解释就是:
pop IP ;把栈顶元素值赋值给IP,把栈顶向下移动两字节
pop CS ;把新栈顶元素复制给CS,再次将栈顶向下移动两字节
CPU在执行call指令时,进行两步操作,(1)将当前的IP或CS与IP压入栈中,(2)jmp标号处
call实现转移的方法和jmp原理相同,call分为两种(不含短转移,因为call操作需要用到栈,而对栈的操作是默认16位的),近转移,远转移。
call 标号 ;相当于执行push IP,jmp near prt 标号
call far prt 标号 ;相当于执行push CS,push IP,jmp far prt 标号
体会一下call和ret指令利用栈的理由,在思考一下C语言中的递归算法,是不是能深刻了解递归中函数的返回过程了?不断call函数,然后就把IP不断压入栈中,最后返回时,不断popIP,就完成了逐级返回。
2.寄存器冲突
在这之前,已经提到过寄存器冲突的问题了(CX的循环问题),现在介绍的是一种标准的调用函数的过程。
因为我们CPU中的寄存器是有限的,所以你的子函数也难免会利用到你父函数中使用的寄存器,那么为了解决这类问题,汇编语言在编写子函数一般使用以下框架:
(1)子程序开始:将父程序的所有寄存器入栈。
(2)子程序内容
(3)子程序结束:将寄存器出栈恢复
(4)ret,retf返回
因为利用到了栈,所以子程序中改变的寄存器的值并不会影响到父程序(除非你在子程序中直接修改栈中内容),这也就更加深刻的解释了C语言中传参的问题,为什么实参无法影响到形参。
日后如果碰到汇编程序一开头就在不断push一些寄存器,那么这接下来的程序就很可能是子函数了。
网友评论