go-1.17+ 调用规约

go-1.17是一个很不友好的版本,这里我指的是函数调用规约的变更。在此之前,虽然栈传参比较奇怪,但是在掌握了规律后,参数信息很好获取。升级到go-1.17之后,笔者发现变更后的寄存器传值方式并不是系统的调用规约,至少和C/C++的是完全不一致的。这个问题使得笔者在处理ebpf方案时,始终无法覆盖go-1.17+的版本。虽然短期不会造成影响,线上服务使用的大多还在go-1.16以下,但是这始终是一个绕不过去的问题。近期通过查阅资料和参考其他开源项目里对这部分内容的处理,整理了一下go-1.17+的调用规约。

go1.17之前使用的是内存栈来传递参数,这种传参的方式使得golang的语言设计很灵活:golang函数的多返回值能够很容易的实现。同样的,由于golang需要这样灵活的能力,是的系统默认的调用规约方式并不适用。在Proposal: Register-based Go calling convention文章里对这个问题进行了详细的讨论,总结起来是golang的特性使得使用系统默认规约并不能带来多语言交互上的收益,且golang希望保持独特。
本文下面会给出总结的调用规约,并且给出验证程序。本文档的整理所基于的平台是x86_64centos8系统。其他架构下,寄存器名称可能不同。

# 调用规约

入参:

参数序号标准规约golang规约
1rdirax
2rsirbx
3rdxrcx
4rcxrdi
5r8rsi
6r9r8
7栈传值r9
8栈传值r10
9栈传值r11
10栈传值栈传值

返回值:

参数序号标准规约golang规约
1raxrax
2-rbx
3-rcx
4-rdi
5-rsi
6-r8
7-r9
8-r10
9-r11
10-栈传值

# 验证规约

这个条件是比较好验证的,看下验证代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// go version 1.18
// ./go_18/arg/main.go
package main

import "fmt"

//go:noinline
func longArgs(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 uint64) (r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11 uint64) {

	return a1 + 1, a2 + 2, a3 + 3, a4 + 4, a5 + 5, a6 + 6, a7 + 7, a8 + 8, a9 + 9, a10 + 10, a11 + 11
}

func main() {
	a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 := longArgs(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
	fmt.Println(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11)
}

先生成plan9代码以说明参数传入和参数返回均是使用的寄存器,并且寄存器顺序是一致的(内存传参时也是)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
go build -gcflags "-N -S -l" >> arg.info
# 然后截取部分生成 plan9 汇编
# go_18/arg
"".longArgs STEXT nosplit size=411 args=0x68 locals=0x50 funcid=0x0 align=0x0
	0x0000 00000 	TEXT	"".longArgs(SB), NOSPLIT|ABIInternal, $80-104
	0x0000 00000 	SUBQ	$80, SP
	0x0004 00004 	MOVQ	BP, 72(SP)
	0x0009 00009 	LEAQ	72(SP), BP
	0x000e 00014 	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x000e 00014 	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x000e 00014 	FUNCDATA	$5, "".longArgs.arginfo1(SB)
	0x000e 00014 	MOVQ	AX, "".a1+120(SP)  # 注意这里的读取参数寄存器
	0x0013 00019 	MOVQ	BX, "".a2+128(SP)
	0x001b 00027 	MOVQ	CX, "".a3+136(SP)
	0x0023 00035 	MOVQ	DI, "".a4+144(SP)
	0x002b 00043 	MOVQ	SI, "".a5+152(SP)
	0x0033 00051 	MOVQ	R8, "".a6+160(SP)
	0x003b 00059 	MOVQ	R9, "".a7+168(SP)
	0x0043 00067 	MOVQ	R10, "".a8+176(SP)
	0x004b 00075 	MOVQ	R11, "".a9+184(SP)
	0x0053 00083 	MOVQ	$0, "".r1+64(SP)
	0x005c 00092 	MOVQ	$0, "".r2+56(SP)
	0x0065 00101 	MOVQ	$0, "".r3+48(SP)
	0x006e 00110 	MOVQ	$0, "".r4+40(SP)
	0x0077 00119 	MOVQ	$0, "".r5+32(SP)
	0x0080 00128 	MOVQ	$0, "".r6+24(SP)
	0x0089 00137 	MOVQ	$0, "".r7+16(SP)
	0x0092 00146 	MOVQ	$0, "".r8+8(SP)
	0x009b 00155 	MOVQ	$0, "".r9(SP)
	0x00a3 00163 	MOVQ	$0, "".r10+104(SP)
	0x00ac 00172 	MOVQ	$0, "".r11+112(SP)
	0x00b5 00181 	MOVQ	"".a1+120(SP), DX
	0x00ba 00186 	INCQ	DX
	0x00bd 00189 	MOVQ	DX, "".r1+64(SP)
	0x00c2 00194 	MOVQ	"".a2+128(SP), DX
	0x00ca 00202 	ADDQ	$2, DX
	0x00ce 00206 	MOVQ	DX, "".r2+56(SP)
	0x00d3 00211 	MOVQ	"".a3+136(SP), DX
	0x00db 00219 	ADDQ	$3, DX
	0x00df 00223 	MOVQ	DX, "".r3+48(SP)
	0x00e4 00228 	MOVQ	"".a4+144(SP), DX
	0x00ec 00236 	ADDQ	$4, DX
	0x00f0 00240 	MOVQ	DX, "".r4+40(SP)
	0x00f5 00245 	MOVQ	"".a5+152(SP), DX
	0x00fd 00253 	ADDQ	$5, DX
	0x0101 00257 	MOVQ	DX, "".r5+32(SP)
	0x0106 00262 	MOVQ	"".a6+160(SP), DX
	0x010e 00270 	ADDQ	$6, DX
	0x0112 00274 	MOVQ	DX, "".r6+24(SP)
	0x0117 00279 	MOVQ	"".a7+168(SP), DX
	0x011f 00287 	ADDQ	$7, DX
	0x0123 00291 	MOVQ	DX, "".r7+16(SP)
	0x0128 00296 	MOVQ	"".a8+176(SP), DX
	0x0130 00304 	ADDQ	$8, DX
	0x0134 00308 	MOVQ	DX, "".r8+8(SP)
	0x0139 00313 	MOVQ	"".a9+184(SP), DX
	0x0141 00321 	ADDQ	$9, DX
	0x0145 00325 	MOVQ	DX, "".r9(SP)
	0x0149 00329 	MOVQ	"".a10+88(SP), DX   # 这里使用栈传递a10
	0x014e 00334 	ADDQ	$10, DX
	0x0152 00338 	MOVQ	DX, "".r10+104(SP)
	0x0157 00343 	MOVQ	"".a11+96(SP), DX
	0x015c 00348 	ADDQ	$11, DX
	0x0160 00352 	MOVQ	DX, "".r11+112(SP)  # 这里使用栈传递 a11
	0x0165 00357 	MOVQ	"".r1+64(SP), AX    # 注意这里返回参数的寄存器
	0x016a 00362 	MOVQ	"".r2+56(SP), BX
	0x016f 00367 	MOVQ	"".r3+48(SP), CX
	0x0174 00372 	MOVQ	"".r4+40(SP), DI
	0x0179 00377 	MOVQ	"".r5+32(SP), SI
	0x017e 00382 	MOVQ	"".r6+24(SP), R8
	0x0183 00387 	MOVQ	"".r7+16(SP), R9
	0x0188 00392 	MOVQ	"".r8+8(SP), R10
	0x018d 00397 	MOVQ	"".r9(SP), R11
	0x0191 00401 	MOVQ	72(SP), BP
	0x0196 00406 	ADDQ	$80, SP
	0x019a 00410 	RET

从上面的plan9可以看出来,函数入参和返回值确实是使用寄存器传递的,且寄存器信息是一致的。实际上到这里就足够了。但是笔者还需要确定下使用的寄存器名称并进行验证,因为这些参数是在做ebpf逻辑处理的时候使用的。

# 使用ebpf获取入参并输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 这里表述下依据plan9寄存器符号推测的实际寄存器名称:
#define GO_PARAM1(x) ((x)->rax)
#define GO_PARAM2(x) ((x)->rbx)
#define GO_PARAM3(x) ((x)->rcx)
#define GO_PARAM4(x) ((x)->rdi)
#define GO_PARAM5(x) ((x)->rsi)
#define GO_PARAM6(x) ((x)->r8)
#define GO_PARAM7(x) ((x)->r9)
#define GO_PARAM8(x) ((x)->r10)
#define GO_PARAM9(x) ((x)->r11)

struct event {
    u32 pid;
    u8 comm[64];

    // args
    u64 arg0;
    u64 arg1;
    u64 arg2;
    u64 arg3;
    u64 arg4;
    u64 arg5;
    u64 arg6;
    u64 arg7;
    u64 arg8;
    u64 arg9;
    u64 arg10;
};
SEC("uprobe/main.longArgs")
int uprobe__main_long_args(struct pt_regs *ctx) {
    struct event args={};
    args.pid = bpf_get_current_pid_tgid();
    bpf_get_current_comm(&args.comm, sizeof(args.comm));


    // read args 0-8,从寄存器中获取
    args.arg0 = GO_PARAM1(ctx);
    args.arg1 = GO_PARAM2(ctx);
    args.arg2 = GO_PARAM3(ctx);
    args.arg3 = GO_PARAM4(ctx);
    args.arg4 = GO_PARAM5(ctx);
    args.arg5 = GO_PARAM6(ctx);
    args.arg6 = GO_PARAM7(ctx);
    args.arg7 = GO_PARAM8(ctx);
    args.arg8 = GO_PARAM9(ctx);
    
    // read args 9-10,从栈上获取
    bpf_probe_read(&args.arg9, sizeof(args.arg9), (void*)(PT_REGS_SP(ctx))+8);
    bpf_probe_read(&args.arg10, sizeof(args.arg10), (void*)(PT_REGS_SP(ctx))+16);

    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &args, sizeof(args));
    return 0;
}

编译完成后,启动这部分ebpf监听任务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 启动监听
./go_18 -bin_path ./arg/arg -uprobe main.longArgs
2023/03/03 22:07:32 Listening for events..
# 触发 ./arg/arg 执行
2023/03/03 22:07:46 pid: 756309, comm: arg
2023/03/03 22:07:46 /home/odin/pdliyan/blog/go_18/arg/arg: main.longArgs  value: {Pid:756309 Comm:[97 114 103 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] _:[0 0 0 0] Arg0:1 Arg1:2 Arg2:3 Arg3:4 Arg4:5 Arg5:6 Arg6:7 Arg7:8 Arg8:9 Arg9:10 Arg10:11}
# 请注意这里的参数是和我们的代码一致的。
2023/03/03 22:07:46 event info: {Pid:756309 Comm:[97 114 103 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] _:[0 0 0 0] Arg0:1 Arg1:2 Arg2:3 Arg3:4 Arg4:5 Arg5:6 Arg6:7 Arg7:8 Arg8:9 Arg9:10 Arg10:11}
# 退出
2023/03/03 22:08:38 Received signal, exiting program...

从上面的输出可以确定plan9的寄存器符号和实际寄存器的对应关系是正确的。

以上就验证了困扰笔者的go-17+参数调用规约的问题。可以看到,依旧十分的奇葩。
周末愉快。

Hello, World!
使用 Hugo 构建
主题 StackJimmy 设计