一、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 directoryBuilding 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 archInitcmd/compile/internal/amd64.Init# 通过代码文件及行数设置断点b /usr/local/go/src/cmd/compile/internal/gc/main.go:345p outfile#间接通过函数名设置断点b parseFilesb 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 = falseredo: // 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 } returnassignop: 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 TypeTypeDecl 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定义如下:
// Statementstype ( 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 语言的了解。
待续