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

naa*_*sif 2 arm microprocessors program-counter stack-pointer

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

old*_*mer 8

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
Run Code Online (Sandbox Code Playgroud)

当您在此问题上标记 arm 时,使用一个简单的函数,使用 arm 指令集之一编译和反汇编。

让我们假设一个简单的串行非管道老式执行。

为了到达这里,发生了一个调用(在此指令集中,分支和链接中的 bl)将程序计数器修改为 0x200C。程序计数器用于获取指令 0xe3100001,然后在获取之后在执行之前将程序计数器设置为指向下一条指令 0x2010。由于此程序计数器是针对此特定指令集描述的,因此它提取并暂存下一条指令 0xe92d4010,并且在执行 0x200C 指令之前,pc 包含值 0x2014,即前面两条指令。出于演示目的,让我们想想我们从 0x200C 获取 0xe3100001 的老派,现在将 pc 设置为 0x2010 等待执行完成和下一个获取周期。

第一条指令测试 r0 的 lsbit,传入的参数 (x),程序计数器未修改,因此下一次提取从 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 this take the value at address sp+0 and put it in r4 take sp+4 and put it in the lr 然后将8添加到堆栈指针。

fetch 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 进行编码的通用存储倍数。

某些指令集您看不到它以任何方式都看不到的程序计数器。同样,某些指令集您看不到堆栈指针,它以任何方式都看不到。


coo*_*sed 5

好吧,它们是根本不同的概念。它们都包含内存地址,但是请记住,指令和数据都(有效地)保存在相同的内存空间中。

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

堆栈指针包含的顶端地址硬件堆栈,这是的存储器区域,该运行的代码的用途作为暂存器。值临时存储在此处,函数的参数有时放置在此处,代码地址也可以存储在此处(例如,当一个函数调用另一个函数时)。

  • 的确,这一切都是真的。但是考虑到 OP 问题的级别,我想我会跳过一些细节。:-) 我已经使用了一些架构,其中故意读取 PC 确实报告了实际指令地址,尽管(内部硬件算术魔术)和其他值取决于管道长度的架构。但是,除非您真的要读取并依赖 PC 值,否则就所有意图和目的而言,它都是指向当前指令的指针。 (2认同)