0x00 前言
在逆向 Golang 程序时,发现调用约定和平时遇到的 C/C++ 不太一样,ida 反编译效果也不咋样,于是研究一番。
本次分析的 go 版本为 go1.19.3:
1
2
|
> go version
go version go1.19.3 windows/amd64
|
0x01 寄存器还是栈
Go internal ABI specification [2] 中给出了函数调用时参数和返回值传递的规范,总的来说:使用寄存器和栈混合放置的方式,优先使用寄存器,不适用的情况使用栈。
具体使用寄存器还是栈取决于值的类型,每个参数/返回值,要么存在栈上要么存在寄存器上,不存在混合情况。
为了适配不同的架构,这里用 R0, R1, R2 … Ri 来表示某个架构的寄存器列表,NI 和 NFP 表示架构定义的整数和浮点寄存器序列的长度。
在 amd64 上,对于的真实寄存器序列是:RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11
参考:在 windows x64 上,函数调用传递的参数分别使用:rcx, rdx, r8, r9,放不下的放到栈上 [1]
考虑函数参数以及返回值中的各项,假设每项类型为 T,值为 V,它应该分配到哪里?策略如下:(起始 i=0)
- 如果 T 是 boolean、整数,分配给寄存器 Ri,i++
- 如果 T 是适合两个寄存器的整数类型,LSB 一半分给 Ri,MSB 一半分给 R(i+1), i+=2
- 如果 T 是浮点类型并且可以在浮点寄存器中不损失精度地表示,分配给寄存器 FP 并递增 FP
- 如果 T 是复数类型,递归地寄存器分配它的实部和虚部。
- 如果 T 是指针类型、map 类型、chan 类型或函数类型,则分配给寄存器 Ri,i++。
- 如果 T 是 string 类型、interface 类型或 slice 类型,使用多个寄存器分配
- string 和 interface 用 2 个寄存器
- slice 用 3 个寄存器,放置 ptr, len, cap 三个部分。(https://go.dev/blog/slices-intro)
- 如果 T 是结构类型,则递归分配每个字段。
- 如果 T 是数组:
- len(T) == 0 : 不分配
- len(T) == 1 : 递归分配这一个元素
- len(T) > 1 : 使用栈分配
- 如果 i >= NI 或者 FP >= NFP ,使用栈分配
注意不要混淆的 slice 和数组的情况
0x02 栈的分配
caller 调用 callee 时,调用者(caller)分配栈空间,按照上述规则部分参数放于寄存器中,其他放在栈里面。
如下面的示意图所示,栈空间分成四大部分:
- 寄存器参数 spill space:每个放在寄存器中的参数都相应地预留这个区域。(好像是和编译器优化有关)
- 存放到栈中的部分返回值
- 存放到栈中的部分参数
- receiver
在调用开始时:spill space, 返回值区域都是未初始化状态, callee 最后会把返回值填入寄存器和 result stack 中。
备注:receiver 指的是 *T,详见:https://go.dev/tour/methods/4
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
|
+------------------------------+
| . . . |
| 2nd reg argument spill space |
| 1st reg argument spill space |
| <pointer-sized alignment> |
| . . . |
| 2nd stack-assigned result |
| 1st stack-assigned result |
| <pointer-sized alignment> |
| . . . |
| 2nd stack-assigned argument |
| 1st stack-assigned argument |
| stack-assigned receiver |
+------------------------------+ ↓ lower addresses
中文版:
+------------------------------+
| . . . |
| 2nd 寄存器参数 spill 空间 |
| 1st 寄存器参数 spill 空间 |
| . . . |
| 2nd 存在栈中的返回值 |
| 1st 存在栈中的返回值 |
| . . . |
| 2nd 存在栈中的参数 |
| 1st 存在栈中的参数 |
| 存在栈中的 receiver |
+------------------------------+ ↓ lower addresses
|
0x03 例子
示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// [...]
xx := []uintptr{1, 2, 3, 4}
r1, r2 := f(6, [2]uintptr{7, 8}, 15, xx)
// [...]
}
func f(a1 uint8, a2 [2]uintptr, a3 uint8, a4 []uintptr) (r1 R1 , r2 string){
r1 = R1 {
x:12,
y: [2]uintptr{11,13},
}
r2 = "abc"
return
}
|
对照着下面的反汇编结果(windows/amd64),查看参数传递情况:
- uint8 属于整数类型,a1 放入 rax 中
- a2 类型是整数数组,且长度大于 0,放入栈中
- a3 也是 uint8,放入 rbx
- a4 类型是 slice,分三部分(ptr, len, cap)放入寄存器(rcx, rdi, rsi) 中
1
2
3
4
5
6
7
8
9
10
11
12
13
|
.text:000000000046F2AE mov qword ptr [rsp+158h+var_90], 1
.text:000000000046F2BA mov qword ptr [rsp+158h+var_90+8], 2
.text:000000000046F2C6 mov qword ptr [rsp+158h+var_80], 3
.text:000000000046F2D2 mov qword ptr [rsp+158h+var_80+8], 4
.text:000000000046F2DE mov [rsp+158h+var_158], 7 ; a2[0]
.text:000000000046F2E6 mov [rsp+158h+var_150], 8 ; a2[1]
.text:000000000046F2EF mov eax, 6 ; a1
.text:000000000046F2F4 mov ebx, 0Fh ; a3
.text:000000000046F2F9 lea rcx, [rsp+158h+var_90] ; a4.ptr
.text:000000000046F301 mov edi, 4 ; a4.len
.text:000000000046F306 mov rsi, rdi ; a4.cap
.text:000000000046F309 call main_f ;; 函数调用
|
相应地,查看返回值传递情况:
- r1 是结构体,根据成员进行分配,r1.y 是字符串,所以 r1 作为一个整体需要放到栈上
- r2 是字符串,放入 rax,rbx 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
.text:000000000046F540 main_f proc near ; CODE XREF: main_main+2A9↑p
.text:000000000046F540
.text:000000000046F540 arg_10 = xmmword ptr 18h
.text:000000000046F540 arg_20 = qword ptr 28h
.text:000000000046F540
.text:000000000046F540 movups [rsp+arg_10], xmm15
.text:000000000046F546 movups [rsp+arg_10+8], xmm15
.text:000000000046F54C mov qword ptr [rsp+arg_10], 0Ch ; [rsp+0x18] r1
.text:000000000046F555 mov qword ptr [rsp+arg_10+8], 0Bh ; [rsp+0x20]
.text:000000000046F55E mov [rsp+arg_20], 0Dh ; [rsp+0x28]
.text:000000000046F567 lea rax, unk_481BFE("abc") ; r2
.text:000000000046F56E mov ebx, 3
.text:000000000046F573 retn
.text:000000000046F573 main_f endp
|
0x04 参考