Featured image of post eBPF: Attach Go(Part0): Go 语言基础和特性

eBPF: Attach Go(Part0): Go 语言基础和特性

本文是使用eBPF挂钩Go程序的第一篇,主要介绍了Go语言的一些基础类型和特性。

   本文是eBPF与Go系列的第一篇,主要介绍了Go语言的一些基础数据类型和特性,其中特别分析和总结Go语言中接口的原理和实现。

0.问题前言

    去年,我使用eBPF技术对HTTPS网络流量进行明文数据采集,先后对OpenSSL、GnuTLS和LibreSSL等C/C++库进行了分析和适配。虽然说这已经能覆盖当前网络环境的大多数HTTPS场景了,但网络技术日新月异,现在服务端的网络环境远比我们已知的要复杂,尤其是近年来云原生概念的突起,一批批应用纷纷上云。在谈及云原生时,不可避免的的话题是有关Go、Docker和k8s等容器相关的技术。因而,当我想要更全面地采集网络中的HTTPS明文数据时,就需要针对Go语言编程环境下的SSL/TLS (即Go语言中的GoTLS模块)应用进行分析研究。

    所以,eBPF与Go这个系列的文章其实最终是为使用eBPF采集Go程序的HTTPS明文数据目标服务的。在此之前,我在这方面的背景大约如下:

  • 对eBPF技术已有足够的了解和应用,已有libbpf应用于OpenSSL、GnuTLS和LibreSSL的经验;

  • 零散地翻阅过Go语言的相关书籍,但对其实语言底层和特性并不熟悉(因为没有真正写过Go);

  • 对eBPF在Go程序的应用不了解,这方面和C/C++必然存在差异,应该需要特别地研究Go ABI;

  • 已有众多有关eBPF在Go的开源项目(我学习研究过的cilium、eCapture、deepflow、pixie等),但有些坑是不得不亲自踩的;

  • 众多博客都表示:对Go程序使用uretprobe时会引起的程序崩溃,需要另辟蹊径。

    因此,eBPF与Go系列将分为以下3部分进行叙述:

  1. 即本文,将介绍Go语言的基础类型及其内存布局,尤其挑选其中接口的实现进行叙述;

  2. 进一步的,针对Go ABI 进行叙述;

  3. 最终介绍,如何使用eBPF技术捕获GoTLS的明文数据,并不引起崩溃的方法。

1.数据类型

    在编程语言中,数据类型可谓是基础中的基础。对于Go语言,其基础数据类型及对应的内存大小和对齐如下:

Type64-bit32-bit
SizeAlignSizeAlign
bool, uint8, int81111
uint16, int162222
uint32, int324444
uint64, int648884
int, uint8844
float324444
float648884
complex648484
complex128168164
uintptr, *T, unsafe.Pointer8844

表格来源: https://github.com/golang/go/tree/master/src/cmd/compile/abi-internal.md

    另外byterune类型分别是uint8int32的别名,而mapchanfunction类型的布局相当于表中的*T。当然,未来为了描述Go中存在的其他复合类型,我们定义一个序列S的N个字段的布局,这些字段的类型为t1, t2,…我们定义每个字段相对于基数0开始的字节偏移量,以及序列的大小和对齐方式,如下所示:

1
2
3
4
5
6
7
8
# 其中,sizeof(S) 即 类型S的大小
# 其中,alignof(S) 即 类型S的对齐方式
offset(S, i) = 0  if i = 1
             = align(offset(S, i-1) + sizeof(t_(i-1)), alignof(t_i))
alignof(S)   = 1  if N = 0
             = max(alignof(t_i) | 1 <= i <= N)
sizeof(S)    = 0  if N = 0
             = align(offset(S, N) + sizeof(t_N), alignof(S))
  • 对于数组类型 [N]T,即由N个类型为T的字段组成的序列;

  • 字符串类型是一个*[len]字节指针的序列;

  • 结构体类型struct {f1 t1;…;fM tM}表示为序列t1,…, tM, tP,其中tP为:

    • 如果sizeof(tM) = 0且sizeof(ti)中的任何一个≠0,则类型为byte;

    • 否则为空(大小为0,对齐为1).

1.0数组

    数组(array) 是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度也是数组类型的组成部分,数组在初始化之后大小就无法改变,因而存储元素类型相同、但是大小不同的数组类型在 Go 语言看来也是完全不同的。数组的内存布局如下:

1
2
3
4
// [4]int{1,2,3,4}
// +---+---+---+---+
// | 1 | 2 | 3 | 4 |
// +---+---+---+---+

1.1字符串

    字符串是由字符组成的数组,C 语言中的字符串使用字符数组 char[] 表示。数组会占用一片连续的内存空间,而内存空间存储的字节共同组成了字符串,Go 语言中的字符串只是一个只读的字节数组。

1
2
3
4
5
6
// ~/src/internal/unsafeheader/unsafeheader.go#lin34
// or ~/src/reflect/value.go#2780    
type StringHeader struct {
    Data uintptr
    Len  int
}

"hello" 字符串在内存中的存储方式:

图 - Go 语言切片的实现原理 字符串内存布局

1.2切片

    切片(slice) 比其数组在Go中通常更常用一些,切片可以理解为动态数组。切片的实现如下:

  • Data 是指向数组的指针;

  • Len 是当前切片的长度;

  • Cap 是当前切片的容量,即 Data 数组的大小。

1
2
3
4
5
6
// ~/src/runtime/slice.go#line15 or ~/src/reflect/value.go#2793    
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

切片的内存布局如下:

图 - Go 语言切片的实现原理 切片内存布局

2.特别: 接口实现原理

2.0接口

    接口是一种抽象类型,没有暴露所包含的数据的布局和内部细节,它只对外提供了一组方法的签名。作为使用者,你只需要知道它能做什么即可,而无需关心它里面有什么。

接口定义

    Go语言提供了interface关键字来定义相应的接口,在接口中我们只能定义方法签名,不能包含成员变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 定义一个error接口, 包含Error()方法
type error interface {
    Error() string
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Reader interface {
    Read(p []byte) (n int, err error)
}
// 接口与接口间可以嵌套得到新接口, ReadWriter:
type ReadWriter interface{
    Reader
    Writer
}

接口实现

Duck Typing的名称来自于“走路像鸭子,叫声像鸭子,那么就是鸭子”的俗语。在Duck Typing中,不需要显式地指定对象的类型,只要对象的行为与指定的类型相符合,那么它就可以被视作该类型的对象。

    Go接口的实现方式其实就是Duck Typing,也可以说是Go的接口是隐式实现的。比如一个类型需要实现上述定义的error接口,那么它只需要实现Error() string方法即可:

1
2
3
4
5
6
7
8
type RPCError struct {
    Code    int64
    Message string
}

func (e *RPCError) Error() string {
    return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}

    在使用上述 RPCError 结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查。

实现原理

接口也是Go语言中的一种类型,不过在Go语言中共有两种不同类型的接口runtime.ifaceruntime.eface,其中前者是带有方法的接口,后者是不带任何方法的接口interface{}

runtime.eface的结构体定义如下:

1
2
3
4
5
// https://github.com/golang/go/blob/master/src/runtime/runtime2.go
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

由于interface{}runtime.eface不包含任何方法,所以其结构相对简单,仅包含指向底层数据和类型类型的两个指针。runtime._type的结构体定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type _type struct {
    size       uintptr
    ptrdata    uintptr
    hash       uint32
    tflag      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    equal      func(unsafe.Pointer, unsafe.Pointer) bool
    gcdata     *byte
    str        nameOff
    ptrToThis  typeOff
}

可以看到runtime._type包含了诸如类型大小、哈希、对齐、以及种类等信息。_

而runtime.iface的结构体定义如下:

1
2
3
4
5
// https://github.com/golang/go/blob/master/src/runtime/runtime2.go
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

其中包含了一个指向数据的指针和runtime.itab的tab字段,runtime.itab的结构定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//runtime/type.go
type interfacetype struct {
        typ     _type       // 接口的类型信息
        pkgpath name        // 定义接口的包名
        mhdr    []imethod   // 接口中定义的函数表,按字典序排序
}

// https://github.com/golang/go/blob/master/src/runtime/runtime2.go
type itab struct { // 32 字节
    inter *interfacetype // interfacetype是描述接口定义的信息
    _type *_type         // 接口的类型信息
    hash  uint32
    _     [4]byte
    fun   [1]uintptr
}

runtime.itab结构体是接口类型的核心组成部分,每一个runtime.itab都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter 和 _type 两个字段表示。

  • hash 是对 _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type是否一致;
  • fun 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的。

2.1反射

    reflect实现了运行时的反射能力,能够让程序操作不同类型的对象。反射包中有两对非常重要的函数和类型,两个函数分别是:

  • reflect.Typeof 能获取类型信息;

  • reflect.ValueOf能获取数据的运行时表示;

   运行时反射是程序在运行期间检查其自身结构的一种方式。Go 语言反射的三大法则如下:

  1. 从 interface{} 变量可以反射出反射对象;
  2. 从反射对象可以获取 interface{} 变量;
  3. 要修改反射对象,其值必须可设置;

3.总结

    本文主要总结了Go语言的基础类型的内存布局和底层数据结构实现,并特别地介绍了接口的实现细节,以及稍微提及了反射特性。当然,本文并没有足够深入剖析,不过上述提及的知识用于eBPF挂钩Go程序是足够的了。此外,如果想进一步深入学习Go的话,以下这些书籍或许可以成为参考:

4.链接参考

Golang bcc/BPF Function Tracing

Go语言高级编程 - Go语言高级编程 (chai2010.cn)

Go 语言设计与实现 | Go 语言设计与实现 (draveness.me)

Golang Interface实现原理分析_golang的interface{}底层原理-CSDN博客

Licensed under CC BY-NC-SA 4.0
哦吼是一首歌。
Built with Hugo
Theme Stack designed by Jimmy