保存 Caller saved 寄存器 的动作,是硬件行为,还是软件行为?

320 天前
 amiwrong123

我们知道 arm 体系或 x86 体系中(我比较关心 arm ),在进行函数调用时,会区分 Caller saved 和 Callee saved (就是把通用寄存器划分为两个范围)。

软件行为我是指:我在汇编代码中能够看到保存的过程(就是入栈和出栈)。比如在被调用的函数( Callee )里如果改变了 Callee saved 寄存器,那么在 这个函数里的开头结尾,就会分别出一个 入栈保存和出栈恢复 的操作(如果没有改变 Callee saved 寄存器,那么就不会多这两个操作)。

硬件行为我是指:是 CPU 自动做的,不是我在汇编代码里面能看到的指令。

1317 次点击
所在节点    程序员
16 条回复
thinkm
320 天前
函数调用是硬件做的,例如像是 RTOS 之类的操作系统入栈出栈是软件做的。
多年没搞了,记忆中是这样
leonshaw
320 天前
软件行为,caller 自己才知道哪些需要保存
bugu1986
320 天前
os 做的
bugu1986
320 天前
trap 把执行权从硬件给软件
feather12315
320 天前
一部分硬件,一部分软件。

准确地说:
通用寄存是是软件行为,特定的寄存器是硬件行为。
比如函数调用是软件行为,中断异常的寄存器保存( amd64 下的 rip 、返回地址)是硬件行为
feather12315
320 天前
arm 的话,返回地址是 r13 还是 r12 。
这个要查手册页了,programming manual 有详细的介绍( arm 的手册很多,要仔细找找,有个 guide 简述了上述过程
amiwrong123
320 天前
@thinkm
@leonshaw
@bugu1986
@feather12315
之所以我能确信,保存 Callee saved 寄存器的行为,是一个软件行为。是因为我在编译后查看汇编文件,发现大多数函数的实现,在开头部分有 PUSH {r4-r11, lr},在结束部分有 POP {r4-r11, PC}。——即我发现了汇编中的 保存 Callee saved 寄存器的行为。

但我没有找到汇编中,保存 Caller saved 寄存器的行为。虽然我也认为,应该是一个软件行为(最起码在函数调用中,应该是的。在响应中断时,就得另当别论了)。有找到两处 POP {r0}的用法( r0 是 Caller save 的),但也不是正常的函数调用时在用(我希望是调用了 POP {r0}后,马上调用函数,但并不是这么用的),其中一个用的地方是__rt_exit 和_aeabi_uldivmod
amiwrong123
320 天前
@thinkm
@leonshaw
@bugu1986
@feather12315

另外,我猜是不是应该这么理解?

保存 Caller saved 寄存器的行为是一个可选项,如果在函数调用后 不需要使用到 Caller saved 寄存器,那么在调用之前,也就不需要有保存 Caller saved 寄存器的行为了。

Caller saved registers–– If the
data in these registers needs to be used after a C function call, the caller needs to save it
before calling a C function.
文档原话如上。
leonshaw
320 天前
@amiwrong123 对,不用就不需要保存,callee-saved 如果不去动它也不需要保存。区别是 callee-saved 在入口和出口保存 /恢复一次,caller-saved 每次调用前后都要保存 /恢复。寄存器分配算法一般会尽量避免这个开销。
amiwrong123
320 天前
@feather12315
特定的寄存器是硬件行为
=======
确实,汇编中找不到更新 LR 寄存器的指令(即函数调用前,把函数调用的下一个指令 作为返回地址)。但这个事应该是硬件来做的。
feather12315
320 天前
@amiwrong123 #8 right 准确地来讲这是被优化后的代码,如果禁止优化的话 O0 应该就不这样了(没测试,可以看看)


@amiwrong123 #10 是的。这个要查手册页才能确定,而且贼琐碎。
churchmice
320 天前
你都说堆栈了,这个东西硬件是无法帮你保存的,save/restore 都是软件来做的,这个就是俗称的 context switch 了

硬件的话有一种叫做 banking 机制,单这个东西更多的是中断处理相关,比如 cpu 的 r0,r1...r15 寄存器,硬件可以帮你做两套,典型的在 FIQ 发生的时候,硬件进 FIQ 的时候有单独的 r0-r15 寄存器,所以软件就无需自己把 r0-r15 save 到堆栈上面

总结一下
硬件何德何能能知道你的堆栈开在什么地方,在堆栈上的 save/restore 都是需要软件调用指令来完成的,硬件唯一能做的是 banking,在进 FIQ 的时候能让 sw 不用把 r0-r15 存到堆栈上面去

当然,的确也有人做 hw based save/restore,主要是嫌弃 context switch 的时候软件做太慢了,但这种都是私有实现
detached
320 天前
肯定是软件做的。这个问题主要出在汇编里面的函数之间相互调用。硬件是没有办法知道的,只能是软件。这里的软件具体是编译器,在生成代码的时候写入对应的汇编语句。
如果是你自己写的汇编程序,是不需要考虑这个问题的,因为你作为 programmer ,是明确知道哪些寄存器的值是不能改变的,哪些是临时的可以被服用。
简而言之,caller save or callee save 都是 convention ,目的是为了让不同的汇编程序之间相互兼容,对硬件是完全透明的。
detached
320 天前
反驳一下 os 做的这个说法。
os 只有在 context switch 的时候才会保存现场,也就是将所有寄存器的值存在某一块内存( kernel 的栈上)。这里寄存器的保存会发生在同一个进 /线程的函数调用中,与 context switch ,即从一个进 /线程切换到另一个中是两个不同的概念,前者只是正常的函数调用,后者需要 system call trap 到 kernel 中。
bugu1986
320 天前
@amiwrong123 可选,但为了函数调用一定得加
mengzhuo
319 天前
clobber 是软件行为,楼上说的 os 是不对的。

op 你说的是 ps ABI 的行为,手册在这 https://github.com/ARM-software/abi-aa/releases/download/2023Q1/aapcs32.pdf (仔细读 6.3 章) 。说大家都一样的,你看看隔壁 Go 用不用这个 ABI ?

而且多看看历史就知道为啥了,早期比如 6065 就 1 个累加寄存器( ALU ) 2 个辅助寄存器,SP 、PC 、FLAG 一共 6 个寄存器,各种数据就尽量塞内存上的,比如栈地址,程序返回地址什么的。
后来寄存器越来越多才导致了部分数据可以直接放寄存器上,渐渐才有了今天这样 ps ABI 的结果。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/951071

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX