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

时间:2018-08-20 09:57:47

标签: arm microprocessors program-counter stack-pointer

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

2 个答案:

答案 0 :(得分:4)

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

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

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

答案 1 :(得分:3)

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指令集之一进行编译和反汇编。

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

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

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

程序计数器现在包含0x2014,执行0x2010指令。该指令是使用堆栈指针的推送。作为程序员进入此函数时,我们不在乎堆栈指针的确切值是0x2468还是0x4010,我们不在乎。因此,我们只说它包含值/地址sp_start。此推指令使用堆栈来保存两件事,一是链接寄存器lr,r14,即返回地址,当此函数完成时,我们要返回到调用函数。根据该编译器对此指令集使用的调用约定的规则,r4表示必须保留r4,因为如果对其进行修改,则必须将其返回到其被调用时的值。因此,我们将其保存在堆栈中,而不是将x放在堆栈中并在此函数中引用xa次,该编译器选择保存r4中的任何内容(我们不在乎我们只需要保存它),并且使用r4在此函数编译期间保持x不变。我们调用的函数以及它们调用的函数等都将保留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进行提取

从0x201c将pc更改为0x2020,执行x = x | 1,r0是包含函数返回值的寄存器。该指令不会修改程序计数器

从0x2020获取,将PC更改为0x2024,执行弹出。我们尚未修改堆栈指针(另一个保留的寄存器,必须将其放回找到它的位置),因此,我们现在从sp_start-8读取并等于sp_start-8(即sp + 0), 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指令不会修改程序计数器

获取0x2034,将pc设置为0x2038,执行0x2034,就像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将用于被调用方函数了解可用空间在哪里开始。帧指针通常会浪费寄存器,但是某些实现默认情况下会使用它,并且有些指令集是您无法选择的,要达到两倍的指令集,它们将需要使用特定寄存器对堆栈访问进行硬编码,并且偏移仅在一个方向上添加一个正偏移或一个负偏移。在这种情况下,推入实际上是针对通用存储倍数的伪指令,其中寄存器r13编码在其中。

有些指令集您看不到以任何方式都不可见的程序计数器。同样,某些指令集使您无法看到堆栈指针,无论如何它都是不可见的。