Linux从头学04:所有编程语言中的栈操作,底层原理都在这里
扫描二维码
随时随地手机看文章
-
示例代码说明
-
执行主程序
-
初始状态
-
执行代码前 5 句
-
准备调用子程序
-
调用子程序
-
子程序
-
寄存器入栈保护
-
计算字符串长度
-
返回结果
-
寄存器出栈
-
返回指令ret
再次重申:我们不是在学习汇编语言,只是利用汇编代码,去繁存简,用最简单的实例来理解栈的操作。
示例代码说明
主程序:设置数据段、栈段、栈顶这 3 个寄存器,然后调用子程序(函数调用);主程序在调用子程序的时候,就涉及到返回地址的入栈、出栈操作。子程序:从寄存器 si 中获取字符串开始地址,然后计算字符串的长度,最后通过寄存器 ax 返回给主程序;
执行主程序
以下演示的截图,是通过debug.exe这个工具来调试的。
代码相关:cs, ip
栈相关:ss, sp
初始状态
在执行第一条指令之前,首先看一下所有寄存器中的值:
执行代码前 5 句
mov ax, datamov ds, ax
mov ax, stack
mov ss, ax
mov sp, 20h
这5行代码的功能就是:设置ds、ss和sp。
把【数据段】安排在 076C:0000 位置;虽然数据段值定义了6个字节的数据(5个字符1个结束符),但是它与栈段的开始地址之间,预留了16个字节的空间。
把【栈段】 安排在 076D:0000 位置;
把【代码段】安排在 076F:0000 的位置;
准备调用子程序
我们都知道,在调用函数的之后,需要把调用指令后面的那条指令的地址,压入到栈中。
(1)call 的指令码和汇编代码
call的汇编代码是:call 0018。
(2) 栈空间的数据
此时,栈顶寄存器sp的值为0020,即:栈的最高地址的下一个位置(为什么是这个位置?上一篇文章有说明)。
调用子程序
子程序的功能是计算字符串的长度,那么主程序一定要告诉子程序:字符串的开始地址在哪里。
(1) 寄存器的值
从图中看出sp的值变成了001E。还记得之前文章说的入栈操作吗?
Step1:sp = sp -2。由于 sp 的初值是 0020,减去 2 之后就是 001E(都是十六进制);从图中还可以看到,指令寄存器ip的值变成了0018,也就是子程序的第1条指令(push bx)的地址。Step2:把要入栈的值(也就是下一条指令的地址 0013)放在 sp 指向的地址处。
(2)栈空间的数据
可以看到:最后 2 个字节是0013,也即是下面的这样:
子程序
保护使用到的寄存器
我们知道:CPU中寄存器都是公用的。
1. push bx
在入栈之前,bx的值是0000,我们给他入栈。
Step1: 把 sp 的值减 2;此时,栈顶寄存器sp变成 001C (001E - 2)。
Step2: 把要入栈的值放在 sp 地址处(2个字节);
2. push cx
在入栈之前,cx的值是005C,我们给他入栈。
3. 计算字符串的长度
字符串是放在数据段中的。数据段的段地址ds,在主程序的开头已经设置好了。
如果不为0,就把长度值加 1 (inc bx),然后继续取下一个字符(inc si);在循环获取每一个字符的时候,可以用bx寄存器来记录长度,所以在子程序的开头要让bx入栈。如果为0,就停止获取字符,因为已经遇到了字符串末尾的 0。
4. 把字符串长度告诉主程序
字符串的长度计算出来了,我们要把这个值告诉主程序,一般都是通过通用寄存器ax来传递返回结果。
5. pop cx
子程序在返回之前,需要把栈中保存的bx、cx值恢复到寄存器中。
sp = 001A栈中的数据情况如下:
cx = 0000
Step1:把 sp 指向的地址单元的中数据( 2 个字节),放入寄存器 cx 中,于是 cx 中的值变成了:005C;此时,栈中的数据情况:Step2:把 sp 的值自增 2,变成 001C (001A 2)。
6. pop bx
执行过程是一样的:
Step1:把 sp 指向的地址单元的中数据( 2 个字节),放入寄存器 bx 中,于是 bx 中的值变成了:0000;此时,栈中的数据情况:Step2:把 sp 的值自增 2,变成 001E (001C 2)。
7. 返回指令 ret
CPU在执行ret指令时,也有2个动作:
Step1:把 sp 指向的地址单元的中数据( 2 个字节),放入指令寄存器 ip 中,于是 ip 中的值变成了:0013;此时,栈中的数据情况是:Step2:把 sp 的值自增 2,变成 0020 (001E 2)。
栈空间中没有任何有意义的数据了;所以,当CPU执行下一条指令的时候,又回到了主程序中继续执行。。。
cs:ip 指向了主程序中 call 指令的下一条指令(mov ax,4c00h);
------ End ------