跳至正文

Go零消耗debug log技巧

tL;DR, 本文末尾提供零消耗的日志代码,最高性能提升60000%。

看到题目,有人肯定会问,官方的log模块不好么?
Debug Log一般很长,在生产环境还输出的话,也很难找。
再者,log的消耗是比较大的,特别是需要打印行号时。

https://golang.org/src/log/log.go#L158

if l.flag&(Lshortfile|Llongfile) != 0 {
// Release lock while getting caller info - it's expensive.
l.mu.Unlock()
var ok bool
_, file, line, ok = runtime.Caller(calldepth)
if !ok {
file = "???"
line = 0
}
l.mu.Lock()
}

因为需要调用runtime.Caller,这样性能就有较多的损耗。
简单的benchmark,可以发现慢50%。

func BenchmarkWithLine(b *testing.B) {
logger := log.New(ioutil.Discard, "", log.Llongfile|log.LstdFlags)
tf := strings.Repeat("abcde", 1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Print(tf)
}
}
// BenchmarkWithLine-4 500000 2806 ns/op
// BenchmarkWithoutLine-4 1000000 1754 ns/op

虽然,log的性能不差,仅需要1us就能进行一次,但如果在代码中有大量的debug日志,这个损耗累积起来,那也是相当惊人的了。

那么,在生产环境,能不能不执行log语句呢?
可以的,例如

const Dev = false
func BenchmarkConst(b *testing.B) {
logger := log.New(ioutil.Discard, "", log.LstdFlags)
tf := strings.Repeat("abcde", 1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if Dev {
logger.Print(tf)
}
}
}
// BenchmarkConst-4 2000000000 0.29 ns/op

go tool objdump查看生成的二进制文件

 log_test.go:36 0x4efc32 48890424 MOVQ AX, 0(SP)
log_test.go:36 0x4efc36 e815d4fbff CALL testing.(*B).ResetTimer(SB)
log_test.go:36 0x4efc3b 488b842480000000 MOVQ 0x80(SP), AX
log_test.go:36 0x4efc43 31c9 XORL CX, CX
log_test.go:38 0x4efc45 eb03 JMP 0x4efc4a
log_test.go:38 0x4efc47 48ffc1 INCQ CX
log_test.go:38 0x4efc4a 488b90f0000000 MOVQ 0xf0(AX), DX
log_test.go:38 0x4efc51 4839d1 CMPQ DX, CX
log_test.go:38 0x4efc54 7cf1 JL 0x4efc47
log_test.go:43 0x4efc56 488b6c2470 MOVQ 0x70(SP), BP
log_test.go:43 0x4efc5b 4883c478 ADDQ $0x78, SP
log_test.go:43 0x4efc5f c3 RET

可以看出,ResetTimer之后,仅仅是跑了个空的for循环,这是因为编译器发现if语句永远不成立,所以不编译这一段了(如果Dev是var值,那么还是会对比一下,而不是没有语句生成),不过这个方法需要每次debug时都要改代码。不想改代码可以用go build -ldflags -X方法, 但这个仅仅支持字符串,特别麻烦。

所以有没有更好的解决方案呢?有的,使用build tags
下面是例子,一共三个文件:

log.go

package main
func main() {
Debug("it's expensive")
if Dev {
fmt.Println("we are in develop mode")
}
}

log_debug.go

//+build debug
package main
import (
"fmt"
)
const Dev = true
func Debug(a ...interface{}) {
fmt.Println(a...)
}

log_release.go

//+build !debug
package main
const Dev = false
func Debug(a ...interface{}) {}

debug和release最大的差别就在文件头的//+build !debug, 意思是告诉编译器,如果有debug这个tags,那么编译的时候就略过这个文件。
比如你运行go build -tags "debug" && ./main就会输出,不设定的话,就什么都不输出。

再用go tool objdump 查看生成的可执行文件,跟之前的if Dev效果相同,压根不生成语句。这样就不用每次都改代码了来debug了,是不是很赞啊?

对于Debug函数,由于Go的函数是first class,所以Call function不可避免,不过性能损失基本上为零了。

package main
import "testing"
var a = strings.Repeat("abcde", 1024)
func BenchmarkDebug(b *testing.B) {
for i := 0; i < b.N; i++ {
Debug(a)
}
}
go test -bench=.
BenchmarkDebug-4 500000000 3.27 ns/op
go test -bench=. -tags debug
BenchmarkDebug-4 10000000 146 ns/op

总结一下,如果极度要求性能,尽量使用if Dev这种判断模式,如果要求不高,可以使用Debug函数的方法。

Yaml To Go

项目地址:Yaml-To-Go

最近工作需要把yaml配置改成Go的对象,我知道有个json-to-go,但是没有发现yaml-to-go,所以就自己搞了一套。

其实原理很简单,主要是把原来的jsonToGo函数里的解析函数全部替换成yaml的解析器。
虽然简单,但是实际上移植的时候,发现js的库太复杂了……npm 不想安装,怎么办?

Heavy NPM

可以看项目里的dist文件夹,里面会有有编译好的文件。
这样就可以用老办法,直接用script标签载入了。

两行开启Go http quic

QUIC,简单来说,就是使用UDP的传输协议,根据Google自己的报告,速度可以加快30%。
主要优点有:

1. 快速建立链接(不用3次握手和TLS4次握手)
2. 多路复用
3. 改进的流控
4. 快速SSL/TLS握手
5. 适合移动用户访问

quic-layer

这么好的性能,当然要赶紧用Go试试看。

https://github.com/lucas-clemente/quic-go

示例中的代码也很简单。

http.Handle("/", http.FileServer(http.Dir(wwwDir)))
h2quic.ListenAndServeQUIC("localhost:4242", "/path/to/cert/chain.pem", "/path/to/privkey.pem", nil)

不过在实践里,还是碰到了2个坑。

TLS配置

因为我的服务是一个http.Handler, 所以quic需要重新配置TLSconfig,否则就会报错。
下面是示例代码

quic := &h2quic.Server{Server: server}
quic.TLSConfig = &tls.Config{}
quic.TLSConfig.GetCertificate = getCertificate
pln, err := net.ListenPacket("udp", cfg.Listen)
if err != nil {
log.Fatal(err)
}
log.Print("listen quic on udp:%s", cfg.Listen)
go quic.Serve(pln)

HEADER设置

成功启用后,Chrome中的SPDY插件并没有出现绿色的标志,还是继续使用HTTP2,经过查找后,发现Google在自家的header中添加了

 writer.ResponseWriter.Header().Add("alt-svc", `quic=":443"; ma=2592000; v="38,37,36"`)

其中

  • ma是过期时间,单位是秒
  • v是指支持的quic版本
  • alt-svc是alternative-service的缩写
  • quic中是quic的端口,我指定了443

最后通过在chrome地址栏中输入

chrome://net-internals/#quic

quic-demo

Go汇编实战的坑

为啥写

Go的汇编一直是我感兴趣的地方,为了验证之前所学的汇编知识和好玩 ,我决定往Go官方提交一个性能patch。
所以到官方的标准库里搜了一圈,发现adler32并没有硬件加速的实现,而Intel已经公布了相关的SSE加速实现
https://github.com/01org/isa-l/

所以我决定把Intel的抄过来,结果不停地掉坑和爬出来,终于提交了patch(撒花)

https://go-review.googlesource.com/c/51850

希望在Go 1.10发布的时候能进入官方源:)

  1. 写法的区别
  2. Go汇编不支持
  3. 难以调试
  4. 内存越界与LEA
  5. 向官方提交代码需要注意的地方,保证Change ID一致

坑一:Intel和AT&T的写法的区别

Intel写法是:
opcode destination source

AT&T 也就是Go官方汇编语言的写法正好是反过来的:
opcode destination source

抄代码的时候有点绕

坑二 Go汇编不支持:

Go的维护者们对于新添加汇编opcode一直是很保守的,比如2001年前后就有的SSE2 里的PSHLLW()竟然不支持(= =||)。
所以得自己填BYTE,比如官方文档中的MOVQ用BYTE方式编写 https://golang.org/doc/asm

BYTE $0x0f; BYTE $0x6f; BYTE $0x00 // MOVQ (AX), M0

但这里就有一个坑,比如PSHLLD和PSHRQ的Opcode是一样的……只是按/r 寄存器类型进行区别。需要注意

坑三:调试困难

一般代码测试时,都可以直接输出日志,帮助定位问题,但是汇编不行,所以我是通过把需要的值放入某个不用的寄存器, 例如

#define debug R15
// min(a, b int) int
TEXT min(SB), NOSPLIT, $0

在需要的时候,提前把函数返回

MOVQ debug, ret+16(FP)
RET

当然应该有更好的方法,可以把所有寄存器打印出来,不过这个方法够我自己调试用了。

坑四:内存越界与LEA

一般程序中都是指针,或者直接结构体。不过,汇编这里回归本真,只有内存地址和寄存器。所以一定要小心访问数据的边界和跳转的内存地址。
这里学到了一个LEA的用法,地址计算器,把内存地址到目标寄存器里。

LEAQ 0(data)(size*1), end

具体用法

  • 0: 写死的偏移量
  • data: 偏移值
  • size: 动态偏移量,可以用乘法

坑五:

提交代码到gerrit要保持Change ID相同,要不然算一个新的Change。

附录:opcode坑

  • MOVOU: O=oct, U=unaligned, 指的是八个word,即128位,用于XXM寄存器的移动。
  • TESTQ: 对比值,并影响FLAG
  • DIVL:低位除,如果高位有数据就会出错

资料:
Go官方介绍: https://golang.org/doc/asm

[What if] 如果全国人在同一个微信群或qq群,会看到些什么内容?

本文已授权腾讯公司转载:https://www.zhihu.com/question/293021546

简单的说,什么都看不到。

为了简化,我们只说微信群。

红包

根据2014年腾讯的公开数据 1

微信和WeChat月活跃账户4.68亿,90亿条日均消息

如果全国人民在同一个群里说话,每天就是
14E8 x (90 / 4.68) = 2.6E10 条

考虑到大家一般除了睡觉8小时外,而且仅仅匀速发送的话,那就是
(14E8 x (90 / 4.68))/(86400 – 8×3600) =

467414条每秒

目前主频最高的手机CPU,高通骁龙805(APQ8084)2 有2.7GHz的处理能力,一共是4核,如不计算操作系统、显示刷新、网络IO等CPU操作的话
每条信息能分配到 2.7GHz x 4 / 467414 =

23KHz

仅仅相当于生产于1951年的第一台真空管计算机Whirlwind 3的计算能力,自然手机的CPU是处理不了这么大的数据量的。
幸好IT界有个摩尔定律,每18个月CPU性能就能翻倍(或者价钱是一半),假设等到了2025摩尔定律失效时,我们的手机应该有
2e((2025-2017)x12/18) x 2.7 = 108Ghz

看起来不错嘛,不过每条消息才能消费108GHz x4 / 467414 =

931KHz

差不多是1972年的Intel 8008 800KHz4 的水平,结果就是你等了7年,还是进不了这个全国群抢一个红包

Improve

假设我们每个人都获得了能处理消息能力的手机 5(比如说全球超算第一名的太湖之光,1千万个CPU核心)来帮忙处理微信群。
由于微信没有公布数据,因此我们只好假设平均每条消息有10个汉字,这大概相当于30 byte,算上应用层会加上一定的控制字符,再加上TCP/IP网络层的数据消耗大概是74 byte,取个整,平均每条消息有100 byte。
这时每秒数据流量是(100×8)x467414 =

373Mbps

理论上的4G网络,能支持1000Mbps6,但别忘了,是全国人民在同一个群里,很快你周围的人也接近了这个流量,这使得你所在的基站不堪重负,陷入瘫痪。
为了避免网络瘫痪导致你抢不到红包或者看群消息,你需要搬到一个周围没有人的基站,因此人口密度最低的西藏省是个不错的选择。
不过运营商的日子就不好过了,因为全国上下的流量达到了惊人的每秒373Mbps x 14E8 = 5.2E17 bps =

52 Pbps

相当于全国4月份的移动数据总流量的3%,意味着6分钟就能用完全国一年的流量 7,或者把52PB数据用2T 3.5英寸硬盘装起来,然后叠起来,有130m高

52pbs.png

当然,我相信我们抠门的国有运营商肯定会砸下重金为你建设全世界最大的宽带网络,毕竟是为了国家建设全国群,同时,好让你快乐地抢红包。

不过,接下来该花钱的就不是国有企业——腾讯。

为了处理每个人的373Mbps流量,现有的万兆交换机仅仅能支撑26个人,也就是说腾讯至少需要1346万台4口万兆交换机,目前华为4口万兆交换机售价是3927元每台,一共需要支出

1346x10000x3927 = 52,857,420,000 元

一台便宜的2U服务器则需要10000元

1346x10000x10000 = 134,600,000,000 元

仅仅这两项就相当于腾讯CEO马化腾持有的股票 9 市值10

而仅仅是把这些2U服务器叠起来就有88.9×1.346E7=1196594000mm

1196km

国际空间站的轨道高度才340km

2u_server.png

这下,你终于可以愉快地进了群。

但你惊讶地发现,屏幕上除了绿色,什么都没有……

这是因为你的眼睛没办法接收这么快的数据。
而人眼的视觉暂留时间是100-400ms11 ,我们这个群消息停留的时间只有0.002ms,相比之下,电影、电视有41ms。
因此你还没来得及看清消息,它就已经消失了,最后只留下一团绿色的色块在屏幕的正中央。

hot_phone.png

Sntpd开发日志2 Marzullo算法

实现了学生T分布之后,有趣的是,在集群中使用时发现sntpd的偏移量变化非常有规律
student-t-offset

最后我确认是两个问题引起的:

  1. s.poll 的区间设置得过长,时钟一旦偏移,要花很长的时间去校正
  2. 由于95%置信区间的设定,导致时钟源总是变化,因此总有跳变

读了一次论文之后,发现,DLM博士早就知道这事,而且在论文中也指出了这种只使用offset的方法是他们的第一版算法……但是发现由于时钟dispersion(误差)的存在,导致时钟offset不一定准确,会丢掉误差的数值,所以他们使用了Marzullo算法。

https://en.wikipedia.org/wiki/Marzullo%27s_algorithm

marzullo-example-1

看着很复杂,但是主要思想却相当简单,就是先假设所有的区间都是“好区间”,把区间值按大小排序,得出最多的夹杂着最少最大值的区间,然后在反向求最多的夹杂着最少最小值的区间。如果得不出,就缩减窗口项,好区间=所有区间减一,直到找到为止。

以下是我的Go实现

# iset 即区间拆分之后的数值
func (s *Service) marzullo(iset []Interset) (surviors []*Peer) {
sort.Sort(byOffset(iset))
nlist := len(iset) / 2
nl2 := len(iset)
var n int
low := time.Hour
high := -time.Hour
for allow := 0; 2*allow < nlist; allow++ {
n = 0
for _, set := range iset {
low = set.val
n -= set.typE
if n >= nlist-allow {
break
}
}
n = 0
for j := nl2 - 1; j > 0; j-- {
high = iset[j].val
n += iset[j].typE
if n >= nlist-allow {
break
}
}
if high > low {
break
}
}
var p *Peer
for i := 0; i < len(iset); i += 2 {
p = iset[i].peer
if high <= low || p.offset+p.rootDistance < low || p.offset-p.rootDistance > high {
continue
}
surviors = append(surviors, p)
}
return
}

Sntpd开发日志-实现学生T分布滤波器

最近在实现sntpd的时间选取,但是遇到了一个问题,怎么查询到集群里“合适”的机器,一开始我只是用delay最低的,但是这样并不是最好的,而RFC5905里的实现太复杂了,这样的话,我就需要一个“滤波器”,但是样本实在太少了,学过统计学的就知道,样本要大于30才算好,但是,查询太多的机器很影响性能,所以样本一般低于5个。因此一番搜索之后,发现“学生T分布”是最好的选择:小样本估计置信区间。

那正态分布呢?因为我们知道,生物过程或者其他变量过多样本都符合正态分布,而学生T分布就是对正态分布的估算。回到ntp的问题,ntp影响的因素有太多了,网络拥塞,时钟抖动,时钟偏移等等,其实,跟网络打上交道,因素就已经够多了。

所以这个条件下,给sntpd安上一个学生T分布滤波器就很好了:)

首先要选个置信度,我只是要排除非法值,所以95%是个很好的选择。
根据公式

Wiki-学生T分布

计算样本均值,样本标准差,自由度(样本数-1),再按表格查询,就知道置信区间了,这样就大于或者小于置信区间的值,就可以当作非法值去掉了。

How to turn on AVX2 in VirtualBox Guest

  • CPU: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz (AVX2\AVX)
  • HOST OS: Windows 7
  • Guest: Ubuntu 16.04.1 LTS 64 bit

In Host (which Dev is my VM name)

C:\Program Files\Oracle\VirtualBox>VBoxManage.exe setextradata "Dev" VBoxInternal/CPUM/IsaExts/AVX2 1

Check by:

C:\Program Files\Oracle\VirtualBox>VBoxManage.exe getextradata "Dev" enumerate
Key: GUI/Fullscreen, Value: true
Key: GUI/LastCloseAction, Value: SaveState
Key: GUI/LastGuestSizeHint, Value: 1920,995
Key: GUI/LastNormalWindowPosition, Value: 26,30,944,394,max
Key: GUI/LastScaleWindowPosition, Value: 8,30,1272,928
Key: GUI/RestrictedRuntimeDevicesMenuActions, Value: HardDrives
Key: GUI/RestrictedRuntimeMachineMenuActions, Value: SaveState,PowerOff
Key: GUI/ScaleFactor, Value: 1
Key: GUI/StatusBar/Enabled, Value: false
Key: GUI/StatusBar/IndicatorOrder, Value: HardDisks,OpticalDisks,FloppyDisks,Net
work,USB,SharedFolders,Display,VideoCapture,Features,Mouse,Keyboard
Key: VBoxInternal/CPUM/IsaExts/AVX2, Value: 1

Restart your VM and bingo!

Go汇编学习 2.解构AMD64 bytes.Equal

SIMD

上一篇学了内存结构基本知识,本文将学习符号(symbol)、语句的含义。
我个人喜欢通过例子来学习,所以,我就从src/runtime/asm_amd64.s里的bytes·Equal入手吧:)

对应代码

TEXT bytes·Equal(SB),NOSPLIT,$0-49
MOVQ a_len+8(FP), BX
MOVQ b_len+32(FP), CX
CMPQ BX, CX
JNE eqret
MOVQ a+0(FP), SI
MOVQ b+24(FP), DI
LEAQ ret+48(FP), AX
JMP runtime·memeqbody(SB)
eqret:
MOVB $0, ret+48(FP)
RET

预备知识

SB (static base)相关知识

以下是Go asm中的介绍

The SB pseudo-register can be thought of as the origin of memory, so the symbol foo(SB) is the name foo as an address in memory. This form is used to name global functions and data. Adding <>to the name, as in foo<>(SB), makes the name visible only in the current source file, like a top-level static declaration in a C file. Adding an offset to the name refers to that offset from the symbol’s address, so foo+4(SB) is four bytes past the start of foo.

大致翻译一下,例如foo(SB)的符号,就对应了code segement中的地址,全局可见。当添加了 <> 符号后,就变为了当前文件可见,类似于C文件的static声明,还可以通过添加偏移量(offset)来访问其他地址。

指令格式

例子中的TEXT指令就定义了一个叫bytes·Equal的符号(注意是中点号·),接下来就是对应的指令(可以理解成函数体),而最后RET则是返回指令(退出当前stack)。通常情况下,参数大小后跟随着stack frame的大小,使用减号(-)分割。$0-49意味着这是一个0-byte的栈,并且有49-byte长的参数。NOSPLIT说明,不允许调度器调整stack frame的大小,这就意味着必须人工指定stack frame大小。
但为什么是49个byte?

因为我们可以看看bytes.Equal的定义

func Equal(a, b []byte) bool

a, b 分别为[]byte(不定长的byte slice),而slice的结构是:

type slice struct {
array unsafe.Pointer
len int
lcap int
}

unsafe.Pointer 在amd64上是uintptr,即uint64。int在amd64上背后是int64。
因此一个slice占用了3个qword(word=2byte qual=4 即 2×4=8byte 8×8=64bit),即 3×8 = 24byte,然后又有两个slice做为参数,再加上一个bool byte,因此,这个call stack frame应该有 24×2 ([]byte) + 1 (bool) = 49byte。又因为不需要局部变量,因此定义为0个.

$0-49

函数指令解构

汇编是门“死脑筋”+“疯狂简写”的语言,接下来是对函数语句的解析,一旦理解了以后,语句是很简单的。

 MOVQ a_len+8(FP), BX // move qword, a slice的长度放入BX寄存器
MOVQ b_len+32(FP), CX // b slice的长度放入CX寄存器
CMPQ BX, CX // compare qword 对比BX,CX
JNE eqret // jump not equal 如果不相等就跳转至标签eqretequal ret
MOVQ a+0(FP), SI // a的指针放入SI寄存器中
MOVQ b+24(FP), DI // b的指针放入DI寄存器中
LEAQ ret+48(FP), AX // load effective address, 将返回值的内存地址放入AX寄存器中
JMP runtime·memeqbody(SB) // JUMP, 跳转至 runtime·memeqbody(SB) 地址空间
eqret:
MOVB $0, ret+48(FP) // move byte, $0 (意思是数字0, false = 0)传入返回的参数中,即两个slice不相等
RET

这里出现了两个新的概念:

偏移量定义,例如a_len+8(FP),还记得上一篇中讲过,FP是指在低内存位上的么?因此,这里就定义了a_len,即a length = +8(FP),相对于FP偏移8个byte(记得slice的结构吧),这个正好是a的长度所在的位置。不记得的话,可以参考下图


FP +------------> b pointer
|
+ | +-------> b length
| | |
v | | +-> b capacity
Low | | |
+----+----+----+-+--+-+--+--+-+-+
| | | | | | | |
+-+--+-+--+-+--+----+----+----+++
| | | |
| | +-> a capacity +-> return value
| |
| +------> a length
|
+-----------> a uint64-pointer

每个空格相当于一个byte

另一个概念是label,汇编不同于高级语言,汇编的条件跳转基本上都是靠label(标签)实现的,例子中的eqret,就是个label。

AVX

接下来是精华hugeloop

AVX是Intel引以为傲的SIMD指令集,具体介绍在AVX,Go在字符比较中根据CPU的能力分别会使用SSE、AVX、AVX2,这种指令集优化就是我们为什么要写汇编的原因了。


// 64 bytes at a time using ymm registers (一次就能对比64byte64倍性能就问你怕不怕)
hugeloop_avx2:
CMPQ BX, $64 // 对比字符长度
JB bigloop_avx2 // 不够64个字节就用其他方法。
VMOVDQU (SI), Y0 // AVX2 专用加载数据的指令,将SI前32个byte加载进Y0寄存器512bit
VMOVDQU (DI), Y1 // ...
VMOVDQU 32(SI), Y2
VMOVDQU 32(DI), Y3
VPCMPEQB Y1, Y0, Y4 // 对比Y0 - Y1,把结果存入Y4中
VPCMPEQB Y2, Y3, Y5 // 同上
VPAND Y4, Y5, Y6 // AND 操作
VPMOVMSKB Y6, DX // MOVE BYTE MASK  Y6中的每8个bit做一个掩码存入DX中(简单点就是相同就都是0xf
ADDQ $64, SI // SI位移64个byte
ADDQ $64, DI // DI位移64个byte
SUBQ $64, BX // BX 长度减64
CMPL DX, $0xffffffff // 对比DX的低位
JEQ hugeloop_avx2 // 相同则继续对比
VZEROUPPER // 清空Y寄存器
MOVB $0, (AX) // 发现不同,返回
RET

小结

现在对Golang的汇编比较熟悉了,下一篇会摘抄并翻译一些注意事项。

Go汇编学习 1.进程内存地址与寄存器

前言

本文是我学习Go汇编的一些心得,网上关于Go的汇编资料实在是太少了,本人计算机不是科班,所以__可能会有错误__。

进程内存地址空间

操作系统执行程序时,会把程序的二进制部分加载进内存,同时分配一定的内存空间。如下图所示

TEXT/Code segment

TEXT segment,或者叫Code segment,保存了程序的指令。

  • 只能执行
  • 固定长度

Data segment

Data segment 保留着进程初始化的全局和静态的变量。这个数据段可以变为初始化只读区域(initialized read-only area)和初始化读写区域(initialized read-write area)。

Gvar Section

一个全局变量初始化和存储在data segment。这个区域可读/写,但是不能在进程之间共享数据。

BSS Section

这个section保存着未初始化的数据。 这个section的又叫做零初始化数据section(zero-initialized data section)

Heap Section

heap是进程的线性增长地址空间,当程序使用malloc()获取的动态内存,这份内存就是在heap之中。heap区域是在BSS segement之后,并会“增长”到更高的地址空间。heap区域是所有共享库、动态模块都可见的。

  • 可读写
  • 变量大小
  • 请求之后动态分配,需要自己释放

Stack Segment

这个Segement保存所有局部变量。当调用函数时,这个函数的局部变量就全部分布在stask上(这个函数就形成了stack frame)。当函数退出时,所有该函数相关的局部变量会自动从stask上退出。其他信息,包括返回地址和局部参数,也是保存在stack的stack frame之上。stack是LIFO(last in first out 先进后出)结构,一般是分配在内存的高地址上,因此是向低位增长,还有每个寄存器,中间值或者stack frame。

  • 可读写
  • 变量大小
  • 动态增长\缩小

跟Go asm有什么关系?

Go有几个虚拟寄存器

  • FP (stack frame pointer) stack帧低位指针,指向参数和局部变量,offset为正数
  • SP(virtual stack pointer)stack帧高位指针(栈顶),offset应为负数
  • PC (program counter) 程序计数器,负责跳转和分支
  • SB (static base pointer) 静态全局符号(symbol)

其中FP和SP的区别如下图所示:


High +-----------------+
| |
| Stack frame 0 |
| |
+-----------------+
| | <---+ SP
| Stack frame 1 |
| | <---+ FP
Low +-----------------+

这里有个疑问的地方,啥是符号(symbol),根据Symbol里的解释是:

符号(symbol)是帮助人们标记特定内存地址的东西

也就是说例如

TEXT DEMO(SB)

就代表指向code segment中代表DEMO函数的内存地址。

寄存器

每种CPU都有不同的寄存器,AMD64平台里常用的有以下几个:

  • [A-D]X //通用的64位寄存器
  • R8-15 // 之前的其实是A-Dx、SI、DI等寄存器占用了
  • SI // 源(source register)
  • DI // 目的地 (destination register)
  • X0-15 // sse用
  • Y0-15 // avx2 用

了解完内存结构和寄存器后,下一篇将学习各种符号的意义:)