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

标签 arm microprocessors program-counter stack-pointer

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

最佳答案

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),程序计数器未修改,因此下一次提取从 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 开始向下增长,而不是踩在我们的便笺簿上。

现在我们获取 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 进行编码的通用存储倍数。

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

关于arm - 堆栈指针和程序计数器有什么区别?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51928246/

相关文章:

rust - 为什么在将自定义目标添加到rust(arm-unknown-linux-uclibceabihf)时得到VFP注册参数?

c - ADC 模数转换

c - 访问内核模块中进程的预定指令

c - 用strtok拆分字符串然后保存错误

linux-kernel - 英特尔处理器 : "If CPUID.06H:EAX.[7] = 1" Meaning?

syntax-error - verilog程序计数器语法错误

c++ - 代码如何在 C++ 抽象机上存储和执行?

ubuntu - ARM 嵌入式的交叉编译

android - 如何在 Android 设备上运行 VS 代码

汇编中的C变量使用,如何在arm aarch64中选择32位操作数