ARM PC寄存器为什么指向下一条要执行的指令后面的那一条指令?

52
根据ARM IC,当处于ARM状态时,PC 的值是当前指令的地址加8字节。而在Thumb状态下:
- 对于 B、BL、CBNZ 和 CBZ 指令,PC 的值是当前指令的地址加4字节。 - 对于所有其他使用标签的指令,PC 的值是当前指令的地址加4字节,结果的第1位被清零以使其字对齐。
简单地说,PC寄存器的值指向下一个指令之后的指令。这是我不理解的事情。通常(特别是在x86上),程序计数器寄存器用于指向将要执行的下一条指令的地址。
那么,这背后的前提是什么呢?也许是条件执行吗?

1
我相信对架构更熟悉的人可以给出更详细的解释,但简而言之; R15 包含下一条要获取的指令的地址,由于预取,它(对于 ARM 状态)比当前执行的指令提前了 8 或在某些情况下是 12 个字节。 - Joachim Isaksson
@JoachimIsaksson 在哪些情况下,R15的值应该是当前指令地址加上12个字节? - newbie
@Notlikethat 在x86-64上,您可以直接读取RIP:lea rax,[rip]。在x86-32上,最直接的方法可能是使用call指令,它将EIP作为返回地址push。不过,在ARM上,它的暴露程度远不及它在任何指令或寻址模式中都可以成为src或dst,如果我没记错的话。 - Peter Cordes
@Peter 好的,我认输了 ;) 我想这里的“寄存器”是指“可以作为指令操作数的东西”,而我的x86知识在32位SSE2时代之后就有点模糊了... - Notlikethat
一个相关的线程:https://stackoverflow.com/questions/59404844/strange-content-when-debugging-some-armv5-assembly-code - smwikipedia
2个回答

83

这是一个糟糕的遗留抽象泄漏问题。

原始的ARM设计有一个三级流水线(取指-译码-执行)。为了简化设计,他们选择让PC寄存器读取当前指令获取地址线上的值,而不是两个周期前正在执行的指令。由于大多数PC相对地址是在链接时计算的,所以更容易让汇编器/链接器来处理这个2个指令的偏移量,而不是设计所有逻辑来“修正”PC寄存器。

当然,这都是“30年前有意义的事情”。现在想象一下,在今天的15+级、多发射、乱序流水线上保持寄存器中有有意义的值需要付出的代价,你可能会明白为什么现在很难找到一个CPU设计师认为将PC寄存器作为一个通用寄存器是一个好主意。

不过,也好过像延迟槽那样可怕。相反,与您所想的相反,使每个指令执行有条件性实际上只是围绕预取偏移量的另一个优化。而不是总是在绕过条件代码时花费流水线刷新延迟(或像疯子一样执行剩下的内容),您可以完全避免非常短的分支;流水线保持繁忙状态,解码的指令在标志不匹配时只需执行NOP。再次说明,现在我们有有效的分支预测器,它最终成为了一个妨碍,但对于1985年来说,这很酷。

*"...在这个星球上具有最多NOP的指令集."


2
喜欢你的答案!我能问一下你对使用PC寄存器的最低有效位来确定CPU状态的看法吗?这不奇怪吗? - newbie
6
@新手 用户所说的不是PC的最低有效位(lsb),因为这样会导致对齐错误,而只有在bxblxbxj指令的目标地址的最低有效位上才会控制指令集切换。当前状态在CPSR的第5位中表示。 - Notlikethat
2
我一直在想,ARM 设计师们为了保持 CPU 与旧行为的兼容性而发过多少次牢骚。此外,尽管延迟槽很糟糕,但最糟糕的是它们的文档记录非常不足,特别是汇编器如何处理它们(汇编器通常试图向您隐藏它们的存在,这似乎是最糟糕/最令人困惑的事情)。 - Michael Burr
@MichaelBurr:使用PC的LSB是否会导致对齐错误取决于是否将获取定义为使用地址[PC]或[PC&~1]。我觉得奇怪的是需要使用BX或BLX而不能使用例如ldr r15,[r0]跳转到由r0标识的存储在内存中的指针。你有任何想法为什么在ARM7-TDMI上需要这样做吗? - supercat
1
@supercat 在早期的核心中,Thumb状态涉及切换到一个更或多或少是单独的插入式预解码阶段,将Thumb编码转换为等效的ARM编码并将其馈送到常规ARM流水线的开头。请注意"Thumb指令控制器",以及ARM7DMI(没有T)也是一件事。 ARMv7架构(一旦Thumb-2变得普遍)进行了大量清理,并重新定义了大多数写入PC以进行交互。 - Notlikethat
显示剩余17条评论

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级流水线是:获取、解码、执行、内存、写回。

ARM的5级流水线

每个时钟周期PC寄存器增加4,因此当指令冒泡到执行——即当前指令时,PC寄存器已经过去了2个时钟周期!现在是+8。实际上这意味着:PC指向“获取”指令,当前指令表示“执行”指令,因此PC表示将要执行的下一个下一个指令。

顺便说一句: 图片来自Harris的《数字设计和计算机体系结构ARM版》。


2
OP问的是“为什么会这样”,而不是它是否正确。 - phuclv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接