问题描述
我的教授布置了家庭作业,事情是这样的。这是 ARM 程序集,想象这是一个空的降序堆栈。这意味着内存地址从高地址移动到低地址,空意味着堆栈指针指向堆栈上方的空白空间。在此示例中,地址在括号中。我会用| |为空的空间。 TOS是栈顶,SP是栈帧的当前位置。
|___| (80)
|___| (84)
|___| (88)
|___| SP (92)
|___| TOS (96)
|___| (100)
这是有问题的代码。我会解释我认为在每一行之后会发生什么
-
STMED sp!,{fp,lr}
(FP为R11,LR为R13。因为低位寄存器进入低位地址,当前值FP存入88,LR存入92。栈为ED栈,所以SP是84,比FP高一点) -
MOV fp,sp
(FP 现在指向与 SP 相同的位置,84。之前 FP 的值存储在位置 88) -
SUB SP,SP,#4
(SP 指向 80) -
STR R3,[fp,#12]
(FP为84,所以R3存放在84+12等于96,替换旧的TOS) -
STR R6,#-4]
(R6 存放在 84-4 即 80)
所以这是我的逻辑,对我来说很有意义,但我的教授说我错了。她说我不应该使用 FP 指向的位置,而是使用堆栈中的 FP 值(在位置 88)。这意味着 R3 将存储在 100 点,R6 将存储在 84 点。她坚持认为这是正确的,并说帧指针一旦放入堆栈就无法更改,并且它是堆栈帧的基础。我明白这一切,但我不明白她的逻辑。我们将值存储在堆栈上,然后将其更改为指向其他内容。为什么我们还在使用旧值?有人可以向我解释一下吗?
解决方法
从你的解释来看,教授似乎是错误的。但是,可能存在误解(即,您误解了问题)。也就是说,编译器将先前的 FP
保存到堆栈(先前的堆栈帧),然后基于更新的堆栈使用新的 FP
是很常见的。这是新的堆栈框架。一种处理溢出到内存的先前函数局部变量,更新后的值用于新的溢出、可变大小的数组等。我相信这在{{3} }.
-
STMED sp!,{fp,lr}
- 为存储先前帧指针和链接寄存器而执行的典型序言操作。这必须在非叶函数中完成。A(){B();} B(){C();} C{}
函数A()
或B()
具有更复杂的主体。 -
MOV fp,sp
- 将帧指针更新为新的堆栈帧(接下来为该例程保留堆栈空间)。 -
SUB SP,SP,#4
- 在堆栈上为新例程保留空间。 -
STR R3,[fp,#12]
- 这不是标准的,因为您通常不会在堆栈历史中回顾到目前为止。当然,在汇编程序中一切都是有效的,但这通常是一个错误。 -
STR R6,#-4]
- 这可能是为了将寄存器保存到堆栈帧中,以便它可以重用于其他计算。
STM
和 LDM
的另一个用途是加载和存储结构和数组数据。但是,它没有与fp
、lr
等组合使用。我认为练习的目的是在32位ARM CPU上展示函数序言和结尾。
我认为您的教授可能有更好的教学时间来描述编译器如何使用这些机制来映射常见的高级/中级构造。
通常,如果您使用汇编程序进行编码,您会尽量避免像瘟疫一样使用堆栈。如果您在汇编程序中使用堆栈,则是为了与更高级别的语言进行交互。 99% 的汇编程序都是叶子函数,不会包含这样的代码。自定义汇编器会尽量使用寄存器。
但是,通常情况下,您希望了解编译器在做什么,并且需要查看生成的汇编程序。对于这个用例,编译器使用的模式类型很重要。
教人是一项艰巨的工作,我认为您只是对教授试图提出的概念有误解。所以我认为没有人可以回答谁是对的问题?希望以上内容可以帮助您了解编译器如何使用堆栈、帧指针和链接寄存器。这是理解 ARM 汇编程序的一个实用方面。