背景
在 arm64 环境,go 无法通过 fp 进行堆栈展开, github.com/golang/go/i…
目前最新的 ebpf agent,通过 eh_frame 格式进行堆栈展开,也不支持 go: github.com/parca-dev/p…
通过 eh_frame 进行堆栈展开的原理: www.polarsignals.com/blog/posts/…
通过 debug_frame 可以支持 arm64 环境下的 go 堆栈展开。
实现
程序:
package main
func main() {
a()
}
func a() {
b()
}
禁止内联优化:
go build -gcflags="all=-l"
汇编:
TEXT main.main(SB) /root/testjson/main.go
func main() {
0x6eec0 f9400b90 MOVD 16(R28), R16
0x6eec4 eb3063ff CMP R16, RSP
0x6eec8 54000109 BLS 8(PC)
0x6eecc f81f0ffe MOVD.W R30, -16(RSP)
0x6eed0 f81f83fd MOVD R29, -8(RSP)
0x6eed4 d10023fd SUB $8, RSP, R29
a()
0x6eed8 9400000a CALL main.a(SB)
}
0x6eedc a97ffbfd LDP -8(RSP), (R29, R30)
0x6eee0 910043ff ADD $16, RSP, RSP
0x6eee4 d65f03c0 RET
func main() {
0x6eee8 aa1e03e3 MOVD R30, R3
0x6eeec 97fff591 CALL runtime.morestack_noctxt.abi0(SB)
0x6eef0 17fffff4 JMP main.main(SB)
0x6eef4 00000000 ?
0x6eef8 00000000 ?
0x6eefc 00000000 ?
TEXT main.a(SB) /root/testjson/main.go
func a() {
0x6ef00 f9400b90 MOVD 16(R28), R16
0x6ef04 eb3063ff CMP R16, RSP
0x6ef08 54000109 BLS 8(PC)
0x6ef0c f81f0ffe MOVD.W R30, -16(RSP) // 当前 lr 的值,表示的是 0x6eedc,也就是下一条指令地址,将其放入 sp - 16 中,执行后 sp 减小 16,增加了堆栈大小
0x6ef10 f81f83fd MOVD R29, -8(RSP)
0x6ef14 d10023fd SUB $8, RSP, R29
b()
0x6ef18 9400000a CALL main.b(SB) // 执行完这个指令后 lr 的值就变了,lr 的值为 0x6ef1c
}
0x6ef1c a97ffbfd LDP -8(RSP), (R29, R30) // 恢复 R30 的值
0x6ef20 910043ff ADD $16, RSP, RSP // 增大 sp,释放空间
0x6ef24 d65f03c0 RET
func a() {
0x6ef28 aa1e03e3 MOVD R30, R3
0x6ef2c 97fff581 CALL runtime.morestack_noctxt.abi0(SB)
0x6ef30 17fffff4 JMP main.a(SB)
0x6ef34 00000000 ?
0x6ef38 00000000 ?
0x6ef3c 00000000 ?
设置断点:
(gdb) break main.a
Breakpoint 1 at 0x6ef0c: file /root/testjson/main.go, line 7.
(gdb) run
Starting program: /root/testjson/testjson
寄存器输出
Thread 1 "testjson" hit Breakpoint 1, main.a () at /root/testjson/main.go:7
7 func a() {
(gdb) info registers lr
lr 0x6eedc 454364 // 执行完 a 函数后将要执行的地址
(gdb) info registers sp // sp 寄存器的值
sp 0x400003e750 0x400003e750
(gdb) info registers pc // 当前 pc 指令地址,还没有执行过。
pc 0x6ef0c 0x6ef0c <main.a+12>
(gdb) %10
Undefined command: "". Try "help".
(gdb) x/10x $sp #此时0x000442a8为执行完函数 a 后会执行的指令地址。
0x400003e750: 0x000442a8 0x00000000 0x00000000 0x00000000
0x400003e760: 0x0006c704 0x00000000 0x00064000 0x00000040
0x400003e770: 0x00000000 0x00000000
此时可以通过 debug_frame 进行堆栈展开。
debug_frame 对于函数 a 的描述:
左闭右开的地址范围 [,)
0x6ef00 -> 0x6ef10, sp delta 为 0, 通过 lr 链接寄存器展开;
0x6ef10 -> 0x6ef24, sp delta 为 16, 从当前的 sp + 16 访问
0x6ef24 -> 0x63f3c, sp delta 为 0, 通过 lr 链接寄存器展开
如果当前 pc 地址在 0x6ef10, 则通过 lr 寄存器可以获取 nextpc:
nextpc = *lr, 为 0x6eedc。
也就是说,分两种情况:
当函数调用子函数时,会提前分配堆栈空间。
因此:
1,正常情况下,lr 为该函数返回后要执行的下一行指令地址,sp 为当前函数的堆栈。
2,函数调用子函数前后,sp 会减小,分配堆栈空间给子函数,lr 表示的是子函数返回后需要执行的下一条指令地址,之前的 lr 的值,会被保存在堆栈之中。
最大的展开表 unwind row 大小
pc uint64
spDelta int32
fp 堆栈展开
堆栈展开原理:
// fpTracebackPCs populates pcBuf with the return addresses for each frame and
// returns the number of PCs written to pcBuf. The returned PCs correspond to
// "physical frames" rather than "logical frames"; that is if A is inlined into
// B, this will return a PC for only B.
func fpTracebackPCs(fp unsafe.Pointer, pcBuf []uintptr) (i int) {
for i = 0; i < len(pcBuf) && fp != nil; i++ {
// return addr sits one word above the frame pointer
pcBuf[i] = *(*uintptr)(unsafe.Pointer(uintptr(fp) + goarch.PtrSize))
// follow the frame pointer to the next one
fp = unsafe.Pointer(*(*uintptr)(fp))
}
return i
}
- nextfp = *fp
- nextrip = (*(fp + 8)) - 1
需要注意 fp 是一个很抽象的东西,它跟 info registers fp 不是同一个东西。
arm64
在 amd64 上的一个汇编代码:
r30 寄存器就是 lr。 r29 就是 fp.
TEXT main.main(SB) /root/testjson/main.go
main.go:3 0x6d660 f9400b90 MOVD 16(R28), R16
main.go:3 0x6d664 eb3063ff CMP R16, RSP
main.go:3 0x6d668 54000109 BLS 8(PC)
main.go:3 0x6d66c f81f0ffe MOVD.W R30, -16(RSP)
main.go:3 0x6d670 f81f83fd MOVD R29, -8(RSP)
main.go:3 0x6d674 d10023fd SUB $8, RSP, R29
main.go:4 0x6d678 9400000a CALL main.a(SB)
main.go:5 0x6d67c a97ffbfd LDP -8(RSP), (R29, R30)
main.go:5 0x6d680 910043ff ADD $16, RSP, RSP
main.go:5 0x6d684 d65f03c0 RET
main.go:3 0x6d688 aa1e03e3 MOVD R30, R3
main.go:3 0x6d68c 97fff609 CALL runtime.morestack_noctxt.abi0(SB)
main.go:3 0x6d690 17fffff4 JMP main.main(SB)
main.go:3 0x6d694 00000000 ?
main.go:3 0x6d698 00000000 ?
main.go:3 0x6d69c 00000000 ?
TEXT main.a(SB) /root/testjson/main.go
main.go:7 0x6d6a0 f9400b90 MOVD 16(R28), R16
main.go:7 0x6d6a4 eb3063ff CMP R16, RSP
main.go:7 0x6d6a8 54000109 BLS 8(PC)
main.go:7 0x6d6ac f81f0ffe MOVD.W R30, -16(RSP)
main.go:7 0x6d6b0 f81f83fd MOVD R29, -8(RSP)
main.go:7 0x6d6b4 d10023fd SUB $8, RSP, R29
main.go:8 0x6d6b8 9400000a CALL main.b(SB)
main.go:9 0x6d6bc a97ffbfd LDP -8(RSP), (R29, R30)
main.go:9 0x6d6c0 910043ff ADD $16, RSP, RSP
main.go:9 0x6d6c4 d65f03c0 RET
main.go:7 0x6d6c8 aa1e03e3 MOVD R30, R3
main.go:7 0x6d6cc 97fff5f9 CALL runtime.morestack_noctxt.abi0(SB)
main.go:7 0x6d6d0 17fffff4 JMP main.a(SB)
main.go:7 0x6d6d4 00000000 ?
main.go:7 0x6d6d8 00000000 ?
main.go:7 0x6d6dc 00000000 ?
TEXT main.b(SB) /root/testjson/main.go
main.go:11 0x6d6e0 f9400b90 MOVD 16(R28), R16 // 此时 rip 是 0x0006d67c,注意不是 0x6d6bc,也就是说缺少了函数的 parent。
main.go:11 0x6d6e4 eb3063ff CMP R16, RSP
main.go:11 0x6d6e8 54000109 BLS 8(PC)
main.go:11 0x6d6ec f81f0ffe MOVD.W R30, -16(RSP)
main.go:11 0x6d6f0 f81f83fd MOVD R29, -8(RSP)
main.go:11 0x6d6f4 d10023fd SUB $8, RSP, R29 // 执行完该行后是 0x0006d6bc
// 正常来说这里是 b 中实际要执行的代码。
main.go:12 0x6d6f8 9400000a CALL main.c(SB)
main.go:13 0x6d6fc a97ffbfd LDP -8(RSP), (R29, R30) // 执行完该行后又是 0x0006d67c
main.go:13 0x6d700 910043ff ADD $16, RSP, RSP
main.go:13 0x6d704 d65f03c0 RET
main.go:11 0x6d708 aa1e03e3 MOVD R30, R3
main.go:11 0x6d70c 97fff5e9 CALL runtime.morestack_noctxt.abi0(SB)
main.go:11 0x6d710 17fffff4 JMP main.b(SB)
main.go:11 0x6d714 00000000 ?
main.go:11 0x6d718 00000000 ?
main.go:11 0x6d71c 00000000 ?
汇编:
TEXT ·getfp<ABIInternal>(SB),NOSPLIT|NOFRAME,$0
MOVD R29, R0
RET
在 amd64 上:
TEXT ·getfp<ABIInternal>(SB),NOSPLIT|NOFRAME,$0
MOVQ BP, AX
RET
正如注释中所说,通过 fp 堆栈展开可能会丢掉第二个函数。(在 amd64 上也是同样的)
或者实际上也不算。比如 0x6d6a0 - 0x6d6b4 虽然在 TEXT A 之下,但实际上是为执行 a 中真正的函数进行准备,可接受这个误差。