如果修改信号处理程序中的 ctx.rip 和 ctx.rsp 会发生什么
What happens if ctx.rip and ctx.rsp in a signal handler is modified
众所周知,程序被信号中断并进入内核space然后切换到用户space信号处理程序。信号处理完成后,将重新进入内核space,然后切换回被中断的地方。
我最近正在阅读 go 1.14 中新实现的异步抢占,它使用 OS 信号来中断 "non-preemptive" 用户 goroutine。我正在调试非常简单的程序:
package main
import (
"runtime"
"time"
)
func tightloop() {
for {
}
}
func main() {
runtime.GOMAXPROCS(1)
go tightloop()
time.Sleep(time.Millisecond)
println("OK")
runtime.Gosched()
}
在 Go 1.14 中,当抢占信号到达时,tightloop
将被 OS 中断并进入预先配置的信号处理程序 runtime·sigtramp
:
TEXT runtime·sigtramp(SB),NOSPLIT,
MOVQ DX, ctx-56(SP)
MOVQ SI, info-64(SP)
MOVQ DI, signum-72(SP)
MOVQ $runtime·sigtrampgo(SB), AX
CALL AX
RET
sigtrampgo
最终调用 sighandler
。
//go:nosplit
//go:nowritebarrierrec
func sigtrampgo(sig uint32, info *siginfo, ctx unsafe.Pointer) {
(...)
setg(g.m.gsignal)
(...)
sighandler(sig, info, ctx, g)
setg(g)
(...)
}
据我阅读 sighandler
函数,它调用 doSigPreempt
并修改从系统内核传递的 ctx
,并将 rip
设置为序言runtime.asyncPreempt
.
//go:nowritebarrierrec
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
_g_ := getg()
c := &sigctxt{info, ctxt}
(...)
if sig == sigPreempt {
doSigPreempt(gp, c)
}
}
func doSigPreempt(gp *g, ctxt *sigctxt) {
if canPreempt {
// here modifies the rip and rsp
ctxt.pushCall(funcPC(asyncPreempt))
}
(...)
}
但是,我注意到 asyncPreempt 并没有立即执行
信号处理程序已完成,而是:
morestack
或 morestack_noctxt
在 sighandler
被 返回后调用 (不进入结语或序言) ,它调用 newstack
并检查抢占标志并进入调度循环,因此安排主 goroutine 完成异步抢占。
OK
执行前的输出asyncPreempt
这是我在运行时插入的打印日志:
mstart1 call schedule()
enter schedule()
park_m call schedule()
enter schedule()
mstart1 call schedule()
enter schedule()
mstart1 call schedule()
enter schedule()
park_m call schedule()
enter schedule()
park_m call schedule()
enter schedule()
park_m call schedule()
enter schedule()
mstart1 call schedule()
enter schedule()
park_m call schedule()
enter schedule()
rip: 17149264 eip: 824634034136
before pushCall asyncPreempt
after pushCall asyncPreempt
rip: 17124704 eip: 824634034128 // rip points to asyncPreempt
calling newstack: m0, g0 // how could newstack is called?
newstack call gopreempt_m
gopreempt_m call goschedImpl
goschedImpl call schedule()
enter schedule()
OK
gosched_m call goschedImpl
goschedImpl call schedule()
enter schedule()
asyncPreempt2
asyncPreempt2
asyncPreempt2
asyncPreempt2
preemptPark
gopreempt_m call goschedImpl
goschedImpl call schedule()
enter schedule()
虽然我检查了转储的汇编代码,但没有堆栈拆分检查
asyncPreempt
或 sigtramp
.
抱歉说来话长,我的问题是:
- 运行时何时、谁以及如何在
sighandler
之后调用 morestack
?我错过了什么?
- 修改
ctx
是否会改变程序在完成信号处理程序后跳转到修改后的rip
指令?
非常感谢您阅读问题并感谢 go 团队构建了如此出色的功能。
我已经弄明白了,非常感谢 Ian 的提示:
https://groups.google.com/forum/#!topic/golang-nuts/BA7Dqp_zcwk
根本原因似乎类似于 "uncertainty principle"。
作为观察者,通过在 asyncPreempt
中添加 println
调用
以及 asyncPreempt2
影响实际行为
信号处理后。 println
涉及堆栈拆分检查,
调用 morestack
.
我花了一段时间才意识到 morestack
存储了它的调用者
g.m.morebuf.pc
中的 pc 自 newstack
中的 getcallerpc
以来
总是 returns 来自 morestack
的电脑,这并没有说明
信息太多了。
//go:nosplit
func asyncPreempt2() {
// println("asyncPreempt2 is called") // comment here omits calling morestack.
gp := getg()
gp.asyncSafePoint = true
if gp.preemptStop {
mcall(preemptPark)
} else {
mcall(gopreempt_m)
}
println("asyncPreempt2 finished")
gp.asyncSafePoint = false
}
TLDR:在信号处理程序之后,内核恢复 asyncPreempt
的 rip
并直接切换到它,runtime.asyncPreempt
和 runtime.sigtramp
之间没有任何反应.
众所周知,程序被信号中断并进入内核space然后切换到用户space信号处理程序。信号处理完成后,将重新进入内核space,然后切换回被中断的地方。
我最近正在阅读 go 1.14 中新实现的异步抢占,它使用 OS 信号来中断 "non-preemptive" 用户 goroutine。我正在调试非常简单的程序:
package main
import (
"runtime"
"time"
)
func tightloop() {
for {
}
}
func main() {
runtime.GOMAXPROCS(1)
go tightloop()
time.Sleep(time.Millisecond)
println("OK")
runtime.Gosched()
}
在 Go 1.14 中,当抢占信号到达时,tightloop
将被 OS 中断并进入预先配置的信号处理程序 runtime·sigtramp
:
TEXT runtime·sigtramp(SB),NOSPLIT,
MOVQ DX, ctx-56(SP)
MOVQ SI, info-64(SP)
MOVQ DI, signum-72(SP)
MOVQ $runtime·sigtrampgo(SB), AX
CALL AX
RET
sigtrampgo
最终调用 sighandler
。
//go:nosplit
//go:nowritebarrierrec
func sigtrampgo(sig uint32, info *siginfo, ctx unsafe.Pointer) {
(...)
setg(g.m.gsignal)
(...)
sighandler(sig, info, ctx, g)
setg(g)
(...)
}
据我阅读 sighandler
函数,它调用 doSigPreempt
并修改从系统内核传递的 ctx
,并将 rip
设置为序言runtime.asyncPreempt
.
//go:nowritebarrierrec
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
_g_ := getg()
c := &sigctxt{info, ctxt}
(...)
if sig == sigPreempt {
doSigPreempt(gp, c)
}
}
func doSigPreempt(gp *g, ctxt *sigctxt) {
if canPreempt {
// here modifies the rip and rsp
ctxt.pushCall(funcPC(asyncPreempt))
}
(...)
}
但是,我注意到 asyncPreempt 并没有立即执行 信号处理程序已完成,而是:
morestack
或morestack_noctxt
在sighandler
被 返回后调用 (不进入结语或序言) ,它调用newstack
并检查抢占标志并进入调度循环,因此安排主 goroutine 完成异步抢占。OK
执行前的输出asyncPreempt
这是我在运行时插入的打印日志:
mstart1 call schedule()
enter schedule()
park_m call schedule()
enter schedule()
mstart1 call schedule()
enter schedule()
mstart1 call schedule()
enter schedule()
park_m call schedule()
enter schedule()
park_m call schedule()
enter schedule()
park_m call schedule()
enter schedule()
mstart1 call schedule()
enter schedule()
park_m call schedule()
enter schedule()
rip: 17149264 eip: 824634034136
before pushCall asyncPreempt
after pushCall asyncPreempt
rip: 17124704 eip: 824634034128 // rip points to asyncPreempt
calling newstack: m0, g0 // how could newstack is called?
newstack call gopreempt_m
gopreempt_m call goschedImpl
goschedImpl call schedule()
enter schedule()
OK
gosched_m call goschedImpl
goschedImpl call schedule()
enter schedule()
asyncPreempt2
asyncPreempt2
asyncPreempt2
asyncPreempt2
preemptPark
gopreempt_m call goschedImpl
goschedImpl call schedule()
enter schedule()
虽然我检查了转储的汇编代码,但没有堆栈拆分检查
asyncPreempt
或 sigtramp
.
抱歉说来话长,我的问题是:
- 运行时何时、谁以及如何在
sighandler
之后调用morestack
?我错过了什么? - 修改
ctx
是否会改变程序在完成信号处理程序后跳转到修改后的rip
指令?
非常感谢您阅读问题并感谢 go 团队构建了如此出色的功能。
我已经弄明白了,非常感谢 Ian 的提示:
https://groups.google.com/forum/#!topic/golang-nuts/BA7Dqp_zcwk
根本原因似乎类似于 "uncertainty principle"。
作为观察者,通过在 asyncPreempt
中添加 println
调用
以及 asyncPreempt2
影响实际行为
信号处理后。 println
涉及堆栈拆分检查,
调用 morestack
.
我花了一段时间才意识到 morestack
存储了它的调用者
g.m.morebuf.pc
中的 pc 自 newstack
中的 getcallerpc
以来
总是 returns 来自 morestack
的电脑,这并没有说明
信息太多了。
//go:nosplit
func asyncPreempt2() {
// println("asyncPreempt2 is called") // comment here omits calling morestack.
gp := getg()
gp.asyncSafePoint = true
if gp.preemptStop {
mcall(preemptPark)
} else {
mcall(gopreempt_m)
}
println("asyncPreempt2 finished")
gp.asyncSafePoint = false
}
TLDR:在信号处理程序之后,内核恢复 asyncPreempt
的 rip
并直接切换到它,runtime.asyncPreempt
和 runtime.sigtramp
之间没有任何反应.