首页
论坛
课程
招聘
[原创]Go解析
2022-4-12 22:25 10435

[原创]Go解析

2022-4-12 22:25
10435

Go解析

闲谈: 在IDA7.5有插件支持反编译Go, IDA7.6已经支持的情况下, 为什么写这篇文章呢, 就算能反编译, 也是需要对照汇编的, 对于反编译出来的很多函数, 在调试时也要很快的知道哪些才是真正存储数据的地方, 增加分析效率.

Ready

Go源码下载

https://github.com/golang/go

main_main查找

这里简单提一下main_main的查找
*有符号,直接ctrl+f,IDA中搜索main_main即可

 

*无符号:

  1. 字符串查找

runtime_main调用main_main

  • runtime_mainIDA反编译代码参考

    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
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    void runtime_main()
    {
      PVOID ArbitraryUserPointer; // rax
      __int64 v1; // rdx
      __int64 i; // rax
      __int64 v3; // [rsp+0h] [rbp-50h]
      __int64 v4; // [rsp+0h] [rbp-50h]
      __int64 v5; // [rsp+0h] [rbp-50h]
      __int64 v6; // [rsp+10h] [rbp-40h]
      __int64 v7; // [rsp+20h] [rbp-30h] BYREF
      __int64 v8; // [rsp+28h] [rbp-28h]
      __int64 v9; // [rsp+30h] [rbp-20h]
      __int128 v10; // [rsp+38h] [rbp-18h]
      void *retaddr; // [rsp+50h] [rbp+0h] BYREF
     
      while ( (unsigned __int64)&retaddr <= *(_QWORD *)(*(_QWORD *)NtCurrentTeb()->NtTib.ArbitraryUserPointer + 16LL) )
        runtime_morestack_noctxt();
      v10 = 0LL;
      HIBYTE(v7) = 0;
      v9 = *(_QWORD *)NtCurrentTeb()->NtTib.ArbitraryUserPointer;
      *(_QWORD *)(**(_QWORD **)(v9 + 48) + 304LL) = 0LL;
      runtime_maxstacksize = 1000000000LL;
      runtime_maxstackceiling = 2000000000LL;
      runtime_mainStarted = 1;
      _InterlockedExchange((volatile __int32 *)&unk_5683A0, 1);
      runtime_systemstack((__int64)&off_4D6020);
      ArbitraryUserPointer = NtCurrentTeb()->NtTib.ArbitraryUserPointer;
      ++*(_DWORD *)(*(_QWORD *)(*(_QWORD *)ArbitraryUserPointer + 48LL) + 580LL);
      v1 = *(_QWORD *)NtCurrentTeb()->NtTib.ArbitraryUserPointer;
      *(_QWORD *)(*(_QWORD *)(v1 + 48) + 312LL) = v1;
      *(_QWORD *)(v1 + 216) = *(_QWORD *)(v1 + 48);
      if ( *(_UNKNOWN **)(v9 + 48) != &runtime_m0 )
        goto LABEL_28;
      byte_568678 = 1;
      runtime_nanotime1(v3);
      runtime_runtimeInitTime = v4;
      if ( !v4 )
      {
    LABEL_27:
        runtime_throw((__int64)"nanotime returning zero", 23LL);
    LABEL_28:
        runtime_throw((__int64)"runtime.main not on m0", 22LL);
        runtime_deferreturn(v5);
        return;
      }
      if ( dword_5AB048 )
      {
        qword_5AAE88 = *(_QWORD *)(*(_QWORD *)NtCurrentTeb()->NtTib.ArbitraryUserPointer + 152LL);
        runtime_inittrace = 1;
      }
      runtime_doInit((__int64)&runtime__inittask);
      HIWORD(v7) = 257;
      *((_QWORD *)&v10 + 1) = &off_4D6028;
      *(_QWORD *)&v10 = (char *)&v7 + 6;
      runtime_gcenable();
      v6 = runtime_makechan((__int64)&unk_4B2580, 0LL);
      if ( runtime_writeBarrier )
        runtime_gcWriteBarrier();
      else
        runtime_main_init_done = v6;
      if ( !runtime_iscgo )
        goto LABEL_12;
      if ( !cgo_thread_start )
      {
    LABEL_26:
        runtime_throw((__int64)"_cgo_thread_start missing", 25LL);
        goto LABEL_27;
      }
      if ( !cgo_notify_runtime_init_done )
      {
        runtime_throw((__int64)"_cgo_notify_runtime_init_done missing", 37LL);
        goto LABEL_26;
      }
      runtime_startTemplateThread();
      runtime_cgocall(cgo_notify_runtime_init_done, 0LL, v6);
    LABEL_12:
      runtime_doInit((__int64)&main__inittask);
      runtime_inittrace = 0;
      runtime_closechan(runtime_main_init_done);
      BYTE6(v7) = 0;
      runtime_unlockOSThread();
      if ( !runtime_isarchive && !runtime_islibrary )
      {
        main_main();
        if ( runtime_runningPanicDefers )
        {
          for ( i = 0LL; i < 1000 && runtime_runningPanicDefers; i = v8 + 1 )
          {
            v8 = i;
            runtime_mcall();
          }
        }
        if ( runtime_panicking )
          v7 = runtime_gopark(0LL, 0LL, 4104, 1LL);
        runtime_exit(0);
        while ( 1 )
          MEMORY[0] = 0;
      }
      HIBYTE(v7) = 0;
      runtime_main_func2(v10);
    }

可以利用查找那些runtime_main中的字符串在无符号的Go程序中找到runtime_main,从而找到main_main

 

图片描述

  1. 编译一份有符号的,同位数的Go可执行程序bindiff恢复符号找到runtime_main或一些关键函数

notice:

 

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

 

可关掉编译优化

1
2
3
-gcflags "-N -l"
 
go build -o hello.exe -gcflags "-N -l" hello.go

Go数据结构解析

数值类型

图片描述

字符串

图片描述
Go程序会将字符串全部存放到一块连续的内存当中,使用的时候会利用runtime_convTstring来构造真正的字符串内存格式

1
2
3
4
struct String{
    char * strPtr;   //指向目标字符串的开始地址
    int64 size;      //字符串大小
}

图片描述

数组

数组有三种存储位置,当数组内元素较少时可以直接存于栈上,较多时存于数据区,而当数据会被返回时会存于堆上

1
2
3
4
5
6
7
8
9
10
11
12
13
func ArrDemo() *[3]int {
 
    a := [...]int{1, 2, 3}
 
    b := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7}
 
    c := [...]int{1, 2, 3}
 
    if len(a) < len(b) {return &c}
 
    return nil
 
}

变量a的汇编如下,它直接在栈上定义并初始化:

1
2
3
4
type arrayHeader struct {
    Data uintptr       
    Len int
}

图片描述

 

变量b的汇编如下,它的初始值被定义在了数据段并进行拷贝初始化:

 

图片描述

 

数据拷贝函数

 

图片描述

 

变量c的汇编如下,尽管它和a的值一样,但是它的地址是runtime.newobject函数新返回的,该函数传入的是数据的类型指针,它将在堆上申请空间存放对象实例,返回的是新的对象指针:

 

图片描述

 

经常会遇到的情况是返回一个结构体变量,然后将其赋值给newobject申请的新变量上。
下面也是这类情况

1
x := []int{1, 2, 3, 4, 5}

图片描述
通常会有一个地址作为runtime_newobject的参数,这个地址存放的是我们将要构造的数组的总大小,比如这里qword_234820地址处存放的就是40(5 * 8)

 

runtime_newobject里面会调用runtime_mallocgc分配内存

 

再之后会将runtime_newobject返回的指向这块内存的指针,存放到当前函数栈中的一个局部变量中,以后对这个数组的操作都是通过这个局部变量来的,比如这里的rsp+168

 

再下方就是对那块内存赋值, 也就是对数组初始化

1
y := []float32{1000.0, 2.0, 3.4, 7.0, 50.0}

图片描述

slice切片

内存结构

一个slice是一个数组某个部分的引用。

 

在内存中,它是一个包含3个域的结构体:

1
2
3
4
5
{
    指向slice中第一个元素的指针
    slice的长度
    slice的容量
}

这里解释一下长度和容量的概念:

 

长度:下标操作的上界,如x[i]中i必须小于长度

 

容量:分割操作的上界,如x[i:j]中j不能大于容量

 

图片描述
数组的slice并不会实际复制一份数据,它只是创建一个新的数据结构,包含了另外的一个指针,一个长度和一个容量数据。

 

比如上面的分割表达式x[1:3]并不分配更多的数据:它只是写了一个新的slice结构的属性来引用相同的存储数据。

 

在例子中,长度为2,只有y[0]和y[1]是有效的索引,但是容量为4,y[0:4]是一个有效的分割表达式。

 

图片描述
这里先创建一个数组,然后rax存放的是构造好的数组的首地址,add rax, 8之后就会指向数组的下标1对应的元素,也就是3存放的地址,也就是我们切片[1:3]的第一个元素的指针,之后

 

.text:00000000004A79A8 mov [rsp], rax
.text:00000000004A79AC mov qword ptr [rsp+8], 2
.text:00000000004A79B5 mov qword ptr [rsp+16], 4

 

这里就在内存中构造出来了一个切片的结构

 

调用了runtime_convTslice,返回值是一个指向新分配的内存的指针,这个新分配的内存存放的元素数量就是切片的容量,比如这里就存放了4个元素,[3, 5, 7, 11] (从切片的那个元素开始,往后取容量那么多个),所以这里就是为什么y[0:4]是一个有效的分割表达式。

 

图片描述

make

slice := make([]int, len)

1
2
slice1 := make([]int, 5)
slice1[3] = 66

图片描述
make的底层会调用runtime_makeslice分配Array,真正的切片结构是后面的runtime_convTslice时创建的

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扩容

 

在对slice进行append等操作时,可能会造成slice的自动扩容。其扩容时的大小增长规则是:

  • 如果新的大小是当前大小2倍以上,则大小增长为新大小
  • 否则循环以下操作:如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。

copy 就是复制一个新的切片

切片截取

1
myvar := slice1[1:3]

myvar的数据结构是新一个新的切片 struct.

map

内存结构

Go语言的map使用Hash表作为底层实现,可以在 $GOROOT/src/pkg/runtime/hashmap.goc 找到它的实现,其中 [runtime.hmap](https://draveness.me/golang/tree/runtime.hmap)是最核心的结构体,我们先来了解一下该结构体的内部字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type hmap struct {
    count     int     //map中键值对数量
    flags     uint8   //map当前是否处于写入状态登
    B         uint8   //2的B次幂表示当前map中桶的数量
    noverflow uint16  //map中溢出桶的数量,当溢出桶太多时,map会进行等量扩容
    hash0     uint32  //生成hash的随机数种子
 
    buckets    unsafe.Pointer  //当前map对应的桶的指针
    oldbuckets unsafe.Pointer  //map扩容时指向旧桶的指针,当所有旧桶中的数据转移到新桶时,清空
    nevacuate  uintptr         //扩容时,用于标记当前旧桶中小于nevacute的数据都已经转移到了新桶
 
    extra *mapextra            //存储map的溢出桶
}
 
type mapextra struct {
    overflow    *[]*bmap
    oldoverflow *[]*bmap
    nextOverflow *bmap
}

这个hash结构使用的是一个可扩展哈希的算法,由hash值mod当前hash表大小决定某一个值属于哪个桶,而hash表大小是2的指数,即上面结构体中的2^B。每次扩容,会增大到上次大小的两倍。结构体中有一个buckets和一个oldbuckets是用来实现增量扩容的。正常情况下直接使用buckets,而oldbuckets为空。如果当前哈希表正在扩容中,则oldbuckets不为空,并且buckets大小是oldbuckets大小的两倍。

 

具体的Bucket结构如下所示:

1
2
3
4
5
type bmap struct {
    tophash [8]uint8 //存储Hash值的高8
    data []byte    //key value数据:key/key/key.../value/value/value...
    overflow *bmap    //溢出bucket的地址
}
  • BUCKETSIZE是用宏定义的8,每个bucket中存放最多8个key/value对, 如果多于8个,那么会申请一个新的bucket,并将它与之前的bucket链起来。
  • 按key的类型采用相应的hash算法得到key的hash值。将hash值的低位当作Hmap结构体中buckets数组的index,找到key所在的bucket。将hash的高8位存储在了bucket的tophash中。注意,这里高8位不是用来当作key/value在bucket内部的offset的,而是作为一个主键,在查找时对tophash数组的每一项进行顺序匹配的。先比较hash值高位与bucket的tophash[i]是否相等,如果相等则再比较bucket的第i个的key与所给的key是否相等。如果相等,则返回其对应的value,反之,在overflow buckets中按照上述方法继续寻找。
  • data区存放的是key—value数据,其中keys放在一起,values放在一起,如此存储是为了节省字节对齐带来的空间浪费。例如map[int64]int8。
  • overflow指针指向的是下一个bucket,据此将所有冲突的键连接起来

    逆向中map的常见函数

    字典操作常见的会转换为如下函数

一般fastrand和makemap连用返回一个map,它为一个指针

 

读字典时使用mapaccess1和mapaccess2,写字典时会使用mapassign函数,它返回一个地址,将value写入该地址,另外还比较常见的是对字典进行遍历,会使用mapiterinit和mapiternext配合。

1
2
3
4
5
6
7
8
9
10
11
12
13
func fastrand() uint32
 
func makemap(mapType *byte, hint int, mapbuf *any) (hmap map[any]any)
 
func mapaccess1(mapType *byte, hmap map[any]any, key *any) (val *any)
 
func mapaccess2(mapType *byte, hmap map[any]any, key *any) (val *any, pres bool)
 
func mapassign(mapType *byte, hmap map[any]any, key *any) (val *any)
 
func mapiterinit(mapType *byte, hmap map[any]any, hiter *any)
 
func mapiternext(hiter *any)

mapaccess和mapassign的第一个参数代表字典的类型,因此能很容易知道字典操作参数和返回值的类型。

赋值和访问

赋值

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

3个参数,第三个参数是key的指针,rsp + 16, 返回值是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
package main
 
import "fmt"
 
func main() {
    countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
 
    fmt.Println("France首都是", countryCapitalMap ["France"])
}

图片描述
比如上方在执行完
图片描述
之后,map buckets是存放了一个键值对的,可以过去查看v5(h *hmap) ,0x000000C00014FE38来查看

 

图片描述
可以看见buckets unsafe.Pointer为byte_C00014FE68

 

图片描述
前面8字节的是tophash数组,目前只存放了一个键值对,就只有一个存放了,hash(”France”)的高8bit,往下就是存放键值对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
 
import "fmt"
 
func main() {
    var countryCapitalMap map[string]string /*创建集合, 默认map是nil*/
    //如果不初始化 map,那么就会创建一个 nil map, nil map 不能用来存放键值对
    countryCapitalMap = make(map[string]string)
 
    countryCapitalMap [ "France" ] = "巴黎"
    countryCapitalMap [ "Italy" ] = "罗马"
    countryCapitalMap [ "Japan" ] = "东京"
    countryCapitalMap [ "India " ] = "新德里"
 
    //或者:countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
 
    /*使用键输出地图值 */
    for country := range countryCapitalMap {
        fmt.Println(country, "首都是", countryCapitalMap [country])
    }
}
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
void __cdecl main_main()
{
  int v0; // [rsp+10h] [rbp-230h]
  __int64 v1; // [rsp+20h] [rbp-220h]
  __int64 v2; // [rsp+20h] [rbp-220h]
  __int64 v3; // [rsp+20h] [rbp-220h]
  __int64 v4; // [rsp+20h] [rbp-220h]
  _QWORD *v5; // [rsp+30h] [rbp-210h]
  __int64 *v6; // [rsp+30h] [rbp-210h]
  __int64 v7; // [rsp+38h] [rbp-208h]
  __int64 v8; // [rsp+40h] [rbp-200h]
  __int64 v9; // [rsp+48h] [rbp-1F8h]
  __int64 v10; // [rsp+50h] [rbp-1F0h]
  __int64 v11; // [rsp+58h] [rbp-1E8h]
  __int64 v12; // [rsp+60h] [rbp-1E0h]
  _QWORD v13[5]; // [rsp+68h] [rbp-1D8h] BYREF
  __int64 v14; // [rsp+90h] [rbp-1B0h] BYREF
  __int128 v15; // [rsp+98h] [rbp-1A8h] BYREF
  __int128 v16; // [rsp+A8h] [rbp-198h]
  __int128 v17; // [rsp+B8h] [rbp-188h]
  __int64 v18[12]; // [rsp+C8h] [rbp-178h] BYREF
  char v19; // [rsp+128h] [rbp-118h] BYREF
 
  while ( (unsigned __int64)&v14 <= *(_QWORD *)(*(_QWORD *)NtCurrentTeb()->NtTib.ArbitraryUserPointer + 16LL) )
    runtime_morestack_noctxt();
  v15 = 0LL;
  v16 = 0LL;
  v17 = 0LL;
  ((void (*)(void))loc_46651C)();
  *(_QWORD *)&v16 = &v19;
  runtime_fastrand();
  HIDWORD(v15) = v0;
  runtime_mapassign_faststr((__int64)&unk_4B7040, (__int64)&v15, (__int64)"France", 6LL);
  v5[1] = 6LL;
  if ( runtime_writeBarrier )
    runtime_gcWriteBarrier();
  else
    *v5 = "巴黎";
  runtime_mapassign_faststr((__int64)&unk_4B7040, (__int64)&v15, (__int64)"Italy", 5LL);
  v5[1] = 6LL;
  if ( runtime_writeBarrier )
    runtime_gcWriteBarrier();
  else
    *v5 = "罗马";
  runtime_mapassign_faststr((__int64)&unk_4B7040, (__int64)&v15, (__int64)"Japan", 5LL);
  v5[1] = 6LL;
  if ( runtime_writeBarrier )
    runtime_gcWriteBarrier();
  else
    *v5 = "东京";
  v8 = runtime_mapassign_faststr((__int64)&unk_4B7040, (__int64)&v15, (__int64)"India ", 6LL);
  v5[1] = 9LL;
  if ( runtime_writeBarrier )
    runtime_gcWriteBarrier();
  else
    *v5 = "新德里";
  ((void (*)(void))loc_466551)();
  runtime_mapiterinit((__int64)&unk_4B7040, &v15, (__int64)v18);
  while ( v18[0] )
  {
    v11 = *(_QWORD *)v18[0];
    v10 = *(_QWORD *)(v18[0] + 8);
    runtime_convTstring(*(_QWORD *)v18[0], v10, v1);
    v12 = v2;
    v9 = runtime_mapaccess1_faststr((__int64)&unk_4B7040, (__int64)&v15, v11, v10, (__int64)v5);
    v7 = runtime_convTstring(*v6, v6[1], v3);
    v13[0] = &unk_4B3260;
    v13[1] = v12;
    v13[2] = &unk_4B3260;
    v13[3] = &off_4EF518;
    v13[4] = &unk_4B3260;
    v14 = v4;
    fmt_Fprintln((__int64)&go_itab__os_File_io_Writer, os_Stdout, (__int64)v13, 3LL, 3LL, v7, v8, v9);
    runtime_mapiternext((__int64)v18);
  }
}

查找过程

  1. 根据key计算出hash值。
  2. 如果存在old table, 首先在old table中查找,如果找到的bucket已经evacuated,转到步骤3。 反之,返回其对应的value。
  3. 在new table中查找对应的value。

这里一个细节需要注意一下。不认真看可能会以为低位用于定位bucket在数组的index,那么高位就是用于key/valule在bucket内部的offset。事实上高8位不是用作offset的,而是用于加快key的比较的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
do { //对每个桶b
    //依次比较桶内的每一项存放的tophash与所求的hash值高位是否相等
    for(i = 0, k = b->data, v = k + h->keysize * BUCKETSIZE; i < BUCKETSIZE; i++, k += h->keysize, v += h->valuesize) {
        if(b->tophash[i] == top) {
            k2 = IK(h, k);
            t->key->alg->equal(&eq, t->key->size, key, k2);
            if(eq) { //相等的情况下再去做key比较...
                *keyp = k2;
                return IV(h, v);
            }
        }
    }
    b = b->overflow; //b设置为它的下一下溢出链
} while(b != nil);

插入过程

  1. 根据key算出hash值,进而得出对应的bucket。
  2. 如果bucket在old table中,将其重新散列到new table中。
  3. 在bucket中,查找空闲的位置,如果已经存在需要插入的key,更新其对应的value。
  4. 根据table中元素的个数,判断是否grow table。
  5. 如果对应的bucket已经full,重新申请新的bucket作为overbucket。
  6. 将key/value pair插入到bucket中。

这里也有几个细节需要注意一下。

 

在扩容过程中,oldbucket是被冻结的,查找时会在oldbucket中查找,但不会在oldbucket中插入数据。如果在oldbucket是找到了相应的key,做法是将它迁移到新bucket后加入evalucated标记。并且还会额外的迁移另一个pair。

 

然后就是只要在某个bucket中找到第一个空位,就会将key/value插入到这个位置。也就是位置位于bucket前面的会覆盖后面的(类似于存储系统设计中做删除时的常用的技巧之一,直接用新数据追加方式写,新版本数据覆盖老版本数据)。找到了相同的key或者找到第一个空位就可以结束遍历了。不过这也意味着做删除时必须完全的遍历bucket所有溢出链,将所有的相同key数据都删除。所以目前map的设计是为插入而优化的,删除效率会比插入低一些。

删除过程

删除元素实际上也是先查找元素,如果元素存在则把元素从相应的bucket中删除,如果不存在则什么也不做。

 

notice

 

Go调用汇编和C:https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.1.html

G,M,P模型

在讲解函数调用之前,我们讲解一下GMP结构体

 

并发:一个逻辑流的执行在时间上与另一个流重叠,叫并发流,多个流并发地执行被成为并发。

 

并行:如果两个流并发地运行在不同的处理器核或者计算机上,那么我们成为它们为并行地执行。

 

并行流是并发流的真子集。

 

多线程或多进程是并行的基本条件,但单线程也可以用协程(coroutine)做到并发。简单将Goroutine归纳为协程并不合适,因为它运行时会创建多个线程来执行并发任务,且任务单元可被调度到其它线程执行。这更像是多线程和协程的结合体,能最大限度提升执行效率,发挥多核处理器能力。

 

Go的并发实现非常简单,使用一个go关键字即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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")
}

Go语言虽然使用一个Go关键字即可实现并发编程,但Goroutine被调度到后端之后,具体的实现比较复杂。Go调度器组成。

G

G是Goroutine的缩写,相当于操作系统中的进程控制块,在这里就是Goroutine的控制结构,是对Goroutine的抽象。其中包括执行的函数指令及参数;G保存的任务对象;线程上下文切换,现场保护和现场恢复需要的寄存器(SP、IP)等信息。

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
77
type g struct {
    // Stack parameters.
    // stack describes the actual stack memory: [stack.lo, stack.hi).
    // stackguard0 is the stack pointer compared in the Go stack growth prologue.
    // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
    // stackguard1 is the stack pointer compared in the C stack growth prologue.
    // It is stack.lo+StackGuard on g0 and gsignal stacks.
    // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
    // 记录该goroutine使用的栈
    stack       stack   // offset known to runtime/cgo
 
    //下面两个成员用于栈溢出检查,实现栈的自动伸缩,抢占调度也会用到stackguard0
    stackguard0 uintptr // offset known to liblink
      stackguard1 uintptr // offset known to liblink
 
        _panic         *_panic // innermost panic - offset known to liblink
        _defer         *_defer // innermost defer
 
        // 此goroutine正在被哪个工作线程执行
        m              *m      // current m; offset known to arm liblink
        //这个字段跟调度切换有关,G切换时用来保存上下文,保存什么,看下面gobuf结构体
        sched          gobuf
        syscallsp      uintptr        // if status==Gsyscall, syscallsp = sched.sp to use during gc
        syscallpc      uintptr        // if status==Gsyscall, syscallpc = sched.pc to use during gc
        stktopsp       uintptr        // expected sp at top of stack, to check in traceback
        param          unsafe.Pointer // passed parameter on wakeup,wakeup唤醒时传递的参数
        // 状态Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
      atomicstatus   uint32
        stackLock      uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
        goid           int64
 
        //schedlink字段指向全局运行队列中的下一个g,
        //所有位于全局运行队列中的g形成一个链表
        schedlink      guintptr
        waitsince      int64      // approx time when the g become blocked
        waitreason     waitReason // if status==Gwaiting,g被阻塞的原因
        //抢占信号,stackguard0 = stackpreempt,如果需要抢占调度,设置preempt为true
        preempt        bool       // preemption signal, duplicates stackguard0 = stackpreempt
        paniconfault   bool       // panic (instead of crash) on unexpected fault address
        preemptscan    bool       // preempted g does scan for gc
        gcscandone     bool       // g has scanned stack; protected by _Gscan bit in status
        gcscanvalid    bool       // false at start of gc cycle, true if G has not run since last scan; TODO: remove?
        throwsplit     bool       // must not split stack
        raceignore     int8       // ignore race detection events
        sysblocktraced bool       // StartTrace has emitted EvGoInSyscall about this goroutine
        sysexitticks   int64      // cputicks when syscall has returned (for tracing)
        traceseq       uint64     // trace event sequencer
        tracelastp     puintptr   // last P emitted an event for this goroutine
        // 如果调用了 LockOsThread,那么这个 g 会绑定到某个 m 上
      lockedm        muintptr
        sig            uint32
        writebuf       []byte
        sigcode0       uintptr
        sigcode1       uintptr
        sigpc          uintptr
        // 创建这个goroutine的go表达式的pc
        gopc           uintptr         // pc of go statement that created this goroutine
        ancestors      *[]ancestorInfo // ancestor information goroutine(s) that created this goroutine (only used if debug.tracebackancestors)
        startpc        uintptr         // pc of goroutine function
        racectx        uintptr
        waiting        *sudog         // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
        cgoCtxt        []uintptr      // cgo traceback context
        labels         unsafe.Pointer // profiler labels
        timer          *timer         // cached timer for time.Sleep,为 time.Sleep 缓存的计时器
        selectDone     uint32         // are we participating in a select and did someone win the race?
 
        // Per-G GC state
 
        // gcAssistBytes is this G's GC assist credit in terms of
        // bytes allocated. If this is positive, then the G has credit
        // to allocate gcAssistBytes bytes without assisting. If this
        // is negative, then the G must correct this by performing
        // scan work. We track this in bytes to make it fast to update
        // and check for debt in the malloc hot path. The assist ratio
        // determines how this corresponds to scan work debt.
        gcAssistBytes int64
}

stack 描述了当前 goroutine 的栈内存范围[stack.lo, stack.hi),其中 stack 的数据结构:

1
2
3
4
5
6
7
8
9
10
// Stack describes a Go execution stack.
// The bounds of the stack are exactly [lo, hi),
// with no implicit data structures on either side.
// 描述 goroutine 执行栈
// 栈边界为[lo, hi),左包含右不包含,即 lo≤stack<hi
// 两边都没有隐含的数据结构。
type stack struct {
    lo uintptr   //栈顶,指向内存低地址
    hi uintptr   //栈底,指向内存搞地址
}

stackguard0 和 stackguard1 均是一个栈指针,用于扩容场景,前者用于 Go stack ,后者用于 C stack。

M

M是一个线程或称为Machine,所有M是有线程栈的。如果不对该线程栈提供内存的话,系统会给该线程栈提供内存(不同操作系统提供的线程栈大小不同)。当指定了线程栈,则M.stack→G.stack,M的PC寄存器指向G提供的函数,然后去执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type m struct {   
    /*
        1.  所有调用栈的Goroutine,这是一个比较特殊的Goroutine。
        2.  普通的Goroutine栈是在Heap分配的可增长的stack,而g0的stack是M对应的线程栈。
        3.  所有调度相关代码,会先切换到该Goroutine的栈再执行。
    */
    g0       *g
    curg     *g         // M当前绑定的结构体G
 
    // SP、PC寄存器用于现场保护和现场恢复
    vdsoSP uintptr
    vdsoPC uintptr
 
    // 省略…}

P

P(Processor)是一个抽象的概念,并不是真正的物理CPU。所以当P有任务时需要创建或者唤醒一个系统线程来执行它队列里的任务。所以P/M需要进行绑定,构成一个执行单元。

 

P决定了同时可以并发任务的数量,可通过GOMAXPROCS限制同时执行用户级任务的操作系统线程。可以通过runtime.GOMAXPROCS进行指定。在Go1.5之后GOMAXPROCS被默认设置可用的核数,而之前则默认为1。

Go调度器调度过程

首先创建一个G对象,G对象保存到P本地队列或者是全局队列。

 

P此时去唤醒一个M。P继续执行它的执行序。

 

M寻找是否有空闲的P,如果有则将该G对象移动到它本身。

 

接下来M执行一个调度循环(调用G对象->执行->清理线程→继续找新的Goroutine执行)。

 

M执行过程中,随时会发生上下文切换。

 

当发生上下文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。

 

Go调度器M的栈保存在G对象上,只需要将M所需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M通过访问G的vdsoSP、vdsoPC寄存器进行现场恢复(从上次中断位置继续执行)。

 

后面分析函数的开头和结尾的时候可以对照。

连续栈

Go语言支持goroutine,每个goroutine需要能够运行,所以它们都有自己的栈。假如每个goroutine分配固定栈大小并且不能增长,太小则会导致溢出,太大又会浪费空间,无法存在许多的goroutine。

 

为了解决这个问题,goroutine可以初始时只给栈分配很小的空间,然后随着使用过程中的需要自动地增长。这就是为什么Go可以开千千万万个goroutine而不会耗尽内存。

 

Go1.3版本之后则使用的是continuous stack,下面将具体分析一下这种技术。

基本原理

每次执行函数调用时Go的runtime都会进行检测,若当前栈的大小不够用,则会触发“中断”,从当前函数进入到Go的运行时库,Go的运行时库会保存此时的函数上下文环境,然后分配一个新的足够大的栈空间,将旧栈的内容拷贝到新栈中,并做一些设置,使得当函数恢复运行时,函数会在新分配的栈中继续执行,仿佛整个过程都没发生过一样,这个函数会觉得自己使用的是一块大小“无限”的栈空间。

实际分析

图片描述
图片描述
Go语言和C不同,不是使用栈指针寄存器和栈基址寄存器确定函数的栈的。

 

在Go的运行时库中,每个goroutine对应一个结构体G,大致相当于进程控制块的概念。

 

这个结构体中存了stackbase和stackguard,用于确定这个goroutine使用的栈空间信息。

 

每个Go函数调用的前几条指令,先比较栈指针寄存器跟g->stackguard(偏移0x10),检测是否发生栈溢出。

 

如果栈指针寄存器值超越了stackguard就需要扩展栈空间。

 

函数开头首先gs:0x28,获取到了g结构体地址

 

后面的g+0x10也就是g->stackguard的地址

 

所以是把rsp和g->stackguard的值比较

 

如果SP大于g->stackguard了,则会跳转到函数结尾调用runtime.morestack函数。函数开头几条指令的作用就是检测栈是否溢出。

 

runtime.morestack作用:

 

将一些信息存在M结构体中,这些信息包括当前栈桢,参数,当前函数调用,函数返回地址(两个返回地址,一个是runtime.morestack的函数地址,一个是f的返回地址)。通过这些信息可以把新栈和旧栈链起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void runtime.morestack() {
    if(g == g0) {
        panic();
    } else {
        m->morebuf.gobuf_pc = getCallerCallerPC();
        void *SP = getCallerSP();
        m->morebuf.gobuf_sp = SP;
        m->moreargp = SP;
        m->morebuf.gobuf_g = g;
        m->morepc = getCallerPC();
 
        void *g0 = m->g0;
        g = g0;
        setSP(g0->g_sched.gobuf_sp);
        runtime.newstack();
    }
}

函数调用(支持多返回值)

无论是 x86 还是 x86-64, 都采用栈传递参数

 

返回值传递不通过eax/rax等寄存器,也是通过栈。

 

位置是最后一个参数的下面。例如最后一个参数的地址是 rsp + 0x8, 则:

  • 第一个返回值:rsp + 0x10
  • 第二个返回值:rsp + 0x18
  • 以此类推

一个参数,多个返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
 
import "fmt"
 
func main() {
    var a, b int
    a, b = sayHello(123)
    a = a + 1
    b = b + 2
    fmt.Println(a, b)
}
 
func sayHello(a int)(i,j int){
    i = a + 1
    fmt.Println("execute half")
    j = a + 2
    return
}

图片描述

多个参数,多个返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
 
import (
    "fmt"
)
 
func main() {
    var a, b int
    a, b = sayHello(1234, 5678)
    a = a + 1
    b = b + 2
    fmt.Println(a, b)
}
 
func sayHello(a int, b int)(i,j int){
    i = a + 1
    fmt.Println("execute half")
    j = b + 2
    return
}

图片描述
返回值都会存放到main函数的栈中,main的局部变量

写屏障

图片描述
golang 的一种垃圾回收机制,逆向时可以认为这个表达式永真即可,不用关注runtime_gcWriteBarrier 分支
参考:

 

https://tiancaiamao.gitbooks.io/go-internals/content/zh

 

https://blog.csdn.net/star_of_science/article/details/121802354
https://panda0s.top/2021/04/14/Golang-underlying-data-representaion/#Golang-underlying-data-representaion

 

https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/

 

https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.1.html
https://blog.csdn.net/further_eye/article/details/112506505#t18
https://www.1024sou.com/article/287980.html

 

https://segmentfault.com/a/1190000040933033


【看雪培训】《Adroid高级研修班》2022年春季班招生中!

最后于 2022-4-17 13:42 被SYJ-Re编辑 ,原因: 更新
收藏
点赞7
打赏
分享
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
 
赞赏  Editor   +100.00 2022/05/05 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (6)
雪    币: 6165
活跃值: 活跃值 (3600)
能力值: ( LV13,RANK:239 )
在线值:
发帖
回帖
粉丝
sunfishi 活跃值 3 2022-4-13 09:32
2
0
mark
雪    币: 4588
活跃值: 活跃值 (1725)
能力值: ( LV6,RANK:97 )
在线值:
发帖
回帖
粉丝
fjqisba 活跃值 2022-4-13 09:47
3
0
go语言确实挺不好逆向的,IDA代码很难阅读
雪    币: 1038
活跃值: 活跃值 (9862)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 活跃值 8 2022-4-13 15:19
4
0
论坛上深度解析Go语言的帖子不错,感谢分享!
雪    币: 9468
活跃值: 活跃值 (9054)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2022-4-15 16:17
5
0
fjqisba go语言确实挺不好逆向的,IDA代码很难阅读
go不仅是不好逆向,正向也是不好写,环境配置就开始掉头发了
雪    币: 229
活跃值: 活跃值 (56)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Beta猫 活跃值 2022-5-4 07:47
6
0
kanxue 论坛上深度解析Go语言的帖子不错,感谢分享!

有点意思,求个查重率:https://mp.weixin.qq.com/s/CrmgqLwXUaR7Uccj_72f3g

最后于 2022-5-4 07:48 被Beta猫编辑 ,原因:
雪    币: 7010
活跃值: 活跃值 (6624)
能力值: ( LV9,RANK:290 )
在线值:
发帖
回帖
粉丝
SYJ-Re 活跃值 3 2022-5-5 22:29
7
0
Beta猫 kanxue 论坛上深度解析Go语言的帖子不错,感谢分享! 有点意思,求个查重率:https://mp.weixin.qq.com/s/Crmg ...
可以,在数据结构里面是有引用内容的,也有自己实际去分析的内容,而且最后给出了参考链接
游客
登录 | 注册 方可回帖
返回