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)

  1. 如果 T 是 boolean、整数,分配给寄存器 Ri,i++
  2. 如果 T 是适合两个寄存器的整数类型,LSB 一半分给 Ri,MSB 一半分给 R(i+1), i+=2
  3. 如果 T 是浮点类型并且可以在浮点寄存器中不损失精度地表示,分配给寄存器 FP 并递增 FP
  4. 如果 T 是复数类型,递归地寄存器分配它的实部和虚部。
  5. 如果 T 是指针类型、map 类型、chan 类型或函数类型,则分配给寄存器 Ri,i++。
  6. 如果 T 是 string 类型、interface 类型或 slice 类型,使用多个寄存器分配
    • string 和 interface 用 2 个寄存器
    • slice 用 3 个寄存器,放置 ptr, len, cap 三个部分。(https://go.dev/blog/slices-intro)
  7. 如果 T 是结构类型,则递归分配每个字段。
  8. 如果 T 是数组:
    • len(T) == 0 : 不分配
    • len(T) == 1 : 递归分配这一个元素
    • len(T) > 1 : 使用栈分配
  9. 如果 i >= NI 或者 FP >= NFP ,使用栈分配

注意不要混淆的 slice 和数组的情况

0x02 栈的分配

caller 调用 callee 时,调用者(caller)分配栈空间,按照上述规则部分参数放于寄存器中,其他放在栈里面。

如下面的示意图所示,栈空间分成四大部分:

  1. 寄存器参数 spill space:每个放在寄存器中的参数都相应地预留这个区域。(好像是和编译器优化有关)
  2. 存放到栈中的部分返回值
  3. 存放到栈中的部分参数
  4. 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 参考