为啥ARM PC寄存器指向下一条要执行的指令之后?
Posted
技术标签:
【中文标题】为啥ARM PC寄存器指向下一条要执行的指令之后?【英文标题】:Why does the ARM PC register point to the instruction after the next one to be executed?为什么ARM PC寄存器指向下一条要执行的指令之后? 【发布时间】:2014-07-28 07:09:54 【问题描述】:根据 ARM IC。
在ARM状态下,PC的值是当前指令的地址加上8个字节。
在拇指状态:
对于 B、BL、CBNZ 和 CBZ 指令,PC 的值是当前指令的地址加上 4 个字节。 对于所有其他使用标签的指令,PC 的值是当前指令的地址加上 4 个字节,结果的 bit[1] 清零以使其字对齐。
简单地说,PC寄存器的值指向下一条指令之后的指令。这是我不明白的。通常(特别是在 x86 上)程序计数器寄存器用于指向要执行的下一条指令的地址。
那么,这背后的前提是什么?可能是条件执行?
【问题讨论】:
我相信更熟悉该架构的人可以给出更详细的解释,但简而言之; R15 包含要获取的下一条指令的地址,因为预取它(对于 arm 状态)8 或在某些情况下比当前执行的指令提前 12 个字节。 @JoachimIsaksson 什么情况下R15的值应该是当前指令的地址加上12个字节? @Notlikethat 你可以直接在 x86-64 上阅读 RIP:lea rax, [rip]
。在 x86-32 上,最直接的方法可能是使用 call
指令,以 push
es EIP 作为返回地址。但是,它远没有在 ARM 上那么暴露,它可以是几乎任何指令或寻址模式 IIRC 的 src 或 dst。
@Peter 好吧,我承认 ;) 我想我在这里用“寄存器”的意思是“可以作为指令操作数的东西”,而我的 x86 知识有点淡出 32 位之外SSE2时代...
相关话题:***.com/questions/59404844/…
【参考方案1】:
这是一个令人讨厌的遗留抽象泄漏。
最初的 ARM 设计有一个 3 级流水线(获取-解码-执行)。为了简化设计,他们选择将 PC 读取为当前在取指地址线上的值,而不是 2 个周期前当前正在执行的指令的值。由于大多数 PC 相关地址是在链接时计算的,因此让汇编器/链接器补偿该 2 指令偏移比设计所有逻辑来“纠正”PC 寄存器更容易。
当然,这一切都牢牢地放在“30 年前有意义的事情”堆上。现在想象一下,在今天的 15+ 阶段、多问题、无序流水线中,要在该寄存器中保持有意义的值需要什么,您可能会理解为什么现在很难找到一个认为将 PC 暴露为注册是个好主意。
不过,从好的方面来说,至少它不像delay slots 那样可怕。相反,与您想象的相反,有条件地执行每条指令实际上只是围绕该预取偏移量的另一种优化。当围绕条件代码进行分支时(或者仍然像疯子一样执行管道中剩下的任何内容),您可以完全避免非常短的分支,而不是总是不得不采取管道刷新延迟;管道保持忙碌状态,当标志不匹配* 时,解码后的指令可以作为 NOP 执行。同样,这些天我们有有效的分支预测器,它最终更像是一个障碍而不是帮助,但对于 1985 年来说它很酷。
* "...the instruction set with the most NOPs on the planet."
【讨论】:
喜欢你的回答!请问您对使用 PC 寄存器的最低有效位来确定 CPU 状态有何看法?是不是很奇怪? @newbie 这不是 PC 的 lsb - 这会导致对齐错误 - 它只是bx
、blx
或的 目标地址 的 lsb 或bxj
控制指令集切换的指令。当前状态在 CPSR 的第 5 位中指示。
糟糕!我知道对齐异常,但我认为 lsb 只是在实际获取 PC 寄存器时被忽略。现在清楚了,谢谢! :)
我想知道 ARM 设计人员有多少次诅咒必须让 CPU 与旧行为兼容。此外,与延迟槽一样糟糕的是,关于它们的最糟糕的事情是它们的文档记录很差,尤其是汇编器如何处理它们(汇编器经常试图向你隐藏它们的存在,这似乎是最糟糕/最令人困惑的事情在我看来)。
@supercat 在早期的内核上,Thumb 状态涉及切换或多或少是一个单独的螺栓预解码阶段,它将 Thumb 编码转换为等效的 ARM 编码并将其输入到开始常规 ARM 流水线。注意"Thumb instruction controller",ARM7DMI(没有 T)也是一个东西。 ARMv7 架构(曾经是 Thumb-2 很常见)进行了一次大清理,确实重新定义了大多数对 PC 的写入以实现互通。【参考方案2】:
确实如此……
一个例子如下: C程序:
int f,g,y;//global variables
int sum(int a, int b)
return (a+b);
int main(void)
f = 2;
g = 3;
y = sum(f, g);
return y;
编译成程序集:
00008390 <sum>:
int sum(int a, int b)
return (a + b);
8390: e0800001 add r0, r0, r1
8394: e12fff1e bx lr
00008398 <main>:
int f, g, y; // global variables
int sum(int a, int b);
int main(void)
8398: e92d4008 push r3, lr
f = 2;
839c: e3a00002 mov r0, #2
83a0: e59f301c ldr r3, [pc, #28] ; 83c4 <main+0x2c>
83a4: e5830000 str r0, [r3]
g = 3;
83a8: e3a01003 mov r1, #3
83ac: e59f3014 ldr r3, [pc, #20] ; 83c8 <main+0x30>
83b0: e5831000 str r1, [r3]
y = sum(f,g);
83b4: ebfffff5 bl 8390 <sum>
83b8: e59f300c ldr r3, [pc, #12] ; 83cc <main+0x34>
83bc: e5830000 str r0, [r3]
return y;
83c0: e8bd8008 pop r3, pc
83c4: 00010570 .word 0x00010570
83c8: 00010574 .word 0x00010574
83cc: 00010578 .word 0x00010578
见上面LDR的PC值——这里是用来加载变量f,g,y的地址到r3。
83a0: e59f301c ldr r3, [pc, #28];83c4 main+0x2c
PC=0x83c4-28=0x83a8-0x1C = 0x83a8
PC 的值就是当前执行指令的下一条指令。由于ARM使用32位指令,但它使用字节地址,所以+ 8表示8字节,两条指令的长度。
如此附加 ARM 架构的 5 阶段管道 linefetch、decode、execute、memory、writeback
ARM's 5 stage pipeline
PC 寄存器每个时钟加 4,所以当指令冒泡执行时——当前指令,PC 寄存器已经过了 2 个时钟!现在是+8。这实际上意味着:PC指向“获取”指令,当前指令意味着“执行”指令,所以PC意味着下一个要执行的指令。
顺便说一句: 图片来自 Harris 的《数字设计与计算机架构 ARM 版》一书
【讨论】:
OP 询问为什么会这样,而不是是否正确以上是关于为啥ARM PC寄存器指向下一条要执行的指令之后?的主要内容,如果未能解决你的问题,请参考以下文章