堆栈指针和程序计数器有什么区别?

What is the difference between Stack Pointer and Program Counter?

众所周知,微处理器执行任务的过程就是从内存中一条一条地执行二进制指令,有一个程序计数器保存着下一条指令的地址。如果我没记错的话,这就是处理器执行任务的方式。但是还有另一个名为堆栈指针的指针,它的功能几乎与程序计数器相同。我的问题是为什么我们需要一个堆栈指针来指向内存地址(堆栈)?有人能告诉我堆栈指针和程序计数器之间的主要区别吗?

嗯,它们是根本不同的概念。它们都包含内存地址,但请记住,指令和数据都保存在(有效地)同一内存中 space.

程序计数器 包含当前正在执行的指令的地址。事实上,CPU 在执行指令之前使用程序计数器中的值来获取指令。随着指令的执行,它的值递增,如果代码分支,它的值将被强制覆盖。

堆栈指针包含hardware stack顶部的地址,这是运行代码用作暂存器的内存区域.值临时存储在那里,函数的参数有时会放在那里,代码地址也可以存储在那里(例如,当一个函数调用另一个函数时)。

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、分支和 link),该调用将程序计数器修改为 0x200C。程序计数器用于获取该指令 0xe3100001,然后在执行之前获取之后,程序计数器设置为指向下一条指令 0x2010。由于此程序计数器是针对此特定指令集描述的,因此它获取并暂存下一条指令 0xe92d4010,并且在执行 0x200C 指令之前,pc 包含值 0x2014,提前两条指令。出于演示目的,让我们想想我们从 0x200C 获取 0xe3100001 的旧学校,pc 现在设置为 0x2010 等待执行完成和下一个获取周期。

第一条指令测试 r0 的 lsbit,传入的参数 (x),程序计数器未被修改,因此下一次提取从 0x2010 读取 0xe92d4010

程序计数器现在包含0x2014,执行0x2010指令。这个指令是一个push,它使用栈指针。作为程序员进入这个函数时,我们不关心堆栈指针的确切值是什么它可能是 0x2468 也可能是 0x4010,我们不关心。所以我们只说它包含 value/address sp_start。这个push指令是用栈来保存两件事,一个是link寄存器lr,r14,return地址,当这个函数执行完我们要return到调用函数。并且 r4 根据此编译器对此指令集使用的调用约定规则表示,必须保留 r4,因为如果您修改它,则必须 return 将其设置为调用时的值。所以我们要把它保存在堆栈上,而不是把 x 放在堆栈上并在这个函数中多次引用 x,这个编译器选择保存 r4 中的任何内容(我们不关心我们只需要保存它)和在编译时使用 r4 在此函数的持续时间内保持 x。我们调用的任何函数和他们调用的函数等都将保留 r4,因此当我们调用 returns 的任何人返回给我们时,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 change pc to 0x2020,这执行 x=x|1,r0 是包含函数 return 值的寄存器。该指令不修改程序计数器

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,我们开始时的值,把它放回原来的样子。

fetch from 0x2024 将 pc 更改为 0x2028。 bx lr 是 r14 的分支,基本上它是函数的 return,这修改了程序计数器以指向调用函数,调用函数之后的指令称为 fun()。 pc 已修改,从该函数继续执行。

如果0x2018处的bne确实发生了那么bne执行期间的pc变为0x2028我们从0x2028获取并在执行前将pc变为0x202c。 0x2028为加法指令,不修改程序计数器。

我们从0x202c取数据,在执行前把pc改成0x2030。 bl 指令确实修改了程序计数器和 link 寄存器,在这种情况下它将 link 寄存器设置为 0x2030,并将程序计数器设置为 0x2008。

show 函数执行,returns 获取 0x2030 将 pc 更改为 0x2034 发生在 0x2030 的 orr 指令不会修改程序计数器

fetch 0x2034 set pc to 0x2038 execute 0x2034, like 0x2020 这取地址 sp+0 的值并将其放入 r4 取 sp+4 并将其放入 lr 然后将 8 添加到堆栈指针。

fetch 0x2038 将 pc 设置为 0x203c。这确实 return 将调用者 return 地址放入程序计数器中,导致下一次提取来自该地址。

程序计数器用于获取当前指令并指向下一条指令。

在这种情况下,堆栈指针同时执行这两项工作,它显示堆栈顶部的位置,免费使用的位置 space 以及提供一个相对地址来访问此函数中的项目,因此对于推送后此功能的持续时间保存的 r4 寄存器位于 sp+0,因为此代码是设计的,return 地址位于 sp+8。如果我们在堆栈上还有其他一些东西,那么堆栈指针将进一步移动到 then free space 并且堆栈上的项目将位于 sp+0、sp+4、sp+8 等处,或者8、16、32 或 64 位项目的其他值。

一些指令集和一些编译器设置也可以设置一个帧指针,它是第二个堆栈指针。一项工作是跟踪已用堆栈 space 和空闲堆栈 space 之间的边界。另一项工作是提供一个指针,从中进行相对寻址。在此示例中,堆栈指针本身 r13 用于两个作业。但是我们可以告诉编译器,在其他指令集中你别无选择,我们可以将帧指针保存到堆栈中,然后帧指针 = 堆栈指针。然后我们将堆栈指针在这种情况下移动 8 个字节,帧指针将用作 fp-4 和 fp-8 可以说是对堆栈上的两个项目进行寻址,而 sp 将用于被调用函数以了解空闲 space开始。帧指针通常是对寄存器的浪费,但有些实现默认使用它,并且有些指令集您没有选择,要达到两倍的距离,它们需要使用特定寄存器对堆栈访问进行硬编码,并且仅在一个方向上的偏移添加正偏移或负偏移。在这种情况下,在 arm 中,推送实际上是通用存储倍数的伪指令,其中对寄存器 r13 进行了编码。

有些指令集您看不到程序计数器,您无论如何都看不到它。同样,有些指令集你看不到堆栈指针,它对你来说是不可见的。