Golang 数据结构到底是怎么回事?gdb调一调?

发表于 2年以前  | 总阅读数:1009 次

不仅限于语法,使用gdb,dlv工具更深层的剖析golang的数据结构

Golang数据结构

变量:有意义的一个数据块。

变量名:一个有意义的数据块的名字。

为什么特意会有这个章节?

golang本质是全局按照值传递的,也就是copy值,那么你必须知道你的内存对象是包含哪些内容,才能清楚掌握你在copy值的时候复制了哪些东西,这个很重要,第一部分的正文内容从这里开始。具体如下类型:

  1. num
  2. bool
  3. string
  4. array
  5. slice
  6. map
  7. channel
  8. interface

这些结构实际在地址空间栈是什么形式的实现?这里直接看地址空间的内容,以下都是以这个例子进行分析:

package main

func main () {
var n int64 = 11
var b bool = true

var s string = "test-string-1"

var a [3]bool = [3]bool{true, false, true}
var sl []int = []int{1,2,3,4}

var m map[int]string
var c chan int
var in interface{}

    _, _, _, _, _, _, _, _ = n, b, s, a, sl, m, c , in
}

数值类型

n:n就是一个8字节的数据块。

Bool类型

b:就是一个1字节的数据块。

String类型

string类型在go里是一个复合类型,s变量是一个16字节的变量。其中str字段指向字符串存储的地方。

(gdb) pt s
type = struct string {
  uint8 *str;
  int len;
}

换句话说,s就是代表了一个16字节的数据块。所以我们每次定义一个string变量,除了字符序列,s的本身结构是分配16个字节在栈上。

赋值语句

0x000000000044ebed <+61>:    lea    0x1e81d(%rip),%rax        # 0x46d411
0x000000000044ebf4 <+68>:    mov    %rax,0x48(%rsp)
0x000000000044ebf9 <+73>:    movq   $0xd,0x50(%rsp)

地址空间存储

(gdb) p/x uintptr(&s)
$9 = 0xc000030750
(gdb) x/2gx 0xc000030750
0xc000030750:    0x000000000046d411    0x000000000000000d
(gdb) x/s 0x000000000046d411
0x46d411:    "test-string-1triggerRatio=value method xadd64 failedxchg64 failed}\n\tsched={pc: but progSize  nmidlelocked= out of range  t.npagesKey=  untyped args -thread limit\nGC assist waitGC worker initMB; alloca"...

数组类型

数组类型,就是在地址空间中连续的一段内存块,和c一样(旁白:和c一样,都是平坦的内存结构)。

(gdb) p &a
$13 = ([3]bool *) 0xc00003070d
(gdb) x/6bx 0xc00003070d
0xc00003070d:    0x01    0x00    0x01    0x0b    0x00    0x00

Slice类型

这是个复合类型,变量本身是一个管理结构(和string一样),这个管理结构管理着一段连续的内存。


(gdb) pt sl
type = struct []int {
  int *array;
  int len;
  int cap;
}

map 类型 和 channel 类型

其中,map类型和channel类型特别提一点,变量本身本质上是一个指针类型。也就是说上面我们定义了两个变量m,c,从内存分配的角度来讲,只在栈上分配了一个指针变量,并且这个指针还是nil值,所以我们经常看到 go 的一个特殊说明:slice,map,channel 这三种类型必须使用make来创建,就是这个道理。因为如果仅仅定义了类型变量,那仅仅是代表了分配了这个变量本身的内存空间,并且初始化是nil,一旦你直接用,那么就会导致非法地址引用的问题。slice 的24个字节的管理空间,map和channel的一个指针8个字节的空间。那么如果是调用了make,其实就会把下面的结构分配并初始化出来。


(gdb) pt m
type = struct hash<int, string> {
  int count;
  uint8 flags;
  uint8 B;
  uint16 noverflow;
  uint32 hash0;
  struct bucket<int, string> *buckets;
  struct bucket<int, string> *oldbuckets;
  uintptr nevacuate;  
  struct runtime.mapextra *extra;
} *

(gdb) pt c
type = struct hchan<int> {
  uint qcount;
  uint dataqsiz;
  void *buf;
  uint16 elemsize;
  uint32 closed;
  runtime._type *elemtype;
  uint sendx;
  uint recvx;
  struct waitq<int> recvq;
  struct waitq<int> sendq;
  runtime.mutex lock;
} *

那么make又做了什么,make 是 golang 的关键字,编译器会自动转变成函数调用,go 语言的关键字基本都是转换成内部的函数调用,梳理golang的关键字对应的转换表:

(旁白:编译器牛逼,golang 比c 高级就高级在编译器帮你做了非常多的事情)

Interface

特别说明下interface,因为这个也是 golang 的一个特色点,你实现了interface定义的方法集,那么可以当作这个接口用,换句话说,你实现了这些行为,就是这个接口对象,怎么实现的?这个和c++的多态是很像的,和python的行为多态更像。c++是通过虚表结构来实现的多态。go实现的接口是通过interface结构来实现的。

回忆下 c++ 的多态,c++ 是静态就定义好了继承或组合关系,虚表的个数是确定的:

  1. 对象头部存在一个虚表指针
  2. 虚表的内容是编译器在编译期间就定好了的
  3. 每个类都是有自己的虚表的,不同类创建出来的对象虚表指针是指向自己类的虚表

有继承覆盖的情况如下:

所以 c++ 的多态就一目了然,虽然我们可能不知道当前对象是那个类,当时我们通过头部的虚表指针找到对应的虚表,按照一样的偏移offset去获取到函数指针,这个对象方法自然就会是我们对象正确的方法。

go interface

type iface struct {
  tab  *itab
  data unsafe.Pointer
}

type eface struct {
  _type *_type
  data  unsafe.Pointer
}

当我们把一个对象赋值给接口,调用方法的时候,接口怎么能获取到对象正确的方法? iface 接口里 data 存放私有数据,一般是具体对象地址。关键就在 itab 结构,本质上是一个pair(interface,concrete)。

itab结构

这个结构会在两种情况下生成:

  1. 编译期间( static in compile ):这个是大部分情况,只要是编译器能分析出来的,就会尽量在编译期间生成itab结构,然后存放在.rodata只读数据区域。比如类型赋值到接口的场景。(旁白:绝大部分是这种情况,只要编译器能帮你干的就顺手帮你干了,这种情况的运行时开销几乎没有,就是一个间接调用的开销)
  2. 运行期间(runtime):有些场景是在编译期间无法确认itab的,这个只能等到runtime期间,动态的去查找、获取、生成。比如接口和接口直接相互转换的场景,这个就只能在运行期间才能确定
  • 这种情况下,带来的开销是很大的,所以内部这种情况下是有对itab做一个全局缓存的,优化( interface 类型,具体类型)查找itab的性能
type itab struct {
   inter *interfacetype
   _type *_type
   hash  uint32 // copy of _type.hash. Used for type switches.
   _     [4]byte
  fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

字段含义:

  1. inter是描述接口类型的变量。
  2. _type是描述具体对象的类型对象。
  3. fun是一个可变数组,依次存放着这个pair(interface,concrete)实际的方法地址。

内存布局:

这么个结构如果是编译器生成,是存在.rodata里。

编译器静态生成 itab

举个例子:

package main

type Node interface {
    Add(a, b int32) int32
    Sub(a, b int64) int64
}

type SObj struct{ id int32 }
func (adder SObj) Add(a, b int32) int32 { return a + b }
func (adder SObj) Sub(a, b int64) int64 { return a - b }

func main() {
    m := Node(SObj{id: 6754})
    m.Add(10, 32)
}

这里通过 SObj 变量类型赋值到接口 Node. 首先呢,这个在编译期就会检查是否能够这样赋值,允许赋值的满足的要求就是:SObj实现了Node的声明的两个方法。编译通过之后呢,会生成一个二元组对pair(Node,SObj)。调用过程呢,就是先获取itab,在获取fun,根据偏移获取方法地址。看下汇编代码:

(gdb) disassemble
Dump of assembler code for function main.main:
  => 0x000000000044ec70 <+0>:    mov    %fs:0xfffffffffffffff8,%rcx
  0x000000000044ec79 <+9>:    cmp    0x10(%rcx),%rsp
  0x000000000044ec7d <+13>:    jbe    0x44ecf8 <main.main+136>  
  0x000000000044ec7f <+15>:    sub    $0x40,%rsp
  0x000000000044ec83 <+19>:    mov    %rbp,0x38(%rsp)
  0x000000000044ec88 <+24>:    lea    0x38(%rsp),%rbp
  0x000000000044ec8d <+29>:    movl   $0x0,0x24(%rsp)

  // 根据itab, val 构造出 interface结构 (这个是)
  0x000000000044ec95 <+37>:    movl   $0x1a62,0x24(%rsp)
  0x000000000044ec9d <+45>:    lea    0x2993c(%rip),%rax        # 0x4785e0 <go.itab.main.SObj,main.Node> // itab的地址;编译期确定
  0x000000000044eca4 <+52>:    mov    %rax,(%rsp)
  0x000000000044eca8 <+56>:    mov    0x24(%rsp),%eax           // value的值6754
  0x000000000044ecac <+60>:    mov    %eax,0x8(%rsp)
  0x000000000044ecb0 <+64>:    callq  0x407fd0 <runtime.convT2I32>
  0x000000000044ecb5 <+69>:    mov    0x18(%rsp),%rax
  0x000000000044ecba <+74>:    mov    0x10(%rsp),%rcx
  0x000000000044ecbf <+79>:    mov    %rcx,0x28(%rsp)           // interface m的地址
  0x000000000044ecc4 <+84>:    mov    %rax,0x30(%rsp)

  0x000000000044ecc9 <+89>:    mov    0x28(%rsp),%rax
  0x000000000044ecce <+94>:    test   %al,(%rax)
  0x000000000044ecd0 <+96>:    mov    0x18(%rax),%rax           // 获取到SObj.Add的地址
  0x000000000044ecd4 <+100>:    mov    0x30(%rsp),%rcx
  // 传参数 10,20
  0x000000000044ecd9 <+105>:    movabs $0x200000000a,%rdx
  0x000000000044ece3 <+115>:    mov    %rdx,0x8(%rsp)
  0x000000000044ece8 <+120>:    mov    %rcx,(%rsp)
  // 调用到 SObj.Add 方法
  0x000000000044ecec <+124>:    callq  *%rax
  0x000000000044ecee <+126>:    mov    0x38(%rsp),%rbp
  0x000000000044ecf3 <+131>:    add    $0x40,%rsp
  0x000000000044ecf7 <+135>:    retq
  0x000000000044ecf8 <+136>:    callq  0x446fb0 <runtime.morestack_noctxt>
  0x000000000044ecfd <+141>:    jmpq   0x44ec70 <main.main>
  End of assembler dump.

(gdb) p m
$3 = {tab = 0x4785e0 <SObj,main.Node>, data = 0xc00005e000}
(gdb) p &m
$4 = (main.Node *) 0xc000030778
(gdb) p $rsp + 0x28
$5 = (void *) 0xc000030778
(gdb) x/1gx 0x28 + $rsp
0xc000030778:    0x00000000004785e0
(gdb) x/1gx 0x00000000004785e0 + 0x18
0x4785f8 <go.itab.main.SObj,main.Node+24>:    0x000000000044ed70
(gdb) info symbol 0x000000000044ed70
main.(*SObj).Add in section .text of /root/go-proj/test_interface_1

查看二进制 .rodata 段

[root@bogon go-proj]# objdump -xt -j .rodata ./test_interface_1|grep SObj
00000000004785e0 g     O .rodata    0000000000000028 go.itab.main.SObj,main.Node

这里还要提一点,这里定义的两个方法reciver都是变量值,而不是指针,但其实golang默认的都是按照引用操作的,reciver为指针的版本一定会生成,取值则是按照反引用取值的。所以编译器其实生成了四个函数。

000000000044ec30 g     F .text    0000000000000015 main.SObj.Add
000000000044ed70 g     F .text    000000000000008c main.(*SObj).Add

000000000044ec50 g     F .text    0000000000000019 main.SObj.Sub
000000000044ee00 g     F .text    0000000000000097 main.(*SObj).Sub

业务逻辑是在 main.(*SObj).Add和main.(*SObj).Sub 里面,另外两个是包装函数。所以,当reciver为值的时候,你传指针或者值都可以。编译期可以帮你判定搞定。当reciver为引用的时候,你只能传引用,因为编译期没法搞定,因为这种情况就是假定我们可能要修改原对象,你如果传值,值copy之后,就不是原来的值了,这就违背了基本语义。

动态生成 itab

这个主要用在接口<->接口的场景。这种情况的itab表是动态生成的,编译期是没有静态生成的。这种两个都是接口的,你就必须是动态的去找,因为只有在runtime的时候才能知道里面赋值的到底是什么具体类型。对于itab会有一个全局hash缓存表,缓存优化性能。

举个例子:

package main

type Student struct { name string }

type Ione interface { getName() string }
type Itwo interface { getName() string }

func (s Student) getName() string { return s.name }
func main() {
var i Ione
var t Itwo

    s := Student{ name:"concrete obj" }

    i = s
    t = i // interface 2 interface

    _ = t
}

其中由于 i = s 是concrete类型到interface的赋值,这里编译器可以直接生成静态的itab结构。而由于 t = i 是接口到接口的赋值,这个编译器是无法去生成静态itab的(不要看到这里的简单例子,觉得自己肉眼可以在编译期间确定静态itab表。如果放到项目的大环境,比如纯操作接口的一个函数里面,你是无法分析出interface是否绑定到某个对象)。

(旁白:动态的有点是更灵活,缺点是现场性能损耗)

t = i 是调用 convI2I 转换函数生成 itab <Itwo, Student>。

关键路径:


convI2I -> getitab
            -> new itab & itab.init
            -> itabAdd

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/qtQoZaX_SJi6_TD-uGUPWA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:8月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:8月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:8月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:8月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:8月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:8月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:8月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:8月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:8月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:8月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:8月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:8月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:8月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:8月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:8月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236897次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  7046次阅读
 目录