我们在此前的学习中已经了解了CPU中众多的寄存器,比如通用寄存器 AX、BX、CX、DX,还有段寄存器 CS、DS、SS、ES。在内存访问和灵活寻址的学习中,我们重点学习了 BX 寄存器和 CX 寄存器。BX 寄存器通常配合 DS段寄存器来实现内存访问,而 CX 寄存器则配合 Loop 指令实现循环控制。
此外,除了上述我们提到的寄存器外,还有几个特殊的寄存器。IP 寄存器,全称“指令指针寄存器”,配合 CS 段寄存器使用,任意时刻 CPU 将 CS:IP 指向的地址下的数据当作代码来执行;SP 寄存器,全称“栈顶指针寄存器”,配合 SS 段寄存器使用,任意时刻 CPU 将 SS:SP 指向的地址当作栈的栈顶。
另外,我们还学习了两个 BX 寄存器的小伙伴,SI、DI。配合这两个小伙伴,BX 就能够实现多种的内存灵活寻址。
那么本篇博文,我们将介绍一个新的寄存器:BP。BP 在用法和特性上,更像是 BX 的兄弟,至于为什么这么说,相信你看完本篇博文就会明白了~那么就让我们开始本篇学习吧!
我们知道,SS 段寄存器是用来存放栈段的段地址,通常情况下,它和 SP 紧密相连,共同指向了栈顶的地址。可是它并不像 CS 段寄存器那样单一,除了拥有 SP 这个左膀外,它还有一个右臂:BP。
bp 寄存器在用法上和 bx 寄存器极为相似,只不过 bx 寄存器默认绑定的段寄存器是 DS,而 bp 寄存器默认绑定的段寄存器则是:SS。首先我们先来看一下 bp 寄存器都有哪些用法和实现的功能,从中我们可以窥探出 bp 与 bx 之间的区别联系。
做为 bx 的兄弟,bx 拥有的特性,bp 也一概不拉,bx 的值可以是一个偏移地址,bp 也行。那么首先就来说一下 [bp]:
[bp] 用来表示一处内存地址,该地址的偏移地址为 bp 寄存器中的值,段地址默认为 SS 段寄存器中的值。
汇编指令:
mov ax,[bp],含义为:将偏移地址为 bp 寄存器中的值,段地址为 SS 段寄存器中的值,的内存地址下的一个字数据,送入AX寄存器中。
数学化描述:(ax) = ((ss) * 16 + (bp))
做为 bx 的兄弟,你的朋友也就是我的朋友,所以 bp 也可以和 bx 的小伙伴们一起玩耍组合。例如和立即数 idata 组合进行寻址:[bp+idata]
[bp+idata] 用来表示一处内存地址,该地址的偏移地址为 bp 寄存器中的值加上一个立即数 idata,段地址默认为 SS 段寄存器中的值。
汇编指令:
mov ax,[bp+idata],含义为:将偏移地址为 bp 寄存器中的值加上一个立即数,段地址为 SS 段寄存器中的值,的内存地址下的一个字数据,送入AX寄存器中。
数学化描述:(ax) = ((ss) * 16 + (bp) + idata)
我们知道,[bx+idata] 寻址方式实现了类似 C语言中一维数组的数据访问,那么同样,[bp+idata] 也是实现了一维数组的访问。看到这里,你可能会意识到了,bx 存在的一系列灵活寻址,bp 是否也可以呢?答案是:当然可以!
bp 当然可以和 si、di 组合,实现更加灵活的寻址:[bp+si]、[bp+di]
[bp+si] 用来表示一处内存地址,该地址的偏移地址为 bp 寄存器中的值加上 si 寄存器中的值,段地址默认为 SS 段寄存器中的值。
汇编指令:
mov ax,[bp+si],含义为:将偏移地址为 bp 寄存器中的值加 si 寄存器的值,段地址为 SS 段寄存器中的值,的内存地址下的一个字数据,送入AX寄存器中。
数学化描述:(ax) = ((ss) * 16 + (bp) + (si))
[bp+di] 的含义和用法与 [bp+si] 相同。
同样,[bp+si] 的寻址方式实现了类似 C语言中二维数组的数据访问。
和 bx 一样,bp 也可以和 idata、si 或者 di 进行组合,实现灵活寻址:[bp+si+idata]、[bp+di+idata]
[bp+si+idata] 用来表示一处内存地址,该地址的偏移地址为 bp 寄存器中的值加上 si 寄存器中的值,再加上一个立即数 idata,段地址默认为 SS 段寄存器中的值。
汇编指令:
mov ax,[bp+si+idata],含义为:将偏移地址为 bp 寄存器中的值加 si 寄存器的值再加上一个立即数 idata,段地址为 SS 段寄存器中的值,的内存地址下的一个字数据,送入AX寄存器中。
数学化描述:(ax) = ((ss) * 16 + (bp) + (si) + idata)
[bp+di+idata] 的含义和用法与 [bp+si+idata] 相同。
除了默认绑定的段寄存器:SS 外,bp 也是可以和其他段寄存器进行友好交互,当然这需要在汇编语句中使用显示指定,才可起到效果。显示指定我们之前也学习过,格式为:段寄存器:[]
bp 和 ES
mov ax,es:[bp],将偏移地址为 bp 寄存器的值,段地址为 ES 寄存器的值,的地址下的一个字数据送入 AX 寄存器中
bp 和 DS
mov ax,ds:[bp],将偏移地址为 bp 寄存器的值,段地址为 DS寄存器的值,的地址下的一个字数据送入 AX 寄存器中
现在我们来思考博文开头说到的那个问题:为什么说 bp 是 bx 的兄弟,而不是 bx 的伙伴呢?
要想弄明白这个问题,我们需要先弄明白 bx 的伙伴定义。我们看 bx 的伙伴:idata、si、di,这三个都可以和 bx 进行组合实现灵活寻址,能够在一起玩耍,所以称呼为伙伴。
那么,bp 可以和 bx 在一起玩耍吗?比如进行组合:[bx+bp],这种可以行吗?我们直接打开 debug 程序,使用 A 命令插入一条汇编语句:mov ax,[bx+bp],来看下是否通过:
如上图所示,我们可以看到直接给报出了 Error,也就说 bx 是无法与 bp 组合搭配进行寻址的。既然无法在一起玩耍,显而易见,bp 就不是 bx 的伙伴了。
正所谓做不成伙伴就当你的兄弟,从上面的以 bp 为主实现的系列灵活寻址方式可以看出,bp 和 bx 拥有相同的定位和用法,其值都可以成为一个偏移地址,搭配对应的段寄存器实现内存数据访问,而且两者都可以和 idata、si、di 进行组合实现灵活寻址。
综上所述,bp 是 bx 的兄弟,而不是 bx 的伙伴。
上面我们讨论完了 bp 和 bx 两人的关系,那么接下来我们就要好好来探究下 bp 与 sp 之间还存在着什么样的火花?
我们知道,sp 寄存器配合 SS 段寄存器来指向当前栈顶位置,也就是说任意时刻下 SS:SP 指向了栈顶,所以说 sp 相比 bp 是特殊的存在。
通常情况下我们操作 sp,是通过一些专门的汇编指令,例如我们此前学习的进栈指令:PUSH,以及出栈指令:POP。有关这两个指令的详解,大家可以翻看此前的博文进行温习,这里不再赘述。尽管 PUSH、POP 这两个指令可以实现修改 sp 值,但是并不能做到一个绝对修改,也就是说我们实际上并不能将 sp 直接设置为一个数值,sp 值变化依靠于它上次的值,比如 PUSH 指令执行后,sp 的值就是基于上次的值减去2,POP 指令执行后,sp 的值就是基于上次的值加上2。
由于 sp 的特殊性,所以不建议在程序运行中途使用 mov 指令对 sp 进行直接赋值,因为不按规则的修改栈顶指针可能会导致栈中数据发生不可逆转的损坏,致使程序执行出现异常甚至崩溃。一般建议是在程序开始的时候申请一段栈空间,并为 SS、SP 赋值,这样是最为牢靠的。有关具体的实现细节小伙伴可翻看此前博文讲述进行温习,这里不再讨论。
现在我们通过比较一下 BP、SP 两者之间的相同点和不同点,来寻找它们之间的联系:
相同点:
1、其值都可做为偏移地址
2、默认关联的段寄存器都是 SS 栈段寄存器
不同点:
1、两者与 SS 搭配,所指向的内存地址含义:SS:SP 指向当前栈顶,SS:BP 指向了栈内某处内存空间地址
2、两者在汇编中用法不同:BP 可以与 idata、si、di 搭配实现灵活寻址,SP 则不能,需配合特定指令使用
3、两者使用场景不同:BP 用于栈内数据访问,SP 用于定位栈顶位置
4、汇编中,访问两者所指向的内存数据,方式不同:获取 BP 指向的内存数据,使用 MOV 指令即可,获取 SP 指向的内存数据,使用 POP 指令
看到这里,你心中可能会出现一个疑问:有了 SP,为什么又要设计一个 BP 呢?BP 存在的意义是什么?访问栈中数据,我们 POP 也是可以的,来一个 BP 是否多此一举?
答案当然是:不是!BP 存在有着重大意义。我们先通过一个简单的小例子,来逐步深入揭开 BP 身上的面纱~
假设当前内存中栈空间如下,大小为10个字,栈顶地址为0,栈底为14H。设当前为栈满状态,即 SP 指向地址 0H。
地址 | 数据 |
0 | 0001H |
2 | 0002H |
4 | 0003H |
6 | 0004H |
8 | 0005H |
a | 0006H |
c | 0007H |
e | 0008H |
10 | 0009H |
12 | 000AH |
现在程序需要获取地址 EH 下的栈中数据【0008H】进行操作处理,问该如何实现?
针对此问题,首先就是要排除 POP 指令了,因为首先 POP 指令只能按照进栈的顺序依次取出栈中数据,示例中需要获取的数据位置,需要连续 POP 出9次才行。再者,每次 POP 出数据栈顶都会发生改变,已经 POP 出栈的数据如何存放,以及后续数据再次进行入栈问题,这些都是比较复杂的逻辑实现。所以 POP 指令显然并不适合解决该示例。
那么,最完美的解决办法就是对栈空间进行寻址~就需要使用我们的 bp 来上场了!下面我们通过具体编程实现,来熟悉 bp 的用法操作。
assume cs:code,ss:stack ; 定义代码段、栈段
stack segment
dw 1,2,3,4,5,6,7,8,9,10 ; 初始化栈段内数据
stack ends
code segment
start:
mov ax,stack ; 获取自定义的栈段段地址,放入AX寄存器内
mov ss,ax ; 设置SS寄存器值为自定义的栈段段地址
mov sp,0 ; 设置当前栈顶为0,表示栈满状态
mov bp,sp ; 将当前栈顶位置赋值给bp
mov ax,[bp+14] ; 使用[bp+idata]形式寻址
mov ax,4c00H
int 21H
code ends
end start
上述示例代码实现了题目要求,获取栈空间内指定的位置【0008H】的数据。代码是比较简洁的,首先是设置 SS 段寄存器为自定义栈空间段地址,初始化 SP 值为栈满。下面就是将 sp 赋值给 bp,使用 [bp+idata] 形式进行寻址即可。
我们编译链接,将程序在 debug 中运行:
可以看到AX寄存器成功取到了栈中指定位置的数据。
通过上述示例我们需要学习到的是,栈空间内的寻址,是相对当前栈顶位置进行偏移。示例中需要寻址的地址相对栈顶位置偏移为 +14,所以使用 [bp+14] 来表示该处内存地址。
总结
1、获取栈空间内数据,建议使用 BP 来寻址访问,比 POP 指令更加方便快捷
2、使用BP来寻址时,BP 值一般设置为当前栈顶,即 SP 的值,使用 [bp+idata] 形式进行寻址。即相对当前栈顶偏移。
那么通过该示例,我们知道了bp 的一个作用: 为访问栈段空间数据提供方便!
我们知道程序中存在着方法调用,比如 a 方法内调用 b 方法,b 方法执行完成后重新返回到 a 方法内继续执行,在程序运行在不同方法间跳转时,那么就面临着两个问题:1、方法参数的传递;2、如何返回上层调用。
在高级语言的开发中,例如 C语言、C++、Java 中,我们似乎不用考虑这些问题,方法参数怎么传递?当然是直接 方法名(参数) 就好了呀~如何返回上层调用?方法逻辑执行完毕自然就回来了,还要怎么考虑~
然而,在汇编开发中,我们则必须考虑这两个问题。因为汇编中程序员所能够使用的寄存器资源是有限的:假设你的方法只有一个参数,那么还好,我们可以把这个参数放在AX寄存器中用来承载,可是如果你的方法有好多个参数呢?五个参数,六个参数,这些个参数显然寄存器资源已经不够用了!再假设,你调用了一层方法,a() -> b(),你使用AX寄存器来保存 a() 方法的地址,这样 b() 方法执行结束后就可以拿着 AX 寄存器中的地址直接回到 a() 方法中,可是如果你进行了多层调用呢?a() 方法中调用了 b() 方法,b() 方法中又调用了 c() 方法,c() 方法中又调用了d() 方法,层层调用,很快你就发现,糟糕寄存器又不够用了!
所以,很明显宝贵的寄存器资源是不能用来充当传递方法参数的载器,和充当保留上层方法调用地址的容器。那么谁来负责处理这两个问题呢?答案就是:栈。
栈的设计初衷就是为了解决上述两个问题:方法参数传递和返回上层调用。
比如 a() 方法中调用了 b() 方法,b() 方法需要传入三个参数,那么我们就可以将三个参数依次 PUSH 进栈,然后将当前地址 PUSH 进栈,而后就可以进入 b() 方法内了,此时栈中数据应该是这样的:
0H | a() 方法中调用 b() 方法的指令的下一条指令地址 |
2H | b() 方法传入的参数 3 |
4H | b() 方法传入的参数 2 |
6H | b() 方法传入的参数 1 |
这里需要解释为什么返回地址是在栈顶位置?是因为通常情况下当执行转移指令的时候,IP 寄存器的值发生改变,CPU就会执行到新的程序代码,也就是说程序就进入到了被调用的方法内。按照正常逻辑,在调用该方法之前,你需要准备好该方法所需要传入的参数,也就是说在执行转移指令之前,就需要将所需要的参数数据 PUSH 到栈中,最后再将返回地址 PUSH 进栈中,然后执行转移指令修改 IP 值,这样才是一个正确的方法调用流程。如此,你就看到了栈顶的位置是返回地址。
程序进入到 b() 方法内,如何取到它的三个参数?当然是使用 bp 了!
mov bp,sp ; 获取当前栈顶
mov ax,[bp+2] ; 取到参数3
mov ax,[bp+4] ; 取到参数2
mov ax,[bp+6] ; 取到参数1
如此通过 [bp+idata] 寻址方式,便可以获取到传入的参数数据,下面对数据进行操作处理即可。
本篇主要讲解了 bp 寄存器在汇编中的功能和使用,并学习了以 bp 寄存器为主实现的多种灵活寻址方式,这些都是我们后续学习方法跳转的重要基础。
bp 寄存器的重要性不止是方便访问栈空间数据,在接下来的学习中我们再来逐步学习,现在我们先掌握使用 bp 访问栈中数据。实际上在本系列的汇编学习中,我们并不会遇到 bp 复杂使用情况,主要就是使用 bp 寻址栈段。后面如有涉及到 bp 复杂使用场景,博主在详细讲解。
感谢围观,转发分享请标明出处,谢谢~