Golang underlying data representaion

Golang underlying data representaion

有些沙雕出题人就是喜欢用 go 来恶心人,做个总结

简单记录一下 Golang 各种数据类型的存储形式、参数传递、返回值传递等等

本文的数据均在 64位 环境下得出.

为了方便观察代码,关掉编译优化

1
2
3
-gcflags "-N -l"

go build -o hw.exe -gcflags "-N -l" HelloWorld.go

函数调用

记住最重要的一点,无论是 x86 还是 x86-64, 都采用栈传递参数,类似 stdcall

返回值传递不通过 eax/rax 等寄存器,也是通过栈。位置是最后一个参数之上。例如最后一个参数的地址是 rsp + 0x8, 则:

  • 第一个返回值: rsp + 0x10
  • 第二个返回值: rsp + 0x18
  • 。。。。。。

返回值(一个或多个)

1
2
3
4
5
6
7
8
9
10
11
12
func testfunc(a int) (i,j int)  {
i = a + 1
j = a + 2
return
}
func main() {
var a, b int
a, b = testfunc(123)
a = a + 1
b = b + 2
fmt.Println(a, b)
}

调用代码如下

1
2
3
4
mov     qword ptr [rsp], 123   ; 参数
call main_testfunc
mov rax, [rsp+8] ; 返回值1
mov rcx, [rsp+10h] ; 返回值2

方法调用

方法还有一个额外的参数,可以理解为 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 定义结构体 */
type Circle struct {
radius float64
}

func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("area is: ", c1.getArea(10))
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea(cnt int) float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius * float64(cnt)
}
1
2
3
4
movsd   qword ptr [rsp], xmm0  ;  Circle struct 
mov qword ptr [rsp+8], 0Ah ; 10
call main_Circle_getArea
movsd xmm0, qword ptr [rsp+10h] ; 获取返回值

可以看出 this 相当于第一个参数。

字符串

1
var stringTest = "flag{}"

内存格式

1
2
3
4
5
6
7
struct String{
char * strPtr;
int64 size;
}

debug060:000000C0000D9F40 dq offset aFlagListenobje ; "flag{}"
debug060:000000C0000D9F48 dq 6

Slice 切片

make

1
slice1 := make([]int, len)

make 函数是一个 builtin 函数,貌似由编译器处理?

slice 结构

1
2
3
4
5
struct slice {
dq dataPtr;
dq len;
dq cap;
}

slice 结构构造代码由编译器生成,相当于内联 inline make 函数。

1
2
3
4
5
6
7
8
go:
slice1 := make([]int, len)

asm:
call runtime_makeslice
mov [rsp+28h], rdx // rdx = makeslice 创建的数组基地址
mov [rsp+30h], rax // rax = len
mov [rsp+38h], rcx // rcx = cap

make 最底层调用 runtime_makeslice 分配 Array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// runtime_makeslice
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}

return mallocgc(mem, et, true)
}

makeslice 返回的是一个指向实际数据的指针(不含管理slice的结构体)相当于 malloc(sizeof(Type) * len)

在访问slice中元素时,一般会检测下标是否小于len, 如果越界则调用runtime_panicIndex

append / copy

1
slice1 = append(slice1, 123)

append 的时候会检测目标 slice1.len + 1 与 slice1.cap 的大小关系

若 slice1.len + 1 > slice1.cap 则调用 runtime_growslice 扩容

copy 就是复制一个新的切片

切片截取

1
myvar := slice1[1:20]

myvar 的数据结构是新一个新的切片 struct.
myvar.dataPtr = &slice1.dataPtr[1]
myvar.len = 20 - 1
myvar.cap = slice1.cap - 1
dataPtr 相当于传的一个引用

range

略,不会调用新函数,逻辑比较简单,佷容易理解

map

赋值

runtime_mapassign_*

1
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer{}

3个参数,第三个参数是key的指针,rsp + 0x8 * 2, 返回值是 key 对应的数据指针.

访问

1
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {}

和赋值同理,区别是这个不会为不存在的 key 创建 key pair.

并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
go say("world")
say("hello")
}

1
2
3
4
5
6
7
8
call    runtime_newproc

rsp=>000000C00010FF58

debug062:000000C00010FF58 dq 10h ; rsp here
debug062:000000C00010FF60 dq offset off_515DD0 ;pointer to main_say
debug062:000000C00010FF68 dq offset aWorld ; "world" 这里开始就是参数了
debug062:000000C00010FF70 dq 5

其它

  • convXXX 类似 assert 的功能,逆向时忽略即可

目前就这么多,以后遇到再补吧。

参考

[1] https://www.runoob.com/go/go-slice.html “Go 语言切片(Slice)”

[2] https://golang.org/src/runtime/slice.gosrc/runtime/slice.go”


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!