什么是ARM中的SP(堆栈)和LR?


76

我一遍又一遍地阅读定义,但仍然没有得到ARM中的SP和LR?我了解PC(它显示下一条指令的地址),SP和LR可能相似,但我不了解它是什么。请你帮助我好吗?

编辑:如果您可以通过示例进行说明,那就太好了。

编辑:终于弄清楚了LR是什么,仍然没有得到SP是什么。


3
堆栈并非特定于ARM,(几乎)每个处理器和控制器都有一个堆栈。secure.wikimedia.org/wikipedia/en/wiki/Call_stack
starblue

相关:ARM链接和框架指针。框架指针fp与配合使用sp。在x86中fp将为bp; 它也是函数调用中的一个常见概念,即用于保留局部变量的寄存器。
artless噪音

Answers:


88

LR是链接寄存器,用于保存函数调用的返回地址。

SP是堆栈指针。堆栈通常用于在函数调用之间保存“自动”变量和上下文/参数。从概念上讲,您可以将“堆栈”视为“堆积”数据的地方。您将“数据”保持在另一数据之上,而堆栈指针则告诉您“数据”的“高度”。您可以从“堆栈”的“顶部”删除数据并使其更短。

从ARM体系结构参考中:

SP,堆栈指针

寄存器R13用作指向活动堆栈的指针。

在Thumb代码中,大多数指令无法访问SP。唯一可以访问SP的指令是那些旨在将SP用作堆栈指针的指令。不建议将SP用作堆栈指针以外的任何目的。注意:将SP用作堆栈指针以外的任何其他用途都可能会破坏操作系统,调试器和其他软件系统的要求,从而导致它们发生故障。

LR,链接寄存器

寄存器R14用于存储子例程的返回地址。在其他时候,LR可以用于其他目的。

当BL或BLX指令执行子例程调用时,LR设置为子例程返回地址。要执行子程序返回,请将LR复制回程序计数器。在使用BL或BLX指令进入子例程之后,通常可以通过以下两种方式之一来完成此操作:

•使用BX LR指令返回。

•在子例程输入中,使用以下形式的指令将LR存储到堆栈中:PUSH {,LR},并使用匹配的指令返回:POP {,PC} ...

该链接给出了一个简单的子例程的示例。

这是一个示例,说明如何在调用之前将寄存器保存在堆栈中,然后弹出以恢复其内容。


谢谢,终于我意识到LR的用途是什么,尽管仍然没有真正获得SP ...
good_evening 11-11-23

“堆栈”是什么意思?注册?什么?请给我一个简单的SP示例吗?
2011年

1
@hey堆栈是您保存无法放入寄存器的变量的地方。通常,由于堆栈的工作方式而具有局部性的变量。您可以在en.wikipedia.org/wiki/Stack_(abstract_data_type)上了解更多信息。另外,您还在STACKoverflow上,如何不知道它是什么?
耶稣拉莫斯

@嘿,我添加了一些句子来尝试给您一些关于堆栈是什么的直觉。
Guy Sirton

只是想说,不幸的是,您的两个链接现在都已消失。
hak8or 2015年

46

SP是堆栈寄存器,用于键入r13的快捷方式。LR是链接注册r14的快捷方式。PC是程序计数器,用于键入r15的快捷方式。

当您执行称为分支链接指令bl的调用时,返回地址将放在链接寄存器r14中。程序计数器pc更改为您要跳转到的地址。

当您遇到中断时,传统的ARM内核中有一些堆栈指针(cortex-m系列是一个例外),例如您使用的堆栈与在前台运行时使用的堆栈不同,您不必更改代码即可使用正常情况下,sp或r13硬件已为您完成了切换,并在解码指令时使用正确的开关。

传统的ARM指令集(而不是Thumb)使您可以自由使用堆栈,从低地址到高地址或从高地址到低地址。编译器和大多数人都将堆栈指针设置为高,并使其从高地址向下扩展到低地址。例如,也许您的内存从0x20000000到0x20008000,您设置了链接程序脚本以构建程序以运行/使用0x20000000,并在启动代码中将堆栈指针设置为0x20008000,至少是系统/用户堆栈指针,您必须进行划分如果需要/使用其他堆栈的内存。

堆栈只是内存。处理器通常具有特殊的内存读/写指令,这些指令基于PC,有些则基于堆栈。通常至少将堆栈中的堆栈命名为push和pop,但不必如此(与传统的arm指令一样)。

如果您访问http://github.com/lsasim 我创建了一个教学处理器并拥有汇编语言教程。我在那里某个地方进行了有关堆栈的讨论。它不是手臂处理器,但是故事是一样的,应该直接转换为您要在手臂或大多数其他处理器上理解的内容。

假设您的程序中有20个变量,但是只有16个寄存器减去至少三个特殊用途的寄存器(sp,lr,pc)。您将必须将某些变量保留在ram中。假设r5拥有一个您经常使用的变量,以至于您不想将其保留在ram中,但是在一段代码中,您确实需要另一个寄存器来做某事,而r5没有被使用,您可以将r5保存在重用r5进行其他操作时,只需花费最少的精力即可完成堆栈,然后稍后轻松地将其还原。

传统(并非一路走回头)arm语法:

...
stmdb r13!,{r5}
...temporarily use r5 for something else...
ldmia r13!,{r5}
...

stm是存储多个寄存器,一次可以保存多个寄存器,最多可以在一条指令中保存所有寄存器。

db表示递减,这是从高地址到低地址的向下移动堆栈。

您可以在此处使用r13或sp指示堆栈指针。该特定指令不仅限于堆栈操作,还可以用于其他用途。

!表示完成后用新地址更新r13寄存器,这里stm可再次用于非堆栈操作,因此您可能不想更改基址寄存器,请保留!在这种情况下。

然后在方括号{}中列出要保存的寄存器,以逗号分隔。

ldmia相反,ldm表示负载倍数。ia表示之后增加,其余与stm相同

因此,如果您在按下stmdb指令时看到的堆栈指针位于0x20008000处,因为列表中存在一个32位寄存器,它将在使用r13中的值之前将其递减,因此0x20007FFC然后将r5写入内存中的0x20007FFC并保存该值r13中的0x20007FFC。稍后,假设您在进入ldmia指令时没有错误,则r13中包含0x20007FFC,列表r5中只有一个寄存器。因此,它在0x20007FFC处读取内存,将该值放入r5,ia表示递增,因此0x20007FFC将一个寄存器的大小递增至0x20008000,然后加上!表示将该数字写入r13以完成指令。

为什么要使用堆栈而不是仅使用固定的内存位置?好吧,上面的妙处是,运行代码时r13可以是0x20007654或0x20002000或任何其他值,并且代码仍然可以运行,甚至更好,如果您循环或递归使用该代码,并且适用于每个级别进行递归操作后,您将保存一个r5的新副本,根据您在该循环中的位置,可能已保存了30个副本。并在展开时根据需要放回所有副本。与单个固定的内存位置不起作用。作为示例,这直接转换为C代码:

void myfun ( void )
{
   int somedata;
}

在像这样的C程序中,变量some​​data驻留在堆栈中,如果您递归调用myfun,则取决于递归的深度,您将拥有somedata值的多个副本。另外,由于该变量仅在函数内使用,而在其他地方则不需要,因此您可能不想在程序生命周期内为该变量刻录大量系统内存,因此只希望在该函数中使用那些字节,而在该函数中释放这些字节。没有那个功能。这就是堆栈的用途。

在堆栈上找不到全局变量。

回去...

假设您要实现并调用该函数,则在调用myfun函数时将拥有一些代码/函数。myfun函数要在某些对象上运行时希望使用r5和r6,但它不希望废弃使用r5和r6进行调用的任何内容,因此在myfun()期间,您希望将这些寄存器保存在堆栈中。同样,如果您查看分支链接指令(bl)和链接寄存器lr(r14),则只有一个链接寄存器,如果您从一个函数中调用一个函数,则每次调用时都需要保存该链接寄存器,否则您将无法返回。

...
bl myfun
    <--- the return from my fun returns here
...


myfun:
stmdb sp!,{r5,r6,lr}
sub sp,#4 <--- make room for the somedata variable
...
some code here that uses r5 and r6
bl more_fun <-- this modifies lr, if we didnt save lr we wouldnt be able to return from myfun
   <---- more_fun() returns here
...
add sp,#4 <-- take back the stack memory we allocated for the somedata variable
ldmia sp!,{r5,r6,lr}
mov pc,lr <---- return to whomever called myfun.

因此,希望您可以看到堆栈使用情况和链接寄存器。其他处理器以不同的方式处理相同的事情。例如,某些函数会将返回值放在堆栈上,当您执行return函数时,它通过从堆栈中拉出值来知道要返回的位置。编译器C / C ++等通常具有“调用约定”或应用程序接口(ABI和EABI是ARM定义的名称)。如果每个函数都遵循调用约定,则将其传递的参数传递给根据约定在正确的寄存器或堆栈中被调用的函数。并且每个函数都遵循关于哪些寄存器不必保留其内容以及哪些寄存器必须保留其内容的规则,那么您可以让函数调用函数调用函数并执行递归和各种操作,只要堆栈的深度不深,无法运行到用于全局变量和堆的内存中,因此,您可以整天调用函数并从中返回。上面的myfun实现与您将看到编译器产生的实现非常相似。

现在,ARM有许多内核,而一些指令集则使cortex-m系列的工作方式稍有不同,只要没有一堆模式和不同的堆栈指针即可。在拇指模式下执行拇指指令时,您将使用推入和弹出指令,这些指令没有给您自由使用任何寄存器(如stm)的权利,它仅使用r13(sp),并且不能仅保存所有寄存器的特定子集。流行的手臂装配器使您可以使用

push {r5,r6}
...
pop {r5,r6}

在手臂代码以及拇指代码中。对于手臂代码,它将对正确的stmdb和ldmia进行编码。(在拇指模式下,您也没有选择何时和何处使用db的信息,在db之前递减,在ia后面使用ia递增)。

不,您不必使用相同的寄存器,也不必配对相同数量的寄存器。

push {r5,r6,r7}
...
pop {r2,r3}
...
pop {r1}

假设在这些指令之间没有其他堆栈指针修改,如果您还记得sp将要递减的12个字节,则可以说从0x1000到0x0FF4,将r5写入0xFF4,将r6写入0xFF8,将r7写入0xFFC指针将变为0x0FF4。第一个弹出窗口将取0x0FF4的值并将其放入r2,然后将其取值0x0FF8并将其放入r3,堆栈指针将获得值0x0FFC。之后的最后一个弹出窗口,读取的sp是0x0FFC,并将值放在r1中,然后堆栈指针将获得值0x1000,并在此开始。

《 ARM ARM,ARM体系结构参考手册》(infocenter.arm.com,参考手册,找到一份适用于ARMv5的文件并下载,这是带有ARM和Thumb指令的传统ARM ARM),其中包含针对ldm和stm ARM指令的伪代码。有关如何使用它们的完整图片。同样,整本书都是关于手臂以及如何编程的。首先,程序员模型章节将引导您遍历所有模式下的所有寄存器,等等。

如果要对ARM处理器进行编程,则应首先确定(确切地说,芯片供应商应该告诉您,ARM不会制造芯片,而是让芯片供应商将其放入其芯片中的内核)确切地确定您拥有哪个内核。然后访问arm网站,找到该系列的ARM ARM,并找到包括修订版在内的特定内核的TRM(技术参考手册)(如果供应商已提供该修订版(r2p0表示修订版2.0(两点零,2p0)),甚至如果有较新的版本,请使用供应商在其设计中使用的手册。并非每个内核都支持TRM告诉您所支持的模式和指令的每个指令或模式。ARM ARM覆盖了该内核所处的整个处理器系列的功能。请注意,ARM7TDMI是ARMv4,而不是ARMv7, ARM9不是ARMv9。ARMvNUMBER是家族名称ARM7,不带av的ARM11是核心名称。较新的内核具有诸如Cortex和mpcore之类的名称,而不是ARMNUMBER,从而减少了混乱。当然,他们不得不通过制造ARMv7-m(cortex-MNUMBER)和ARMv7-a(Cortex-ANUMBER)来消除混乱,它们是非常不同的系列,一个用于重型负载,台式机,笔记本电脑等,另一个用于用于咖啡机上的微控制器,时钟和指示灯闪烁之类的东西。google beagleboard(Cortex-A)和stm32价值线发现板(Cortex-M)可以体会到这些差异。甚至是使用超过一个千兆赫兹的多个内核或来自nvidia的较新tegra 2的open-rd.org主板,相同的超级缩放器,多个内核,多个千兆赫兹。

抱歉,很长的帖子,希望对您有用。


7
您的github项目吸引我学习组装知识,但看来您的项目已经消失了。您有替代品吗?:)
戴夫2015年

1
我相信该项目的当前地址是github.com/dwelch67/lsasim(从2020年9月7日起生效)。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.