众所周知,微处理器执行任务的过程只是从内存中逐条执行二进制指令,并且有一个程序计数器保存下一条指令的地址。因此,如果我没记错的话,这就是处理器执行其任务的方式。但是还有另一个名为 Stack Pointer 的指针,它的作用与程序计数器几乎相同。我的问题是为什么我们需要一个堆栈指针来指向内存(堆栈)的地址?有人可以告诉我堆栈指针和程序计数器之间的主要区别吗?
2 回答
嗯,它们是根本不同的概念。它们都包含内存地址,但请记住,指令和数据都保存在(有效地)相同的内存空间中。
程序计数器包含当前正在执行的指令的地址。事实上,CPU 在执行指令之前使用程序计数器中的值来获取指令。随着指令的执行,它的值会增加,如果代码分支,它的值将被强制覆盖。
堆栈指针包含硬件堆栈顶部的地址,这是运行代码用作暂存器的内存区域。值暂时存储在那里,函数的参数有时会放在那里,代码地址也可以存储在那里(例如,当一个函数调用另一个函数时)。
void show ( unsigned int );
unsigned int fun ( unsigned int x )
{
if(x&1) show(x+1);
return(x|1);
}
0000200c <fun>:
200c: e3100001 tst r0, #1
2010: e92d4010 push {r4, lr}
2014: e1a04000 mov r4, r0
2018: 1a000002 bne 2028 <fun+0x1c>
201c: e3840001 orr r0, r4, #1
2020: e8bd4010 pop {r4, lr}
2024: e12fff1e bx lr
2028: e2800001 add r0, r0, #1
202c: ebfffff5 bl 2008 <show>
2030: e3840001 orr r0, r4, #1
2034: e8bd4010 pop {r4, lr}
2038: e12fff1e bx lr
使用一个简单的函数,使用其中一个 arm 指令集进行编译和反汇编,就像您在这个问题上标记 arm 一样。
让我们假设一个简单的串行非管道老式类型执行。
为了到达这里,发生了一个调用(此指令集中的 bl,分支和链接),将程序计数器修改为 0x200C。程序计数器用于获取该指令 0xe3100001,然后在执行之前获取之后,程序计数器设置为指向下一条指令 0x2010。由于该程序计数器是针对该特定指令集进行描述的,因此它会获取并暂存下一条指令 0xe92d4010,并且在执行 0x200C 指令之前,pc 包含值 0x2014,前两条指令。出于演示目的,让我们想想我们从 0x200C 获取 0xe3100001 的老派,现在将 pc 设置为 0x2010 等待执行完成和下一个获取周期。
第一条指令测试 r0 的 lsbit,传入的参数 (x),程序计数器未修改,因此下一次 fetch 从 0x2010 读取 0xe92d4010
程序计数器现在包含 0x2014,执行 0x2010 指令。该指令是使用堆栈指针的推送。作为程序员进入这个函数时,我们不关心堆栈指针的确切值是什么,它可能是 0x2468,也可能是 0x4010,我们不在乎。所以我们只会说它包含值/地址 sp_start。这个push指令是用栈来保存两件事,一是链接寄存器lr,r14,返回地址,当这个函数执行完毕我们要返回到调用函数。r4 根据该编译器为此指令集使用的调用约定规则,必须保留 r4,因为如果您对其进行修改,则必须将其返回到调用时的值。所以我们要把它保存在堆栈上,这个编译器没有将 x 放入堆栈并在此函数中多次引用 x,而是选择保存 r4 中的任何内容(我们不在乎我们只需要保存它)并使用 r4 在此函数的持续时间内保存 x如编译。我们调用的任何函数以及它们调用的函数等都将保留 r4,因此当我们调用的任何人返回给我们时,r4 就是我们调用时的样子。因此堆栈指针本身更改为 sp_start-8,在 sp_start-8 处保存 r4 的已保存副本,在 sp_start-4 处保存 lr 或 r14 的副本,我们现在可以修改 r4 或 lr,因为我们希望我们有一个便笺簿(堆栈)带有一个保存的副本和一个指针,我们可以对其进行相对寻址以获取这些值,并且任何想要使用堆栈的调用函数都将从 sp_start-8 向下增长,而不是踩在我们的暂存器上。这个编译器选择保存 r4 中的任何内容(我们不在乎我们只需要保存它)并使用 r4 在编译的这个函数的持续时间内保存 x。我们调用的任何函数以及它们调用的函数等都将保留 r4,因此当我们调用的任何人返回给我们时,r4 就是我们调用时的样子。因此堆栈指针本身更改为 sp_start-8,在 sp_start-8 处保存 r4 的已保存副本,在 sp_start-4 处保存 lr 或 r14 的副本,我们现在可以修改 r4 或 lr,因为我们希望我们有一个便笺簿(堆栈)带有一个保存的副本和一个指针,我们可以对其进行相对寻址以获取这些值,并且任何想要使用堆栈的调用函数都将从 sp_start-8 向下增长,而不是踩在我们的暂存器上。这个编译器选择保存 r4 中的任何内容(我们不在乎我们只需要保存它)并使用 r4 在编译的这个函数的持续时间内保存 x。我们调用的任何函数以及它们调用的函数等都将保留 r4,因此当我们调用的任何人返回给我们时,r4 就是我们调用时的样子。因此堆栈指针本身更改为 sp_start-8,在 sp_start-8 处保存 r4 的已保存副本,在 sp_start-4 处保存 lr 或 r14 的副本,我们现在可以修改 r4 或 lr,因为我们希望我们有一个便笺簿(堆栈)带有一个保存的副本和一个指针,我们可以对其进行相对寻址以获取这些值,并且任何想要使用堆栈的调用函数都将从 sp_start-8 向下增长,而不是踩在我们的暂存器上。我们调用的任何函数以及它们调用的函数等都将保留 r4,因此当我们调用的任何人返回给我们时,r4 就是我们调用时的样子。因此堆栈指针本身更改为 sp_start-8,在 sp_start-8 处保存 r4 的已保存副本,在 sp_start-4 处保存 lr 或 r14 的副本,我们现在可以修改 r4 或 lr,因为我们希望我们有一个便笺簿(堆栈)带有一个保存的副本和一个指针,我们可以对其进行相对寻址以获取这些值,并且任何想要使用堆栈的调用函数都将从 sp_start-8 向下增长,而不是踩在我们的暂存器上。我们调用的任何函数以及它们调用的函数等都将保留 r4,因此当我们调用的任何人返回给我们时,r4 就是我们调用时的样子。因此堆栈指针本身更改为 sp_start-8,在 sp_start-8 处保存 r4 的已保存副本,在 sp_start-4 处保存 lr 或 r14 的副本,我们现在可以修改 r4 或 lr,因为我们希望我们有一个便笺簿(堆栈)带有一个保存的副本和一个指针,我们可以对其进行相对寻址以获取这些值,并且任何想要使用堆栈的调用函数都将从 sp_start-8 向下增长,而不是踩在我们的暂存器上。
现在我们获取 0x2014 将 pc 更改为 0x2018,这会在 r4 中生成 x(在 r0 中传入)的副本,以便我们稍后在函数中使用它。
我们获取 0x2018 将 pc 更改为 0x201C。这是一个条件分支,因此根据条件,pc 将保持 0x201C 或更改为 0x2028。有问题的标志是在执行 tst r0,#1 期间设置的,其他指令没有触及该标志。所以我们现在有两条路径可以遵循,如果条件不成立,那么我们使用 0x201C 来获取
fetch from 0x201c 将 pc 更改为 0x2020,这执行 x=x|1,r0 是包含函数返回值的寄存器。该指令不修改程序计数器
fetch from 0x2020 将 pc 更改为 0x2024,执行 pop。我们没有修改堆栈指针(另一个保留的寄存器,你必须把它放回你找到它的地方)所以 sp 等于 sp_start-8 (即 sp+0)现在我们从 sp_start-8 读取并放入r4 中的值,从 sp_start-4(即 sp+4)读取,并将该值放入 lr 并将 8 添加到堆栈指针,因此现在将其设置为 sp_start,即我们开始时的值,将其放回你找到它的方式。
从 0x2024 获取将 pc 更改为 0x2028。bx lr 是到 r14 的分支,基本上它是函数的返回,这会修改程序计数器以指向调用函数,调用函数之后的指令称为 fun()。pc 已修改,从该函数继续执行。
如果 0x2018 处的 bne 确实发生了,那么在 bne 执行期间 pc 更改为 0x2028 我们从 0x2028 获取并在执行前将 pc 更改为 0x202c。0x2028 是加法指令,不修改程序计数器。
我们从 0x202c 获取并在执行前将 pc 更改为 0x2030。bl 指令确实修改了程序计数器和链接寄存器,它在这种情况下将链接寄存器设置为 0x2030,将程序计数器设置为 0x2008。
show 函数执行并返回 0x2030 的取指,将 pc 更改为 0x2034 发生在 0x2030 的 orr 指令不会修改程序计数器
fetch 0x2034 set pc to 0x2038 execute 0x2034, like 0x2020 这取地址 sp+0 处的值并将其放入 r4 取 sp+4 并将其放入 lr 然后将 8 添加到堆栈指针。
获取 0x2038 将 pc 设置为 0x203c。这确实返回将调用者返回地址放入程序计数器中,导致下一次提取来自该地址。
程序计数器用于获取当前指令并指向下一条指令。
在这种情况下,堆栈指针执行两项工作,它显示堆栈顶部的位置,可用空间的开始位置以及提供访问此函数中项目的相对地址,因此在推送后此函数的持续时间内保存r4 寄存器位于 sp+0,因为此代码被设计,返回地址位于 sp+8。如果我们在堆栈上还有其他一些东西,那么堆栈指针将被进一步移动到当时的空闲空间中,堆栈上的项目将位于 sp+0、sp+4、sp+8 等或其他值 8 、16、32 或 64 位项目。
一些指令集和一些编译器设置也可以设置一个帧指针,它是第二个堆栈指针。一项工作是跟踪已用堆栈空间和空闲堆栈空间之间的边界。另一项工作是提供一个指针,从中进行相对寻址。在此示例中,堆栈指针本身 r13 用于两个作业。但是我们可以告诉编译器和其他指令集你别无选择,我们可以将帧指针保存到堆栈然后帧指针 = 堆栈指针。然后我们在这种情况下将堆栈指针移动 8 个字节,帧指针将用作 fp-4 和 fp-8 可以说寻址堆栈上的两个项目,并且 sp 将用于被调用函数以了解可用空间在哪里开始。一个帧指针一般是浪费一个寄存器,但是某些实现默认使用它,并且有些指令集您没有选择,要达到两倍的距离,它们将需要使用特定寄存器对堆栈访问进行硬编码,并且仅在一个方向上的偏移量会添加一个正偏移量或否定的。在这种情况下,在 arm 中,推送实际上是用于对寄存器 r13 进行编码的通用存储倍数的伪指令。
有些指令集你看不到程序计数器,它对你来说是不可见的。同样,有些指令集你看不到堆栈指针,它对你来说是不可见的。