共计 14624 个字符,预计需要花费 37 分钟才能阅读完成。
一、Golang 源码下载及装置
官网地址:https://golang.google.cn/dl/
下载安装包或源码包均可
wget https://golang.google.cn/dl/go1.15.4.src.tar.gz
安装包下载很简略 设置好环境变量。。源码装置
cd go/src
./all.bash
装置报错
# ./all.bash
./make.bash: line 165: /root/go1.4/bin/go: No such file or directory
Building Go cmd/dist using /root/go1.4. ()
ERROR: Cannot find /root/go1.4/bin/go.
Set $GOROOT_BOOTSTRAP to a working Go tree >= Go 1.4.
# GOROOT_BOOTSTRAP 设置 go1.4 版本地址 实现了自举,编译后续版本
咱们学习的对象就是 Go 语言版本的编译源码。
二、调试办法
1、DLV 调试编译过程
调试编译器执行过程,必须调试 go 装置目录下的源码,即 GOROOT 下源码,否则报外部包抵触谬误。
编译器目录为 go/src/cmd/compile,调试命令如下:
# 同样必须是 GOROOT 下源码
dlv debug /usr/local/go/src/cmd/compile/main.go
#设置断点 通过包名及办法设置断点
b main.main
#设置编译器编译指标文件
r /Users/xxx/go/src/test/debug1/main.go
#开始调试 continue
c
#执行下一行代码
n
# 打印关怀的变量
p archInit
cmd/compile/internal/amd64.Init
# 通过代码文件及行数设置断点
b /usr/local/go/src/cmd/compile/internal/gc/main.go:345
p outfile
#间接通过函数名设置断点
b parseFiles
b funccompile
2、Goland 调试编译过程
同理 goland 调试编译器 调试的源码也必须位于 go 装置目录
首先须要通过 Goland 新建我的项目 go 我的项目目录位于 GOROOT,例如 /user/local/go
Goland Debug 配置办法如下 和 dlv 相似
关上 /usr/local/go/src/cmd/compile/main.go
点击 main 函数左侧绿色小三角形,执行 debug go build main.go
(未设置则主动)弹出 go build 配置框或点击右上方 Edit Debug Configurations 菜单按钮从新设置
Files /usr/local/go/src/cmd/compile/main.go
# 设置有权限的工作目录
Working Directory /Users/xxx/golang/compile/w
# 设置编译指标文件
Program arguments /Users/xxx/go/src/test/debug1/main.go
以上是 Goland debug 编译器的要害配置,具体应用办法,自行百度,和一般程序 debug 调试用法统一。
三、编译过程概述
编译过程
1、Go 的编译器在逻辑上能够被分成四个阶段:词法与语法分析、类型检查和 AST 转换、通用 SSA 生成和最初的机器代码生成
编译前端 编译后端
---------- | ---------- |
| 词法剖析 | 两头码生成 |
|---------------------- |
| 语法分析 | 代码优化 |
|---------------------- |
| 语义剖析 | 机器码生成 |
|--------- | ---------- |
- 词法剖析生成 token [scanner.next()];
- 语法分析生成形象语法树 AST, 每一个 AST 都对应着一个独自的 Go 语言文件 [parse.decls()];
- 语义剖析进行类型查看,优化改写 ast [checktype1()];
- 两头码生成将形象语法树会通过多轮解决转变成最初的领有 动态单赋值 (SSA) 个性的两头码, 剖析出代码中的无用变量和片段并对代码进行优化;
- 高效代码替换低效的代码,力不从心的进行优化;
- 生成汇编代码(Plan9),进而生成机器码。
词法与语法分析
词法剖析是计算机科学中将字符序列转换为标记(token)序列的过程,词法剖析就是解析源代码文件,它将文件中的字符串序列转换成 Token 序列,不便前面的解决和解析。
语法分析的输出就是词法分析器输入的 Token 序列,这些序列会依照程序被语法分析器进行解析,语法分析过程就是将词法剖析生成的 Token 依照语言定义好的文法(Grammar)自下而上或者自上而下的进行规约,每一个 Go 的源代码文件最终会被演绎成一个 SourceFile 构造。
词法剖析会返回一个不蕴含空格、换行等字符的 Token 序列,例如:package
, import
, (
, io
, )
, …,而语法分析会把 Token 序列转换成有意义的构造体,也就是形象语法树 (AST)。
每一个 AST 都对应着一个独自的 Go 语言文件,这个形象语法树中包含以后文件属于的包名、定义的常量、构造体和函数等。
类型查看
当拿到一组文件的形象语法树之后,Go 语言的编译器会对语法树中定义和应用的类型进行查看,类型查看别离会依照以下的程序对不同类型的节点进行验证和解决:
- 常量、类型和函数名及类型;
- 变量的赋值和初始化;
- 函数和闭包的主体;
- 哈希键值对的类型;
- 导入函数体;
- 内部的申明;
通过对整棵形象语法树的遍历,咱们在每一个节点上都会对以后子树的类型进行验证,以保障以后节点上不会呈现类型谬误的问题,所有的类型谬误和不匹配都会在这一个阶段被发现和裸露进去,构造体是否实现了某些接口也会在这一阶段被查看进去。
两头代码生成
当咱们将源文件转换成了形象语法树、对整棵树的语法进行解析并进行类型查看之后,就能够认为以后文件中的代码不存在语法错误和类型谬误的问题了,Go 语言的编译器就会将输出的形象语法树转换成中间代码。
在类型查看之后,就会通过一个名为 compileFunctions
的函数开始对整个 Go 语言我的项目中的全副函数进行编译,这些函数会在一个编译队列中期待几个后端工作协程的生产,这些并发执行的 Goroutine 会将所有函数对应的形象语法树转换成中间代码。
因为 Go 语言编译器的中间代码应用了 SSA 的个性,所以在这一阶段咱们就可能剖析出代码中的无用变量和片段并对代码进行优化。
机器码生成
Go 语言源代码的 src/cmd/compile/internal
目录中蕴含了很多机器码生成相干的包,不同类型的 CPU 别离应用了不同的包生成机器码,其中包含 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm。
四、编译源代码学习
词法剖析筹备
通过 main 函数,不难追踪到文件解析入口函数 parseFiles,该函数为每个文件创建了一个 noder 对象,noder 领有一个词法文件属性存储该源文件对应词法剖析的后果。
单个源文件对应的词法树 noder 构造体
// noder transforms package syntax's AST into a Node tree.
type noder struct {basemap map[*syntax.PosBase]*src.PosBase
basecache struct {
last *syntax.PosBase
base *src.PosBase
}
file *syntax.File // 词法剖析文件对象指针
linknames []linkname
pragcgobuf [][]string
err chan syntax.Error
scope ScopeID
// scopeVars is a stack tracking the number of variables declared in the
// current function at the moment each open scope was opened. scopeVars []int
lastCloseScopePos syntax.Pos
}
parseFiles 办法开启协程通过 syntax.Parse 进行文件解析.
Parse 函数代码如下:
func Parse(base *PosBase, src io.Reader, errh ErrorHandler, pragh PragmaHandler, mode Mode) (_ *File, first error) {defer func() {if p := recover(); p != nil {if err, ok := p.(Error); ok {
first = err
return
}
panic(p)
}
}()
var p parser
p.init(base, src, errh, pragh, mode)
p.next() // 开始辨认 token
return p.fileOrNil(), p.first //p.fileOrNil()开始生成 token 词法树
}
解析过程中,创立了文件解析器 parser,其中继承了扫描器 sanner 的办法和属性
# 解析器构造体
type parser struct {
file *PosBase
errh ErrorHandler
mode Mode
scanner // 继承扫描器 scanner 的办法属性
base *PosBase // current position base
first error // first error encountered
errcnt int // number of errors encountered
pragma Pragma // pragma flags
fnest int // function nesting level (for error handling)
xnest int // expression nesting level (for complit ambiguity resolution)
indent []byte // tracing support}
扫描器的 fileOrNil()办法返回词法文件构造体 syntax.File,也就是解析结束的词法树,构造体如下:
// package PkgName; DeclList[0], DeclList[1], ...
type File struct {
PkgName *Name
DeclList []Decl
Lines uint
node}
词法剖析过程
Go 语言的词法解析是通过 src/cmd/compile/internal/syntax/scanner.go 文件中的 scanner
构造体实现的,这个构造领会持有以后扫描的数据源文件、启用的模式和以后被扫描到的 Token。
# 扫描器构造体
type scanner struct {
source
mode uint
nlsemi bool // if set 'n' and EOF translate to ';'
// current token, valid after calling next() line, col uint
tok token
lit string // valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF"); may be malformed if bad is true
bad bool // valid if tok is _Literal, true if a syntax error occurred, lit may be malformed
kind LitKind // valid if tok is _Literal
op Operator // valid if tok is _Operator, _AssignOp, or _IncOp
prec int // valid if tok is _Operator, _AssignOp, or _IncOp
}
src/cmd/compile/internal/syntax/tokens.go
文件中定义了 Go 语言中反对的全副 Token 类型,所有的 token
类型都是正整数,你能够在这个文件中找到一些常见 Token 的定义,例如:操作符、括号和关键字等:
const (
_ token = iota
_EOF // EOF
// names and literals _Name // name
_Literal // literal
// operators and operations // _Operator is excluding '*' (_Star) _Operator // op
_AssignOp // op=
_IncOp // opop
_Assign // =
_Define // :=
_Arrow // <-
_Star // *
// delimiters _Lparen // (
_Lbrack // [
_Lbrace // {_Rparen //)
_Rbrack // ]
_Rbrace // }
_Comma // ,
_Semi // ;
_Colon // :
_Dot // .
_DotDotDot // ...
// keywords _Break // break
_Case // case
_Chan // chan
_Const // const
_Continue // continue
_Default // default
_Defer // defer
_Else // else
_Fallthrough // fallthrough
_For // for
_Func // func
_Go // go
_Goto // goto
_If // if
_Import // import
_Interface // interface
_Map // map
_Package // package
_Range // range
_Return // return
_Select // select
_Struct // struct
_Switch // switch
_Type // type
_Var // var
// empty line comment to exclude it from .String tokenCount //
)
最右推导加向前查看形成了 Go 语言词法分析器的最基本原理,次要是由 scanner
这个构造体中的 next
办法驱动来获取下一个词法 token, 办法代码如下:
func (s *scanner) next() {
nlsemi := s.nlsemi
s.nlsemi = false
redo:
// skip white space
c := s.getr()
for c == '' || c =='t'|| c =='n'&& !nlsemi || c =='r' {c = s.getr()
}
// token start
s.line, s.col = s.source.line0, s.source.col0
if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) {s.ident()
return
}
switch c {
case -1:
if nlsemi {
s.lit = "EOF"
s.tok = _Semi
break
}
s.tok = _EOF
case 'n':
s.lit = "newline"
s.tok = _Semi
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
s.number(c)
case '"':
s.stdString()
case '`':
s.rawString()
case ''':
s.rune()
case '(':
s.tok = _Lparen
case '[':
s.tok = _Lbrack
case '{':
s.tok = _Lbrace
case ',':
s.tok = _Comma
case ';':
s.lit = "semicolon"
s.tok = _Semi
case ')':
s.nlsemi = true
s.tok = _Rparen
case ']':
s.nlsemi = true
s.tok = _Rbrack
case '}':
s.nlsemi = true
s.tok = _Rbrace
case ':':
if s.getr() == '=' {
s.tok = _Define
break
}
s.ungetr()
s.tok = _Colon
case '.':
c = s.getr()
if isDecimal(c) {s.ungetr()
s.unread(1) // correct position of '.' (needed by startLit in number)
s.number('.')
break
}
if c == '.' {c = s.getr()
if c == '.' {
s.tok = _DotDotDot
break
}
s.unread(1)
}
s.ungetr()
s.tok = _Dot
case '+':
s.op, s.prec = Add, precAdd
c = s.getr()
if c != '+' {goto assignop}
s.nlsemi = true
s.tok = _IncOp
case '-':
s.op, s.prec = Sub, precAdd
c = s.getr()
if c != '-' {goto assignop}
s.nlsemi = true
s.tok = _IncOp
case '*':
s.op, s.prec = Mul, precMul
// don't goto assignop - want _Star token
if s.getr() == '=' {
s.tok = _AssignOp
break
}
s.ungetr()
s.tok = _Star
case '/':
c = s.getr()
if c == '/' {s.lineComment()
goto redo
}
if c == '*' {s.fullComment()
if s.source.line > s.line && nlsemi {
// A multi-line comment acts like a newline;
// it translates to a ';' if nlsemi is set. s.lit = "newline"
s.tok = _Semi
break
}
goto redo
}
s.op, s.prec = Div, precMul
goto assignop
case '%':
s.op, s.prec = Rem, precMul
c = s.getr()
goto assignop
case '&':
c = s.getr()
if c == '&' {
s.op, s.prec = AndAnd, precAndAnd
s.tok = _Operator
break
}
s.op, s.prec = And, precMul
if c == '^' {
s.op = AndNot
c = s.getr()}
goto assignop
case '|':
c = s.getr()
if c == '|' {
s.op, s.prec = OrOr, precOrOr
s.tok = _Operator
break
}
s.op, s.prec = Or, precAdd
goto assignop
case '^':
s.op, s.prec = Xor, precAdd
c = s.getr()
goto assignop
case '<':
c = s.getr()
if c == '=' {
s.op, s.prec = Leq, precCmp
s.tok = _Operator
break
}
if c == '<' {
s.op, s.prec = Shl, precMul
c = s.getr()
goto assignop
}
if c == '-' {
s.tok = _Arrow
break
}
s.ungetr()
s.op, s.prec = Lss, precCmp
s.tok = _Operator
case '>':
c = s.getr()
if c == '=' {
s.op, s.prec = Geq, precCmp
s.tok = _Operator
break
}
if c == '>' {
s.op, s.prec = Shr, precMul
c = s.getr()
goto assignop
}
s.ungetr()
s.op, s.prec = Gtr, precCmp
s.tok = _Operator
case '=':
if s.getr() == '=' {
s.op, s.prec = Eql, precCmp
s.tok = _Operator
break
}
s.ungetr()
s.tok = _Assign
case '!':
if s.getr() == '=' {
s.op, s.prec = Neq, precCmp
s.tok = _Operator
break
}
s.ungetr()
s.op, s.prec = Not, 0
s.tok = _Operator
default:
s.tok = 0
s.errorf("invalid character %#U", c)
goto redo
}
return
assignop:
if c == '=' {
s.tok = _AssignOp
return
}
s.ungetr()
s.tok = _Operator
}
通过 getr 获取下一个字符,通过许多不同的 switch/case 来辨认 token。
而通过解析器 (parser) 的 fileOrNil 办法来将辨认的 token 退出到词法树:
func (p *parser) fileOrNil() *File {
if trace {defer p.trace("file")()}
f := new(File)
f.pos = p.pos()
// PackageClause
if !p.got(_Package) {p.syntaxError("package statement must be first")
return nil
}
f.PkgName = p.name()
p.want(_Semi)
// don't bother continuing if package clause has errors
if p.first != nil {return nil}
// {ImportDecl ";"}
for p.got(_Import) {f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
p.want(_Semi)
}
// {TopLevelDecl ";"}
for p.tok != _EOF {
switch p.tok {
case _Const:
p.next()
f.DeclList = p.appendGroup(f.DeclList, p.constDecl)
case _Type:
p.next()
f.DeclList = p.appendGroup(f.DeclList, p.typeDecl)
case _Var:
p.next()
f.DeclList = p.appendGroup(f.DeclList, p.varDecl)
case _Func:
p.next()
if d := p.funcDeclOrNil(); d != nil {f.DeclList = append(f.DeclList, d)
}
default:
if p.tok == _Lbrace && len(f.DeclList) > 0 && isEmptyFuncDecl(f.DeclList[len(f.DeclList)-1]) {
// opening { of function declaration on next line
p.syntaxError("unexpected semicolon or newline before {")
} else {p.syntaxError("non-declaration statement outside function body")
}
p.advance(_Const, _Type, _Var, _Func)
continue
}
// Reset p.pragma BEFORE advancing to the next token (consuming ';')
// since comments before may set pragmas for the next function decl. p.pragma = 0
if p.tok != _EOF && !p.got(_Semi) {p.syntaxError("after top level declaration")
p.advance(_Const, _Type, _Var, _Func)
}
}
// p.tok == _EOF
f.Lines = p.source.line
return f
}
通过 fileOrNil 办法,不难看出 go 语言四种不同的顶级申明 token(TopLevelDecl)词法节点:_Const(常量)、_Type(类型)、_Var(变量)、_Func(函数及办法)以及_Package 和_Import 申明 token。
解析器还封装了 p.want()和 p.got(),进行前驱剖析和用意判断。
func (p *parser) got(tok token) bool {
if p.tok == tok {p.next()// 如果以后 tok 和指定 token 相等,返回 true 并主动前驱获取下个 token
return true
}
return false
}
func (p *parser) want(tok token) {if !p.got(tok) {// 如果不合乎预期则报语法错误。p.syntaxError("expecting" + tokstring(tok))
p.advance()}
}
got
其实只是用于疾速判断一些语句中的关键字,如果以后解析器中的 Token 是传入的 Token 就会间接跳过该 Token 并返回 true
;而 want
就是对 got
的简略封装了,如果以后的 Token 不是咱们冀望的,就会立即返回语法错误并完结这次编译。
词法剖析节点介绍
// 顶级申明
type (
Decl interface {
Node
aDecl()}
// Path
// LocalPkgName Path
ImportDecl struct {
LocalPkgName *Name // including "."; nil means no rename present
Path *BasicLit
Group *Group // nil means not part of a group
decl
}
// NameList
// NameList = Values // NameList Type = Values
ConstDecl struct {NameList []*Name
Type Expr // nil means no type
Values Expr // nil means no values
Group *Group // nil means not part of a group
decl
}
// Name Type
TypeDecl struct {
Name *Name
Alias bool
Type Expr
Group *Group // nil means not part of a group
Pragma Pragma
decl }
// NameList Type
// NameList Type = Values // NameList = Values
VarDecl struct {NameList []*Name
Type Expr // nil means no type
Values Expr // nil means no values
Group *Group // nil means not part of a group
decl
}
// func Name Type {Body}
// func Name Type // func Receiver Name Type {Body} // func Receiver Name Type
FuncDecl struct {Attr map[string]bool // go:attr map
Recv *Field // nil means regular function
Name *Name
Type *FuncType
Body *BlockStmt // nil means no body (forward declaration)
Pragma Pragma // TODO(mdempsky): Cleaner solution.
decl
}
)
以 FuncDecl 为例,函数体存储类型为 BlockStmt 指针,常见 statments 定义如下:
// Statements
type (
Stmt interface {
Node
aStmt()}
SimpleStmt interface {
Stmt
aSimpleStmt()}
EmptyStmt struct {simpleStmt}
LabeledStmt struct {
Label *Name
Stmt Stmt
stmt }
BlockStmt struct {List []Stmt
Rbrace Pos
stmt }
ExprStmt struct {
X Expr
simpleStmt }
SendStmt struct {
Chan, Value Expr // Chan <- Value
simpleStmt
}
DeclStmt struct {DeclList []Decl
stmt }
AssignStmt struct {
Op Operator // 0 means no operation
Lhs,Rhs Expr // Rhs == ImplicitOne means Lhs++ (Op == Add) or Lhs-- (Op == Sub)
simpleStmt
}
BranchStmt struct {
Tok token // Break, Continue, Fallthrough, or Goto
Label *Name
// Target is the continuation of the control flow after executing
// the branch; it is computed by the parser if CheckBranches is set. // Target is a *LabeledStmt for gotos, and a *SwitchStmt, *SelectStmt, // or *ForStmt for breaks and continues, depending on the context of // the branch. Target is not set for fallthroughs. Target Stmt
stmt }
CallStmt struct {
Tok token // Go or Defer
Call *CallExpr
stmt }
ReturnStmt struct {
Results Expr // nil means no explicit return values
stmt
}
IfStmt struct {
Init SimpleStmt
Cond Expr
Then *BlockStmt
Else Stmt // either nil, *IfStmt, or *BlockStmt
stmt
}
ForStmt struct {
Init SimpleStmt // incl. *RangeClause
Cond Expr
Post SimpleStmt
Body *BlockStmt
stmt }
SwitchStmt struct {
Init SimpleStmt
Tag Expr // incl. *TypeSwitchGuard
Body []*CaseClause
Rbrace Pos
stmt }
SelectStmt struct {Body []*CommClause
Rbrace Pos
stmt }
)
这些不同类型的 Stmt
形成了全副命令式的 Go 语言代码,从中咱们能够看到十分多相熟的控制结构,例如 if
、for
、switch
和 select
。
语法分析
因为词法分析器 scanner
作为构造体被嵌入到了 parser
中,所以这个办法中的 p.next()
实际上调用的是 scanner
的 next
办法,它会间接获取文件中的下一个 Token,所以词法剖析和语法分析其实是一起进行的。
下面 parseFiles 办法中扫描器和解析器 (syntax.Parse) 生成源文件对应的 syntax.File 对象后,生成语法树的过程是在 p.node()执行过程中生成的
func (p *noder) node() {
types.Block = 1
imported_unsafe = false
p.setlineno(p.file.PkgName)
mkpackage(p.file.PkgName.Value)
xtop = append(xtop, p.decls(p.file.DeclList)...)// 生成形象语法树
for _, n := range p.linknames {
if !imported_unsafe {p.yyerrorpos(n.pos, "//go:linkname only allowed in Go files that import"unsafe"")
continue
}
s := lookup(n.local)
if n.remote != "" {s.Linkname = n.remote} else {
// Use the default object symbol name if the
// user didn't provide one. if myimportpath =="" {p.yyerrorpos(n.pos, "//go:linkname requires linkname argument or -p compiler flag")
} else {s.Linkname = objabi.PathToPrefix(myimportpath) + "." + n.local
}
}
}
// The linker expects an ABI0 wrapper for all cgo-exported
// functions. for _, prag := range p.pragcgobuf {switch prag[0] {
case "cgo_export_static", "cgo_export_dynamic":
if symabiRefs == nil {symabiRefs = make(map[string]obj.ABI)
}
symabiRefs[prag[1]] = obj.ABI0
}
}
pragcgobuf = append(pragcgobuf, p.pragcgobuf...)
lineno = src.NoXPos
clearImports()}
//xtop 申明如下:var xtop []*Node
语法树单个树节点定义如下:
// A Node is a single node in the syntax tree.
// Actually the syntax tree is a syntax DAG, because there is only one
// node with Op=ONAME for a given instance of a variable x.
// The same is true for Op=OTYPE and Op=OLITERAL. See Node.mayBeShared.
type Node struct {
// Tree structure.
// Generic recursive walks should follow these fields. Left *Node
Right *Node
Ninit Nodes
Nbody Nodes
List Nodes
Rlist Nodes
// most nodes
Type *types.Type
Orig *Node // original form, for printing, and tracking copies of ONAMEs
// func Func *Func
// ONAME, OTYPE, OPACK, OLABEL, some OLITERAL
Name *Name
Sym *types.Sym // various
E interface{} // Opt or Val, see methods below
// Various. Usually an offset into a struct. For example: // - ONAME nodes that refer to local variables use it to identify their stack frame position. // - ODOT, ODOTPTR, and ORESULT use it to indicate offset relative to their base address. // - OSTRUCTKEY uses it to store the named field's offset. // - Named OLITERALs use it to store their ambient iota value. // - OINLMARK stores an index into the inlTree data structure. // - OCLOSURE uses it to store ambient iota value, if any. // Possibly still more uses. If you find any, document them. Xoffset int64
Pos src.XPos
flags bitset32
Esc uint16 // EscXXX
Op Op
aux uint8
}
总结
理解 Go 语言的词法分析器 scanner
和语法分析器 parser
让咱们对解析器解决源代码的过程有着比较清楚的意识,同时咱们也在 Go 语言的文法和语法分析器中找到了相熟的关键字和语法结构,加深了对 Go 语言的了解。
待续