>  기사  >  백엔드 개발  >  Go 언어를 컴파일해야 합니까?

Go 언어를 컴파일해야 합니까?

青灯夜游
青灯夜游원래의
2022-12-01 19:06:357105검색

Go 언어를 컴파일해야 합니다. Go 언어는 컴파일된 정적 ​​언어입니다. 즉, Go 언어 프로그램을 실행하려면 먼저 컴파일러에서 바이너리 기계 코드(바이너리 실행 파일)를 생성해야 합니다. 바이너리 파일은 대상 컴퓨터에서 실행될 수 있습니다.

Go 언어를 컴파일해야 합니까?

이 튜토리얼의 운영 환경: Windows 7 시스템, GO 버전 1.18, Dell G3 컴퓨터.

Go 언어는 실행하기 위해 컴파일이 필요한 프로그래밍 언어입니다. 즉, 실행하기 전에 바이너리 기계 코드를 생성하기 위해 컴파일러에서 코드를 생성해야 하며 그런 다음 바이너리 파일을 대상 시스템에서 실행할 수 있습니다.

간단히 말하면 Go 언어는 컴파일된 정적 ​​언어(C 언어와 동일)이므로 Go 언어 프로그램을 실행하기 전에 바이너리 실행 파일로 컴파일해야 합니다.

Go 언어의 구현 원리를 이해하려면 Go 언어의 컴파일 과정을 이해하는 것이 빼놓을 수 없습니다. Go 언어가 어떻게 컴파일을 완료하는지 살펴보겠습니다.

사전 지식

Go 언어의 컴파일 과정을 깊이 이해하고 싶다면 컴파일 과정에 관련된 몇 가지 용어와 전문 지식을 미리 이해해야 합니다. 이 지식은 실제로 일상 업무와 학습에 사용하기 어렵지만 편집 과정과 원리를 이해하는 데는 여전히 매우 중요합니다.

1) 추상 구문 트리

컴퓨터 과학에서 추상 구문 트리(AST), 줄여서 구문 트리는 소스 코드의 문법 구조를 추상적으로 표현한 것입니다. 프로그래밍 언어의 문법 구조를 트리 형태로 나타내며, 트리의 각 노드는 소스 코드의 구조를 나타냅니다.

문법이 "추상적"이라고 불리는 이유는 여기서의 문법이 실제 문법에 나타나는 모든 세부 사항을 나타내지는 않기 때문입니다. 예를 들어, 중첩된 괄호는 트리 구조에 내재되어 있으며 노드로 표시되지 않습니다. if else와 같은 조건문은 두 개의 분기가 있는 노드로 표시될 수 있습니다.

산술식 1+3*(4-1)+2를 예로 들어 보겠습니다. 구문 분석할 수 있는 추상 구문 트리는 아래 그림과 같습니다.

Go 언어를 컴파일해야 합니까?

그림: 추상 구문 트리

추상 구문 트리는 브라우저, 스마트 편집기, 컴파일러 등 다양한 분야에서 사용될 수 있습니다.

2) 정적 단일 할당

컴파일러 설계에서 정적 단일 할당 형식(정적 단일 할당 형식, 일반적으로 SSA 형식 또는 SSA로 약칭)은 중간 표현(IR, 중간 표현)의 속성입니다. 한 번만 할당하면 되며, 사용하기 전에 변수를 정의해야 합니다. 실제로는 각 변수를 한 번만 할당할 수 있는 기능을 달성하기 위해 일반적으로 첨자를 추가하는 데 사용합니다. 다음은 다음 코드를 사용한 간단한 예입니다.

x := 1
x := 2
y := x

위 설명에서 알 수 있듯이 할당 동작의 첫 번째 줄은 다음과 같습니다. x는 두 번째 줄에서 두 번 할당되고 세 번째 줄에서 사용되므로 SSA에서는 다음과 같은 형식이 됩니다.

x1 := 1
x2 := 2
y1 := x2

SSA를 사용하는 중간 코드에서 매우 명확하게 볼 수 있습니다. 변수 y1의 값 x1과는 전혀 관련이 없으므로 기계어 코드를 생성할 때 첫 번째 단계를 생략할 수 있으므로 이 코드 조각을 최적화하기 위해 실행해야 하는 명령어 수를 줄일 수 있습니다.

Wikipedia의 SSA 소개에 따르면 중간 코드에서 SSA 기능을 사용하면 전체 프로그램에 대해 다음과 같은 최적화를 달성할 수 있습니다.

  • 상수 전파
  • 값 범위 전파 전파)
  • sparse 조건부 상수 전파
  • eliminate 코드 제거
  • 전역 값 번호 지정
  • 부분 중복 제거 제거
  • 강도 감소
  • 등록 할당

SSA의 주요 기능은 코드 최적화이므로 컴파일러 백엔드입니다(주로 최적화 및 생성을 담당함). 대상 코드)의 일부입니다. 물론, 코드 컴파일 분야에는 SSA 외에도 많은 중간 코드 최적화 방법이 있습니다. 컴파일러에서 생성된 코드를 최적화하는 것은 매우 오래되고 복잡한 분야이므로 여기서는 소개하지 않겠습니다.

3) 명령어 세트 아키텍처

마지막으로 소개할 필수 지식은 명령어 세트 아키텍처(ISA), 명령어 세트 아키텍처라고도 불리는 컴퓨터 아키텍처 프로그래밍과 관련된 부분입니다. 기본 데이터 유형, 명령어 세트, 레지스터, 주소 지정 모드, 저장 시스템, 인터럽트, 예외 처리 및 외부 I/O가 포함됩니다. 명령어 세트 아키텍처는 특정 프로세서에서 실행되는 기본 명령인 일련의 opcode(기계어)로 구성됩니다.

일반적인 명령어 세트 아키텍처 유형은 다음과 같습니다.

  • 복잡한 명령어 세트 컴퓨팅(CISC);
  • Reduced Instruction Set Computing(RISC);
  • 명시적 병렬 명령어 세트 컴퓨팅(명시적 병렬 명령어 컴퓨팅, 줄여서 EPIC) ;
  • 매우 긴 명령 단어 명령 집합 연산(VLIW).


프로세서(CPU)마다 매우 다른 기계어를 사용하므로, 우리 프로그램을 다른 기계에서 실행하려면 아키텍처에 따라 소스 코드를 다른 기계어로 컴파일해야 합니다.

컴파일 원리

Go 언어 컴파일러의 소스 코드는 cmd/compile 디렉토리에 있습니다. 이 디렉토리에 있는 파일들이 모여 Go 언어 컴파일러를 구성합니다. 컴파일러의 프런트 엔드는 일반적으로 어휘 분석, 구문 분석, 유형 검사 및 중간 코드 생성을 담당합니다. 즉, 중간 코드를 실행할 수 있는 기계 코드로 변환합니다.

Go 언어를 컴파일해야 합니까?

Go의 컴파일러는 어휘 및 구문 분석, 유형 검사 및 AST 변환, 일반 SSA 생성 및 최종 기계 코드 생성의 네 단계로 논리적으로 나눌 수 있습니다. 이 네 단계를 각각 소개하겠습니다.

1) 어휘 및 구문 분석

모든 컴파일 프로세스는 실제로 코드의 소스 파일을 구문 분석하는 것으로 시작됩니다. 어휘 분석의 기능은 소스 코드 파일을 구문 분석하여 파일의 문자열 시퀀스를 토큰으로 변환하는 것입니다. 우리는 일반적으로 어휘 분석을 수행하는 프로그램을 렉서(lexer)라고 부릅니다.

문법 분석의 입력은 어휘 분석기에 의해 출력된 토큰 시퀀스입니다. 이 시퀀스는 문법 분석기에 의해 순서대로 구문 분석됩니다. 문법 분석 프로세스는 언어 정의 문법에 따라 어휘 분석에 의해 생성된 토큰을 변환하는 것입니다. 문법) 하향식 또는 하향식 사양을 통해 각 Go 소스 코드 파일은 결국 SourceFile 구조로 요약됩니다.

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" }

标准的 Golang 语法解析器使用的就是 LALR(1) 的文法,语法解析的结果其实就是上面介绍过的抽象语法树(AST),每一个 AST 都对应着一个单独的Go语言文件,这个抽象语法树中包括当前文件属于的包名、定义的常量、结构体和函数等。

如果在语法解析的过程中发生了任何语法错误,都会被语法解析器发现并将消息打印到标准输出上,整个编译过程也会随着错误的出现而被中止。

2) 类型检查

当拿到一组文件的抽象语法树 AST 之后,Go语言的编译器会对语法树中定义和使用的类型进行检查,类型检查分别会按照顺序对不同类型的节点进行验证,按照以下的顺序进行处理:

  • 常量、类型和函数名及类型;
  • 变量的赋值和初始化;
  • 函数和闭包的主体;
  • 哈希键值对的类型;
  • 导入函数体;
  • 外部的声明;

通过对每一棵抽象节点树的遍历,我们在每一个节点上都会对当前子树的类型进行验证保证当前节点上不会出现类型错误的问题,所有的类型错误和不匹配都会在这一个阶段被发现和暴露出来。

类型检查的阶段不止会对树状结构的节点进行验证,同时也会对一些内建的函数进行展开和改写,例如 make 关键字在这个阶段会根据子树的结构被替换成 makeslice 或者 makechan 等函数。

其实类型检查不止对类型进行了验证工作,还对 AST 进行了改写以及处理Go语言内置的关键字,所以,这一过程在整个编译流程中是非常重要的,没有这个步骤很多关键字其实就没有办法工作。【相关推荐:Go视频教程

3) 中间代码生成

当我们将源文件转换成了抽象语法树,对整个语法树的语法进行解析并进行类型检查之后,就可以认为当前文件中的代码基本上不存在无法编译或者语法错误的问题了,Go语言的编译器就会将输入的 AST 转换成中间代码。

Go语言编译器的中间代码使用了 SSA(Static Single Assignment Form) 的特性,如果我们在中间代码生成的过程中使用这种特性,就能够比较容易的分析出代码中的无用变量和片段并对代码进行优化。

在类型检查之后,就会通过一个名为 compileFunctions 的函数开始对整个Go语言项目中的全部函数进行编译,这些函数会在一个编译队列中等待几个后端工作协程的消费,这些 Goroutine 会将所有函数对应的 AST 转换成使用 SSA 特性的中间代码。

4) 机器码生成

Go语言源代码的 cmd/compile/internal 目录中包含了非常多机器码生成相关的包,不同类型的 CPU 分别使用了不同的包进行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm,也就是说Go语言能够在几乎全部常见的 CPU 指令集类型上运行。

编译器入口

Go语言的编译器入口是 src/cmd/compile/internal/gc 包中的 main.go 文件,这个 600 多行的 Main 函数就是Go语言编译器的主程序,这个函数会先获取命令行传入的参数并更新编译的选项和配置,随后就会开始运行 parseFiles 函数对输入的所有文件进行词法与语法分析得到文件对应的抽象语法树:

func Main(archInit func(*Arch)) {
    // ...
    lines := parseFiles(flag.Args())

接下来就会分九个阶段对抽象语法树进行更新和编译,就像我们在上面介绍的,整个过程会经历类型检查、SSA 中间代码生成以及机器码生成三个部分:

  • 检查常量、类型和函数的类型;
  • 处理变量的赋值;
  • 对函数的主体进行类型检查;
  • 决定如何捕获变量;
  • 检查内联函数的类型;
  • 进行逃逸分析;
  • 将闭包的主体转换成引用的捕获变量;
  • 编译顶层函数;
  • 检查外部依赖的声明;

了解了剩下的编译过程之后,我们重新回到词法和语法分析后的具体流程,在这里编译器会对生成语法树中的节点执行类型检查,除了常量、类型和函数这些顶层声明之外,它还会对变量的赋值语句、函数主体等结构进行检查:

for i := 0; i < len(xtop); i++ {
    n := xtop[i]
    if op := n.Op; op != ODCL && op != OAS && op != OAS2 && (op != ODCLTYPE || !n.Left.Name.Param.Alias) {
        xtop[i] = typecheck(n, ctxStmt)
    }
}

for i := 0; i < len(xtop); i++ {
    n := xtop[i]
    if op := n.Op; op == ODCL || op == OAS || op == OAS2 || op == ODCLTYPE && n.Left.Name.Param.Alias {
        xtop[i] = typecheck(n, ctxStmt)
    }
}

for i := 0; i < len(xtop); i++ {
    n := xtop[i]
    if op := n.Op; op == ODCLFUNC || op == OCLOSURE {
        typecheckslice(Curfn.Nbody.Slice(), ctxStmt)
    }
}

checkMapKeys()

for _, n := range xtop {
    if n.Op == ODCLFUNC && n.Func.Closure != nil {
        capturevars(n)
    }
}

escapes(xtop)

for _, n := range xtop {
    if n.Op == ODCLFUNC && n.Func.Closure != nil {
        transformclosure(n)
    }
}

类型检查会对传入节点的子节点进行遍历,这个过程会对 make 等关键字进行展开和重写,类型检查结束之后并没有输出新的数据结构,只是改变了语法树中的一些节点,同时这个过程的结束也意味着源代码中已经不存在语法错误和类型错误,中间代码和机器码也都可以正常的生成了。

    initssaconfig()

    peekitabs()

    for i := 0; i < len(xtop); i++ {
        n := xtop[i]
        if n.Op == ODCLFUNC {
            funccompile(n)
        }
    }

    compileFunctions()

    for i, n := range externdcl {
        if n.Op == ONAME {
            externdcl[i] = typecheck(externdcl[i], ctxExpr)
        }
    }

    checkMapKeys()
}

在主程序运行的最后,会将顶层的函数编译成中间代码并根据目标的 CPU 架构生成机器码,不过这里其实也可能会再次对外部依赖进行类型检查以验证正确性。

总结

Go语言的编译过程其实是非常有趣并且值得学习的,通过对Go语言四个编译阶段的分析和对编译器主函数的梳理,我们能够对 Golang 的实现有一些基本的理解,掌握编译的过程之后,Go语言对于我们来讲也不再那么神秘,所以学习其编译原理的过程还是非常有必要的。

更多编程相关知识,请访问:编程视频!!

위 내용은 Go 언어를 컴파일해야 합니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.