20200706 go compiler && go viper

2020. 7. 7. 00:12개발/Go

1. How to compile go

https://getstream.io/blog/how-a-go-program-compiles-down-to-machine-code/참조...

go compiler에는 세가지의 phase가 있다고 한다:

1.Scanner - code를 toke단위로 parse해주는 역할

2.Parser - token들을 Abstract Syntax Tree로 바꿔주는 역할

3.Code Generation - Abstract Syntax Tree를 machine code로 바꾸는 역할

 

놀랍게도 이를 구현해주는 package들이 있다고 한다. (go/scanner, go/parser, go/token, go/ast) -> 이를 활용하면 go code를 활용하여 machine code로 변환되는 각 과정을 자세하게 살펴볼 수 있다. 먼저 scanner에 의해 어떻게 변환되는지 살펴보자.

package main

import (
  "fmt"
  "go/scanner"
  "go/token"
)

func main() {
  src := []byte(`package main
import "fmt"
func main() {
  fmt.Println("Hello, world!")
}
`)

  var s scanner.Scanner
  fset := token.NewFileSet()
  file := fset.AddFile("", fset.Base(), len(src))
  s.Init(file, src, nil, 0)

  for {
     pos, tok, lit := s.Scan()
     fmt.Printf("%-6s%-8s%q\n", fset.Position(pos), tok, lit)

     if tok == token.EOF {
        break
     }
  }
}

위 코드는 Hello, world!를 출력하는 코드를 scanner로 token단위로 parse하여 각각의 token을 보여주는 코드이다. 결과는 다음과 같다. 

1:1   package "package"
1:9   IDENT   "main"
1:13  ;       "\n"
2:1   import  "import"
2:8   STRING  "\"fmt\""
2:13  ;       "\n"
3:1   func    "func"
3:6   IDENT   "main"
3:10  (       ""
3:11  )       ""
3:13  {       ""
4:3   IDENT   "fmt"
4:6   .       ""
4:7   IDENT   "Println"
4:14  (       ""
4:15  STRING  "\"Hello, world!\""
4:30  )       ""
4:31  ;       "\n"
5:1   }       ""
5:2   ;       "\n"
5:3   EOF     ""

각 token의 line과 index가 왼쪽에, 정보는 가운데에, 실제 token은 오른쪽에 배치되어있다. 특징할만한 점은 "\n"이 있을때마다 semicolon이 자동으로 붙여지는 것이다. 우리가 go에서 semilcolon을 안붙여도 되는 이유이다.(if err := blabla(); err != nil{}같은 경우 제외)

 

그렇다면 이것을 parser는 어떻게 해석할까?

package main

import (
  "go/ast"
  "go/parser"
  "go/token"
  "log"
)

func main() {
  src := []byte(`package main
import "fmt"
func main() {
  fmt.Println("Hello, world!")
}
`)

  fset := token.NewFileSet()

  file, err := parser.ParseFile(fset, "", src, 0)
  if err != nil {
     log.Fatal(err)
  }

  ast.Print(fset, file)
}

위 코드는 abstract syntax tree를 보여주는 코드이다.

결과는 아래와 같다.

     0  *ast.File {
     1  .  Package: 1:1
     2  .  Name: *ast.Ident {
     3  .  .  NamePos: 1:9
     4  .  .  Name: "main"
     5  .  }
     6  .  Decls: []ast.Decl (len = 2) {
     7  .  .  0: *ast.GenDecl {
     8  .  .  .  TokPos: 3:1
     9  .  .  .  Tok: import
    10  .  .  .  Lparen: -
    11  .  .  .  Specs: []ast.Spec (len = 1) {
    12  .  .  .  .  0: *ast.ImportSpec {
    13  .  .  .  .  .  Path: *ast.BasicLit {
    14  .  .  .  .  .  .  ValuePos: 3:8
    15  .  .  .  .  .  .  Kind: STRING
    16  .  .  .  .  .  .  Value: "\"fmt\""
    17  .  .  .  .  .  }
    18  .  .  .  .  .  EndPos: -
    19  .  .  .  .  }
    20  .  .  .  }
    21  .  .  .  Rparen: -
    22  .  .  }
    23  .  .  1: *ast.FuncDecl {
    24  .  .  .  Name: *ast.Ident {
    25  .  .  .  .  NamePos: 5:6
    26  .  .  .  .  Name: "main"
    27  .  .  .  .  Obj: *ast.Object {
    28  .  .  .  .  .  Kind: func
    29  .  .  .  .  .  Name: "main"
    30  .  .  .  .  .  Decl: *(obj @ 23)
    31  .  .  .  .  }
    32  .  .  .  }
    33  .  .  .  Type: *ast.FuncType {
    34  .  .  .  .  Func: 5:1
    35  .  .  .  .  Params: *ast.FieldList {
    36  .  .  .  .  .  Opening: 5:10
    37  .  .  .  .  .  Closing: 5:11
    38  .  .  .  .  }
    39  .  .  .  }
    40  .  .  .  Body: *ast.BlockStmt {
    41  .  .  .  .  Lbrace: 5:13
    42  .  .  .  .  List: []ast.Stmt (len = 1) {
    43  .  .  .  .  .  0: *ast.ExprStmt {
    44  .  .  .  .  .  .  X: *ast.CallExpr {
    45  .  .  .  .  .  .  .  Fun: *ast.SelectorExpr {
    46  .  .  .  .  .  .  .  .  X: *ast.Ident {
    47  .  .  .  .  .  .  .  .  .  NamePos: 6:2
    48  .  .  .  .  .  .  .  .  .  Name: "fmt"
    49  .  .  .  .  .  .  .  .  }
    50  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
    51  .  .  .  .  .  .  .  .  .  NamePos: 6:6
    52  .  .  .  .  .  .  .  .  .  Name: "Println"
    53  .  .  .  .  .  .  .  .  }
    54  .  .  .  .  .  .  .  }
    55  .  .  .  .  .  .  .  Lparen: 6:13
    56  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
    57  .  .  .  .  .  .  .  .  0: *ast.BasicLit {
    58  .  .  .  .  .  .  .  .  .  ValuePos: 6:14
    59  .  .  .  .  .  .  .  .  .  Kind: STRING
    60  .  .  .  .  .  .  .  .  .  Value: "\"Hello, world!\""
    61  .  .  .  .  .  .  .  .  }
    62  .  .  .  .  .  .  .  }
    63  .  .  .  .  .  .  .  Ellipsis: -
    64  .  .  .  .  .  .  .  Rparen: 6:29
    65  .  .  .  .  .  .  }
    66  .  .  .  .  .  }
    67  .  .  .  .  }
    68  .  .  .  .  Rbrace: 7:1
    69  .  .  .  }
    70  .  .  }
    71  .  }
    ..  .  .. // Left out for brevity
    83  }

  

 위 tree를 보면, decl field는 import, variable, constants, functions와 같은 것들을 선언하는 부류이다. 이 tree를 그림으로 나타내면 아래와 같다고 한다.

package main이 가장 위쪽에 있고, 선언되는 것이 두가지가 있다 -> import "fmt", func main() 그래서 []ast.Decl은 length가 2이며, 각각에 대한 정보가 가지를 치며 뻗어나간다. main function은 이름, declaration, body 세가지로 구성되어 있으며 이는 위 그림에 잘 나타나있다. 

 body를 자세히 보면, statement는 ast.ExprStmt이며, 이는 다시 CallExpr를 포함하는데, 실질적인 function call을 한다. function call을 할때 이를 이루는 요소는 fun, 그리고 args인데, 보이는 그대로 함수와 arguments이다. Fun은 function call에 대한 reference를 가지고 있는데, 그것이 바로 SelectorExpr이다. 

 이제 이를 기반으로 code generation을 살펴보자. go code는 다음과 같다.

package main

import "fmt"

func main() {
  fmt.Println(2)
}

 

 golang을 compile할 때 어떤 환경변수 값들을 설정하여야 한다고 한다. 이는 다음과 같은 명령문으로 행해진다.

GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags "-S" main.go

optimize가 되기 이전의 코드는 다음과 같다. 

b1:
	v1  = InitMem <mem>
	v2  = SP <uintptr>
	v3  = SB <uintptr>
	v4  = ConstInterface <interface {}>
	v5  = ArrayMake1 <[1]interface {}> v4
	v6  = VarDef <mem> {.autotmp_0} v1
	v7  = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v6
	v8  = Store <mem> {[1]interface {}} v7 v5 v6
	v9  = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v8
	v10 = Addr <*uint8> {type.int} v3
	v11 = Addr <*int> {"".statictmp_0} v3
	v12 = IMake <interface {}> v10 v11
	v13 = NilCheck <void> v9 v8
	v14 = Const64 <int> [0]
	v15 = Const64 <int> [1]
	v16 = PtrIndex <*interface {}> v9 v14
	v17 = Store <mem> {interface {}} v16 v12 v8
	v18 = NilCheck <void> v9 v17
	v19 = IsSliceInBounds <bool> v14 v15
	v24 = OffPtr <*[]interface {}> [0] v2
	v28 = OffPtr <*int> [24] v2
If v19 → b2 b3 (likely) (line 6)

b2: ← b1
	v22 = Sub64 <int> v15 v14
	v23 = SliceMake <[]interface {}> v9 v22 v22
	v25 = Copy <mem> v17
	v26 = Store <mem> {[]interface {}} v24 v23 v25
	v27 = StaticCall <mem> {fmt.Println} [48] v26
	v29 = VarKill <mem> {.autotmp_0} v27
Ret v29 (line 7)

b3: ← b1
	v20 = Copy <mem> v17
	v21 = StaticCall <mem> {runtime.panicslice} v20
Exit v21 (line 6)

뭔가 컴퓨터구조시간에 많이 본 어셈블리어와 비슷한 형태이다. 사실상 명령어도 많이 비슷해 보인다. 딱봐도 Addr은 어떤 것에 대한 주소값을 저장하는 것이고, Store은 memory에 저장하라는 것이다. 이를 최적화하면 다음과 같다.

 

b1:
	v1  = InitMem <mem>
	v2  = SP <uintptr>
	v3  = SB <uintptr>
	v4  = ConstInterface <interface {}>
	v5  = ArrayMake1 <[1]interface {}> v4
	v6  = VarDef <mem> {.autotmp_0} v1
	v7  = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v6
	v8  = Store <mem> {[1]interface {}} v7 v5 v6
	v9  = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v8
	v10 = Addr <*uint8> {type.int} v3
	v11 = Addr <*int> {"".statictmp_0} v3
	v12 = IMake <interface {}> v10 v11
	v13 = NilCheck <void> v9 v8
	v14 = Const64 <int> [0]
	v15 = Const64 <int> [1]
	v16 = PtrIndex <*interface {}> v9 v14
	v17 = Store <mem> {interface {}} v16 v12 v8
	v18 = NilCheck <void> v9 v17
	v19 = IsSliceInBounds <bool> v14 v15
	v24 = OffPtr <*[]interface {}> [0] v2
	v28 = OffPtr <*int> [24] v2
If v19 → b2 b3 (likely) (line 6)

b2: ← b1
	v22 = Sub64 <int> v15 v14
	v23 = SliceMake <[]interface {}> v9 v22 v22
	v25 = Copy <mem> v17
	v26 = Store <mem> {[]interface {}} v24 v23 v25
	v27 = StaticCall <mem> {fmt.Println} [48] v26
	v29 = VarKill <mem> {.autotmp_0} v27
Ret v29 (line 7)

b3: ← b1
	v20 = Copy <mem> v17
	v21 = StaticCall <mem> {runtime.panicslice} v20
Exit v21 (line 6)

흠...이거 해석하는 건 오래 걸릴듯하니 계속해서 이어서 써보겠다.

'개발 > Go' 카테고리의 다른 글

20200715 Asynchronous and Parellel in Golang  (1) 2020.07.15
20200513 Golang 기초  (0) 2020.05.13