
Оптимальный способ изучить что-либо — взять да и повозиться, поэкспериментировать с этим и уже разобраться наконец.
Но что именно изучить? Ответ не очевиден, ведь нас, инженеров, объединяет неуемное любопытство и стремление понять то, что скрыто, изучить на глубоком уровне то, что не доступно на поверхности.
Начнем с основ. Golang — компилируемый язык: исходный код перед выполнением компилируется здесь в исполняемый двоичный код. Но преобразование это не мгновенное: между компилятором и машинным кодом, очевидно, происходит что-то еще, посреди них вселенная.
Чтобы исследовать этот космос, я написал простую программу и проверил сгенерированный код, использовав библиотеки Golang. А заодно запустил ряд команд.
Установка
Вот эта программа, по ней изучим каждый этап:
package main
func main() {
c := 0
var a = 1
b := add(a, c, 1)
c = b * 2
}
func add(numbers ...int) int {
var result int
for numb := range numbers {
result += numb
}
return result
}
Базовый процесс: принцип работы
Представьте код Go в виде рукописи, написанной на языке, похожем на английский. При вызове команды go build эта рукопись отправляется переводчику — компилятору Go. Им тщательно парсится каждое слово и предложение — лексический и синтаксический анализ, формируется полное представление о написанном — создается АСД. Посредством трансформационных изменений — промежуточное представление и оптимизация — рукопись дорабатывается и переводится на понятный машине язык — ассемблерный и машинный код. Наконец, переведенная рукопись снабжается необходимыми сносками — компоновка, в результате получается книга — исполняемый двоичный код, готовый для считывания компьютером.
- Вот весь процесс превращения исходного кода в машинный:
Исходный код → токены → парсинг → абстрактное синтаксическое дерево АСД → промежуточное представление → ассемблерный код → машинный, двоичный код → компоновка → исполняемый двоичный код.
Вселенная
Исходный код → токены:
Аналогично этот процесс токенизации представляется дешифрованием рукописной записи, составленной на таинственном языке. Запись — исходный код — представляет собой неразборчивые каракули — байты. Переводчик — scanner.go — прогоняет запись по словарю — tokens.go, распознавая каждый символ и объединяя их в значимые слова — токены. В ходе такого преобразования сообщение становится понятным и обрабатывается дальше в процессе компиляции.
tokens.go — это лексикон языка программирования Go, а scanner.go — интерпретатор, которым считывается и классифицируется синтаксис.
- На этом этапе осуществляется лексический анализ.
- Токенизация — процесс преобразования потока байтов в поток токенов. Токены — это фактически перевод группы байтов или синтаксиса языка программирования в слова, распознаваемые компилятором Golang.
IDENT— это токен-идентификатор переменных, типов, функций и т. д- Вот распознанные компилятором Golang слова. tokens.go — это, по сути, словарь Golang.
- A scanner.go — переводчик, которым словарь tokens.go используется.
- Вот маркированный код из файла calc.go:
calc.go:1:1 package "package"
calc.go:1:9 IDENT "main"
calc.go:1:13 ; "\n"
calc.go:3:1 func "func"
calc.go:3:6 IDENT "main"
calc.go:3:10 ( ""
calc.go:3:11 ) ""
calc.go:3:13 { ""
calc.go:4:2 IDENT "c"
calc.go:4:4 := ""
calc.go:4:7 INT "0"
calc.go:4:8 ; "\n"
calc.go:5:2 var "var"
calc.go:5:6 IDENT "a"
calc.go:5:8 = ""
calc.go:5:10 INT "1"
calc.go:5:11 ; "\n"
calc.go:6:2 IDENT "b"
calc.go:6:4 := ""
calc.go:6:7 IDENT "add"
calc.go:6:10 ( ""
calc.go:6:11 IDENT "a"
calc.go:6:12 , ""
calc.go:6:14 IDENT "c"
calc.go:6:15 , ""
calc.go:6:17 INT "1"
calc.go:6:18 ) ""
calc.go:6:19 ; "\n"
calc.go:7:2 IDENT "c"
calc.go:7:4 = ""
calc.go:7:6 IDENT "b"
calc.go:7:8 * ""
calc.go:7:10 INT "2"
calc.go:7:11 ; "\n"
calc.go:8:1 } ""
calc.go:8:2 ; "\n"
calc.go:10:1 func "func"
calc.go:10:6 IDENT "add"
calc.go:10:9 ( ""
calc.go:10:10 IDENT "numbers"
calc.go:10:18 ... ""
calc.go:10:21 IDENT "int"
calc.go:10:24 ) ""
calc.go:10:26 IDENT "int"
calc.go:10:30 { ""
calc.go:11:2 var "var"
calc.go:11:6 IDENT "result"
calc.go:11:13 IDENT "int"
calc.go:11:16 ; "\n"
calc.go:13:2 for "for"
calc.go:13:6 IDENT "numb"
calc.go:13:11 := ""
calc.go:13:14 range "range"
calc.go:13:20 IDENT "numbers"
calc.go:13:28 { ""
calc.go:14:3 IDENT "result"
calc.go:14:10 += ""
calc.go:14:13 IDENT "numb"
calc.go:14:17 ; "\n"
calc.go:15:2 } ""
calc.go:15:3 ; "\n"
calc.go:17:2 return "return"
calc.go:17:9 IDENT "result"
calc.go:17:15 ; "\n"
calc.go:18:1 } ""
calc.go:18:2 ; "\n"
&{<nil> 1 main [0x14000113080 0x14000113170] 1 195 scope 0x14000104570 {
func main
func add
}
Токены → парсинг:
Этап парсинга — это как собирание паззла, фрагменты которого являются токенами, слагаемыми парсингом в картинку — АСД. При таком структурированном представлении компилятор «понимает» программу: если из-за синтаксической ошибки паззл не складывается, проблема парсером выявляется и до перехода к следующему этапу паззл оказывается собранным.
- На этом этапе осуществляется синтаксический анализ.
- В процессе парсинга генерируется предоставляемое далее АСД.
- В процессе синтаксического анализа токены организуются в структурированном виде — абстрактном синтаксическом дереве АСД. Синтаксическим анализатором Go во время парсинга обеспечивается синтаксическая корректность кода. Например, при обнаружении синтаксической ошибки, чтобы обработать проблему и сообщить о ней, в файле
parser.goвызываются функции вродеsyntaxErrorAt.
Синтаксический анализ → абстрактное синтаксическое дерево АСД:
- Результатом проведенного с помощью модуля parser.go синтаксического анализа кода Go является АСД.
- АСД — основа для процесса компиляции семантического анализа. Прежде чем генерировать код, для обеспечения его корректности и согласованности компилятором выполняются проверка типов, разрешение области, другие семантические проверки.
&ast.File{
Doc:(*ast.CommentGroup)(nil),
Package:1,
Name:(*ast.Ident)(0x1400013e000),
Decls:[]ast.Decl{
(*ast.FuncDecl)(0x14000113080),
(*ast.FuncDecl)(0x14000113170)
},
FileStart:1,
FileEnd:195,
Scope:(*ast.Scope)(0x14000104570),
Imports:[]*ast.ImportSpec(nil),
Unresolved:[]*ast.Ident{
(*ast.Ident)(0x1400013e260),
(*ast.Ident)(0x1400013e280),
(*ast.Ident)(0x1400013e2c0)
},
Comments:[]*ast.CommentGroup(nil),
GoVersion:""
}
Абстрактное синтаксическое дерево АСД → промежуточное представление:
Чтобы усовершенствовать процесс приготовления, рецепт преобразуется компилятором-поваром в подробный план приготовления — промежуточное представление, которое «готовится» в Golang с использованием стиля, называемого единственным статическим присваиванием.
Перед «готовкой» рецепты дорабатываются поваром-компилятором при помощи девиртуализации, встраивания вызовов функций, анализа локальности.
На заключительном, прогулочном этапе рецепт поваром пересматривается, инструкции упрощаются, кулинарные техники переводятся в конкретные этапы этой техники.
- Единственное статическое присваивание применяется в Golang как промежуточное представление.
- АСД подвергается преобразованиям, в том числе конструированию промежуточного представления noding, пока не достигнуто единственное статическое присваивание. Noding связан с переводом синтаксиса АСД в форму, пригодную для дальнейшего анализа и оптимизации, в этот процесс также включаются импорт/экспорт пакетов и встраивание.
- Middle End занимается оптимизациями в промежуточном представлении:
1. Девиртуализацией реализуется два этапа оптимизации «девиртуализации»:
1.1. «Статическая» девиртуализация, при которой вызовы методов интерфейса заменяются прямым конкретным типом, когда конкретный тип известен во время компиляции.
1.2. «Профильная» девиртуализация, при которой косвенные вызовы заменяются прямыми вызовами наиболее часто выполняемых конкретных реализаций.
2. Встраиванием вызовов функций определяется, какие функции пригодны для замены вызова функции ее телом.
3. Анализ локальности: анализированием функций здесь определяется, какие переменные Go выделить в стеке, включая неявные выделения вроде вызовов вnewилиmake, составных литералов и т. д. - Walk — это заключительный этап процесса над промежуточным представлением, здесь выделяется две цели:
1. Сложные операторы разбиваются на отдельные и более простые, появляются временные переменные, соблюдается порядок вычисления. Другое название этапа — order.
2. Здесь удаляется синтаксический сахар: высокоуровневые конструкции Go становятся примитивнее. Операторыswitchпреобразуются в двоичный поиск или таблицы переходов, а операции с картами и каналами заменяются вызовами времени выполнения. - Исходное промежуточное представление, генерируемое компилятором перед упомянутыми выше процессами, показывается командой go tool compile -W calc.go.
Обобщенное единственное статическое присваивание:
- Единственное статическое присваивание — это промежуточное представление более низкого уровня со специфическими свойствами, которыми проще реализовывать оптимизации и, в конечном итоге, генерировать из него машинный код.
- Во время этого преобразования применяются внутренние механизмы специальных функций, которые компилятор обучен заменять сильно оптимизированным кодом в каждом конкретном случае.
- Во время преобразования АСД в единственное статическое присваивание некоторые узлы тоже умаляются до компонентов попроще — для работы с ними остальной части компилятора. Например, циклы с диапазоном переписываются в циклы
for. - Затем применяются независимые от процессора проходы и правила. Они не относятся к какой-то одной компьютерной архитектуре, поэтому выполняются на всех вариантах
GOARCH— архитектуры ЦП, на которую нацелены двоичные файлы Go. На этих этапах удаляются мертвый код, ненужные нулевые значения и неиспользуемые ветвления. Обобщенные правила подстановки в основном касаются выражений: замена выражений на константы, оптимизация перемножений и операций с плавающей точкой.
Функция calc:
buildssa-body
. DCL # calc.go:4:2
. . NAME-main.c esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:4:2
. AS Def tc(1) # calc.go:4:4
. . NAME-main.c esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:4:2
. . LITERAL-0 int tc(1) # calc.go:4:7
. DCL # calc.go:5:6
. . NAME-main.a esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:5:6
. AS Def tc(1) # calc.go:5:6
. . NAME-main.a esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:5:6
. . LITERAL-1 int tc(1) # calc.go:5:10
. DCL # calc.go:6:2
. . NAME-main.b esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:6:2
. DCL # calc.go:6:10
. . NAME-main.numbers esc(no) Class:PAUTO Offset:0 InlFormal OnStack Used SLICE-[]int tc(1) # calc.go:6:10 calc.go:10:10
. BLOCK # calc.go:6:10
. BLOCK-List
. . AS tc(1) # calc.go:6:10
. . . NAME-main..autotmp_8 esc(N) Class:PAUTO Offset:0 Addrtaken AutoTemp OnStack Used ARRAY-[3]int tc(1) # calc.go:6:10
. . AS tc(1) # calc.go:6:10
. . . NAME-main..autotmp_7 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used PTR-*[3]int tc(1) # calc.go:6:10
. . . ADDR PTR-*[3]int tc(1) # calc.go:6:10
. . . . NAME-main..autotmp_8 esc(N) Class:PAUTO Offset:0 Addrtaken AutoTemp OnStack Used ARRAY-[3]int tc(1) # calc.go:6:10
. . BLOCK tc(1) # calc.go:6:10
. . BLOCK-List
. . . AS tc(1) # calc.go:6:10
. . . . INDEX Bounded int tc(1) # calc.go:6:10
. . . . . DEREF Implicit ARRAY-[3]int tc(1) # calc.go:6:10
. . . . . . NAME-main..autotmp_7 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used PTR-*[3]int tc(1) # calc.go:6:10
. . . . . LITERAL-0 int tc(1) # calc.go:6:10
. . . . NAME-main.a esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:5:6
. . BLOCK tc(1) # calc.go:6:10
. . BLOCK-List
. . . AS tc(1) # calc.go:6:10
. . . . INDEX Bounded int tc(1) # calc.go:6:10
. . . . . DEREF Implicit ARRAY-[3]int tc(1) # calc.go:6:10
. . . . . . NAME-main..autotmp_7 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used PTR-*[3]int tc(1) # calc.go:6:10
. . . . . LITERAL-1 int tc(1) # calc.go:6:10
. . . . NAME-main.c esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:4:2
. . BLOCK tc(1) # calc.go:6:17
. . BLOCK-List
. . . AS tc(1) # calc.go:6:17
. . . . INDEX Bounded int tc(1) # calc.go:6:10
. . . . . DEREF Implicit ARRAY-[3]int tc(1) # calc.go:6:10
. . . . . . NAME-main..autotmp_7 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used PTR-*[3]int tc(1) # calc.go:6:10
. . . . . LITERAL-2 int tc(1) # calc.go:6:10
. . . . LITERAL-1 int tc(1) # calc.go:6:17
. . BLOCK tc(1) # calc.go:6:17
. . BLOCK-List
. . . AS tc(1) # calc.go:6:17
. . . . NAME-main.numbers esc(no) Class:PAUTO Offset:0 InlFormal OnStack Used SLICE-[]int tc(1) # calc.go:6:10 calc.go:10:10
. . . . SLICEARR SLICE-[]int tc(1) # calc.go:6:17
. . . . . NAME-main..autotmp_7 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used PTR-*[3]int tc(1) # calc.go:6:10
. . BLOCK # calc.go:6:10
. INLMARK # +calc.go:6:10
. DCL # calc.go:6:10 calc.go:11:6
. . NAME-main.result esc(no) Class:PAUTO Offset:0 InlLocal OnStack Used int tc(1) # calc.go:6:10 calc.go:11:6
. AS tc(1) # calc.go:6:10 calc.go:11:6
. . NAME-main.result esc(no) Class:PAUTO Offset:0 InlLocal OnStack Used int tc(1) # calc.go:6:10 calc.go:11:6
. DCL # calc.go:6:10 calc.go:13:6
. . NAME-main.numb esc(no) Class:PAUTO Offset:0 InlLocal OnStack Used int tc(1) # calc.go:6:10 calc.go:13:6
. FOR-init
. . AS tc(1) # calc.go:6:10 calc.go:13:14
. . . NAME-main..autotmp_9 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:6:10 calc.go:13:14
. . AS tc(1) # calc.go:6:10 calc.go:13:14
. . . NAME-main..autotmp_10 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:6:10 calc.go:13:14
. . . LEN int tc(1) # calc.go:6:10 calc.go:13:14
. . . . NAME-main.numbers esc(no) Class:PAUTO Offset:0 InlFormal OnStack Used SLICE-[]int tc(1) # calc.go:6:10 calc.go:10:10
. FOR # calc.go:6:10 calc.go:13:14
. FOR-Cond
. . LT bool tc(1) # calc.go:6:10 calc.go:13:14
. . . NAME-main..autotmp_9 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:6:10 calc.go:13:14
. . . NAME-main..autotmp_10 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:6:10 calc.go:13:14
. FOR-Post
. . AS tc(1) # calc.go:6:10 calc.go:13:14
. . . NAME-main..autotmp_9 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:6:10 calc.go:13:14
. . . ADD int tc(1) # calc.go:6:10 calc.go:13:14
. . . . NAME-main..autotmp_9 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:6:10 calc.go:13:14
. . . . LITERAL-1 int tc(1) # calc.go:6:10 calc.go:13:14
. FOR-Body
. . AS tc(1) # calc.go:6:10 calc.go:13:14
. . . NAME-main.numb esc(no) Class:PAUTO Offset:0 InlLocal OnStack Used int tc(1) # calc.go:6:10 calc.go:13:6
. . . NAME-main..autotmp_9 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:6:10 calc.go:13:14
. . AS tc(1) # calc.go:6:10 calc.go:14:10
. . . NAME-main.result esc(no) Class:PAUTO Offset:0 InlLocal OnStack Used int tc(1) # calc.go:6:10 calc.go:11:6
. . . ADD int tc(1) # calc.go:6:10 calc.go:14:10
. . . . NAME-main.result esc(no) Class:PAUTO Offset:0 InlLocal OnStack Used int tc(1) # calc.go:6:10 calc.go:11:6
. . . . NAME-main.numb esc(no) Class:PAUTO Offset:0 InlLocal OnStack Used int tc(1) # calc.go:6:10 calc.go:13:6
. DCL tc(1) # calc.go:6:10
. . NAME-main.~r0 esc(no) Class:PAUTO Offset:0 InlFormal OnStack int tc(1) # calc.go:6:10 calc.go:10:26
. BLOCK # calc.go:6:10
. BLOCK-List
. . AS tc(1) # calc.go:6:10
. . . NAME-main.~r0 esc(no) Class:PAUTO Offset:0 InlFormal OnStack int tc(1) # calc.go:6:10 calc.go:10:26
. . . NAME-main.result esc(no) Class:PAUTO Offset:0 InlLocal OnStack Used int tc(1) # calc.go:6:10 calc.go:11:6
. GOTO main..i0 tc(1) # calc.go:6:10
. LABEL main..i0 # calc.go:6:10
. AS Def tc(1) # calc.go:6:4
. . NAME-main.b esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:6:2
. . NAME-main.~r0 esc(no) Class:PAUTO Offset:0 InlFormal OnStack int tc(1) # calc.go:6:10 calc.go:10:26
. AS tc(1) # calc.go:7:4
. . NAME-main.c esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:4:2
. . MUL int tc(1) # calc.go:7:8
. . . NAME-main.b esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:6:2
. . . LITERAL-2 int tc(1) # calc.go:7:10
Функция add:
buildssa-body
. DCL # calc.go:11:6
. . NAME-main.result esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:11:6
. AS tc(1) # calc.go:11:6
. . NAME-main.result esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:11:6
. DCL # calc.go:13:6
. . NAME-main.numb esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:13:6
. FOR-init
. . AS tc(1) # calc.go:13:14
. . . NAME-main..autotmp_4 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:13:14
. . AS tc(1) # calc.go:13:14
. . . NAME-main..autotmp_5 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:13:14
. . . LEN int tc(1) # calc.go:13:14
. . . . NAME-main.numbers esc(no) Class:PPARAM Offset:0 OnStack Used SLICE-[]int tc(1) # calc.go:10:10
. FOR # calc.go:13:14
. FOR-Cond
. . LT bool tc(1) # calc.go:13:14
. . . NAME-main..autotmp_4 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:13:14
. . . NAME-main..autotmp_5 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:13:14
. FOR-Post
. . AS tc(1) # calc.go:13:14
. . . NAME-main..autotmp_4 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:13:14
. . . ADD int tc(1) # calc.go:13:14
. . . . NAME-main..autotmp_4 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:13:14
. . . . LITERAL-1 int tc(1) # calc.go:13:14
. FOR-Body
. . AS tc(1) # calc.go:13:14
. . . NAME-main.numb esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:13:6
. . . NAME-main..autotmp_4 esc(N) Class:PAUTO Offset:0 AutoTemp OnStack Used int tc(1) # calc.go:13:14
. . AS tc(1) # calc.go:14:10
. . . NAME-main.result esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:11:6
. . . ADD int tc(1) # calc.go:14:10
. . . . NAME-main.result esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:11:6
. . . . NAME-main.numb esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:13:6
. RETURN tc(1) # calc.go:17:2
. RETURN-Results
. . AS tc(1) # calc.go:17:2
. . . NAME-main.~r0 esc(no) Class:PPARAMOUT Offset:0 OnStack int tc(1) # calc.go:10:26
. . . NAME-main.result esc(no) Class:PAUTO Offset:0 OnStack Used int tc(1) # calc.go:11:6
Обобщенное единственное статическое присваивание → ассемблерный код:
- Из промежуточного представления компилятором Go генерируется ассемблерный код для целевой платформы, такой как x86 или ARM. Ассемблерный код — это удобное для восприятия человека представление инструкций машинного уровня для конкретной архитектуры.
- Ассемблерный код специфичен для архитектуры машины, на которой выполняется программа.
- Этапы, проходимые единственным статическим присваиванием до генерирования зависимого от ЦП ассемблерного кода, показываются командой GOSSAFUNC=add go tool compile calc.go && open ssa.html.
- Вот финальный ассемблерный код, сгенерированный компилятором после всех этапов для функции add:
00000 (10) TEXT main.add(SB), ABIInternal
00001 (10) FUNCDATA $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB)
00002 (10) FUNCDATA $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
00003 (10) FUNCDATA $5, main.add.arginfo1(SB)
00004 (10) FUNCDATA $6, main.add.argliveinfo(SB)
b1
00005 (10) PCDATA $3, $1
v11
00006 (+13) MOVD $0, R0
v10
00007 (13) MOVD $0, R2
b1
00008 (13) JMP 12
v17
00009 (+13) ADD $1, R0, R3
v14
00010 (+14) ADD R0, R2, R2
v16
00011 (13) MOVD R3, R0
v12
00012 (+13) CMP R0, R1
b2
00013 (13) BGT 9
v5
00014 (+17) MOVD R2, R0
b5
00015 (17) RET
00016 (?) END
Ассемблерный код → машинный, двоичный код:
- Дальше ассемблерный код передается ассемблеру, где преобразуется в машинный, двоичный код.
- В первом столбце следующего примера показан адрес памяти команды на машинном языке, во втором — смещение адреса памяти внутри функции, в четвертом — ассемблерный код, соответствующий пример сгенерирован командой GOSSAFUNC=add go tool compile -S calc.go && open ssa.html:
main.add STEXT size=48 args=0x18 locals=0x0 funcid=0x0 align=0x0 leaf
0x0000 00000 (calc.go:10) TEXT main.add(SB), LEAF|NOFRAME|ABIInternal, $0-24
0x0000 00000 (calc.go:10) MOVD R0, main.numbers(FP)
0x0004 00004 (calc.go:10) FUNCDATA $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB)
0x0004 00004 (calc.go:10) FUNCDATA $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
0x0004 00004 (calc.go:10) FUNCDATA $5, main.add.arginfo1(SB)
0x0004 00004 (calc.go:10) FUNCDATA $6, main.add.argliveinfo(SB)
0x0004 00004 (calc.go:10) PCDATA $3, $1
0x0004 00004 (calc.go:13) MOVD ZR, R0
0x0008 00008 (calc.go:13) MOVD ZR, R2
0x000c 00012 (calc.go:13) JMP 28
0x0010 00016 (calc.go:13) ADD $1, R0, R3
0x0014 00020 (calc.go:14) ADD R0, R2, R2
0x0018 00024 (calc.go:13) MOVD R3, R0
0x001c 00028 (calc.go:13) CMP R0, R1
0x0020 00032 (calc.go:13) BGT 16
0x0024 00036 (calc.go:17) MOVD R2, R0
0x0028 00040 (calc.go:17) RET (R30)
- Директивами FUNCDATA и PCDATA в скомпилированный двоичный файл встраиваются метаданные, которые необходимы среде выполнения Go и особенно сборщику мусора для эффективного управления памятью и точного сканирования стека.
- Поэтому сам код функции запускается только на команде ассемблера
MOVD ZR, R0.
Компоновка → исполняемый двоичный код:
- После того как машинный код сгенерирован, скомпилированный код объединяется компоновщиком с другими необходимыми компонентами — средой выполнения Go, библиотеками, зависимостями — в единый исполняемый двоичный код. Полученный двоичный код выполняется в целевой системе.
- Машинный код, сгенерированный после декомпиляции, получается командой go tool objdump -s main.main calc. А конечный исполняемый двоичный код — командой go build -o calc calc.go, во втором столбце показан конечный адрес в памяти команды на машинном языке, а в третьем — машинный код для ассемблерного кода четвертого столбца:
calc.go:6 0x10005c8b0 aa1f03e0 MOVD ZR, R0
calc.go:13 0x10005c8b4 14000002 JMP 2(PC)
calc.go:13 0x10005c8b8 91000400 ADD $1, R0, R0
calc.go:13 0x10005c8bc f1000c1f CMP $3, R0
calc.go:13 0x10005c8c0 54ffffcb BLT -2(PC)
calc.go:8 0x10005c8c4 d65f03c0 RET
calc.go:8 0x10005c8c8 00000000 ?
calc.go:8 0x10005c8cc 00000000 ?
Заключение
Процесс генерирования исполняемого двоичного кода из исходного кода Go — целое приключение, и мы лишь начали понимать глубинные механизмы этого увлекательного космоса.
Стартовали с рукописной записи на таинственном языке — исходный код Golang, деконструировали ее в пазл из токенов — токенизация, называемая также лексическим анализом. Собрав этот пазл, получили изображение кулинарного рецепта АСД с подробным описанием всех необходимых этапов и техник. По рецепту приготовили уникальное блюдо: ассемблерный код, специфичный для каждого типа архитектуры процессора. Из этого ассемблерного кода в ЦП компьютера генерируется вкусный машинный код.
Читайте также:
- Как я создал 2D-игру с помощью Ebiten за 40 минут
- Настройка приложения Go с проблемами сборки мусора
- Принципы SOLID на Go
Читайте нас в Telegram, VK и Дзен
Перевод статьи Fernando Fragateiro: The Cosmic Adventure Of Golang Compiler





