GoSuda

Что такое Managed Language?

By Lee Yunjin
views ...

Что такое управляемый язык (managed language)?

Управляемый язык — это язык, который, в отличие от неуправляемого языка (языка, который просто выполняет логику, написанную программистом, не выходя за её рамки), осуществляет выполнение GC, оптимизацию времени выполнения (runtime optimization), использование green threads, обработку параллелизма и другие задачи на уровне среды выполнения, тем самым избавляя пользователя от необходимости заниматься рискованным низкоуровневым управлением. У таких языков есть преимущество: можно сосредоточиться исключительно на бизнес-логике, полностью погрузившись в разработку. Однако, с другой стороны, программа может вести себя не так, как предполагает интуиция программиста, что иногда требует тонкой настройки runtime. Прежде всего, мы рассмотрим язык Go, который среди управляемых языков наиболее верен философии минимализма, а его ассемблерный код отличается прямотой.

Бинарная структура языка Go

.text.data.gopclntab, .typelink и др.
Исполняемый машинный кодХранимые данныеСекции runtime языка
Поскольку язык Go не переводит код в машинные инструкции в соотношении 1:1 согласно вводу пользователя, логика в секции .text тесно связана с секциями runtime языка.
Кроме того, в ассемблерный код секции .text добавляются функции, такие как runtime.printnl(), которые пользователь явно не прописывал.
Благодаря такому автоматическому внедрению кода язык Go помогает разработчику избежать ручного управления.

Анализ только функции main в Go

Для начала напишем простой пример исходного кода main.go и рассмотрим его начиная с main на машине AMD64.

 1package main
 2
 3func sayHello(msg string) {
 4    println(msg)
 5}
 6
 7func main() {
 8    sayHello("Hello World")
 9}
10

Затем выполним сборку следующим образом:

1go build main.go
2

Для упрощения низкоуровневой отладки Go поддерживает go tool. Чтобы увидеть только ассемблерный код функции main в главном пакете через go tool, введите следующую команду:

1go tool objdump -s "main\.main" ./main
2

Ассемблер

 1TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
 2  main.go:7             0x468f60                493b6610                CMPQ SP, 0x10(R14)
 3  main.go:7             0x468f64                762f                    JBE 0x468f95
 4  main.go:7             0x468f66                55                      PUSHQ BP
 5  main.go:7             0x468f67                4889e5                  MOVQ SP, BP
 6  main.go:7             0x468f6a                4883ec10                SUBQ $0x10, SP
 7  main.go:8             0x468f6e                90                      NOPL
 8  main.go:4             0x468f6f                e8cca3fcff              CALL runtime.printlock(SB)
 9  main.go:4             0x468f74                488d05da290100          LEAQ 0x129da(IP), AX
10  main.go:4             0x468f7b                bb0b000000              MOVL $0xb, BX
11  main.go:4             0x468f80                e83bacfcff              CALL runtime.printstring(SB)
12  main.go:4             0x468f85                e8f6a5fcff              CALL runtime.printnl(SB)
13  main.go:4             0x468f8a                e811a4fcff              CALL runtime.printunlock(SB)
14  main.go:9             0x468f8f                4883c410                ADDQ $0x10, SP
15  main.go:9             0x468f93                5d                      POPQ BP
16  main.go:9             0x468f94                c3                      RET
17  main.go:7             0x468f95                e8e6afffff              CALL runtime.morestack_noctxt.abi0(SB)
18  main.go:7             0x468f9a                ebc4                    JMP main.main(SB)
19
  • С помощью инструкции CMPQ текущий указатель стека (SP) сравнивается со значением стекового стража (stack guard) внутри регистра блока управления горутиной (R14), чтобы проверить, достаточно ли места в текущем стековом фрейме горутины; если места недостаточно, выполняется переход (JBE) по адресу 0x468f95, который является точкой входа для расширения стека.
  • Предыдущий базовый указатель сохраняется в стеке с помощью PUSHQ BP.
  • Текущий указатель стека (SP) копируется в регистр базового указателя (BP), фиксируя отправную точку стека в начале функции.
  • Затем выделяется 16 байт стекового пространства для локальных переменных (SUBQ $0x10, SP), а NOPL используется для заполнения виртуальными инструкциями с целью выравнивания кэша процессора.
  • Для синхронизации стандартного вывода строк внутри runtime Go вызывается runtime.printlock(SB), который устанавливает блокировку.
  • С помощью инструкции LEAQ начальный адрес строки, выделенной как константа ("Hello World"), сохраняется в регистре AX, который согласно спецификации Go ABI используется как первый параметр среди регистров общего назначения.
  • Затем значение, представляющее длину строки, сохраняется во втором регистре параметров BX (MOVL $0xb, BX, то есть 11 в десятичной системе).
  • Вызывается runtime.printstring(SB), который выполняет вывод в консоль на основе переданных данных: AX (адрес данных) и BX (длина).
  • Для обработки перехода на новую строку вызывается runtime.printnl(SB).
  • Поскольку вывод завершен, блокировка снимается через runtime.printunlock(SB).
  • С помощью ADDQ $0x10, SP восстанавливается выделенная ранее стековая память объемом 16 байт.
  • С помощью POPQ BP восстанавливается исходный базовый указатель.
  • Через RET управление возвращается в точку вызова функции.
  • Если при первоначальной проверке стека места оказалось недостаточно, вызывается runtime.morestack_noctxt.abi0(SB) по адресу 0x468f95, динамически расширяя стек среды выполнения, как и полагается управляемому языку.
  • После завершения расширения стека выполняется возврат (JMP) к точке входа main.main(SB). Как можно заметить, ассемблерный код бизнес-логики достаточно ясен и представляет собой логику, дополненную лишь минимальным управлением со стороны runtime.

При отсутствии оптимизации

Представленный выше вид — это результат автоматической оптимизации компилятором Go путем инлайнинга (inlining) двух отдельных функций. Однако для учебных целей мы сделаем так, чтобы в данном случае sayHello не подвергалась инлайнингу. Для этого скомпилируем исходный код с использованием следующего флага:

1go build -gcflags="-l" main.go
2

Если вывести результат в оболочке, можно обнаружить дублирующийся ассемблерный код.

 1yjlee@elegant:~/compare-assembly/go$ go build -gcflags="-l" main.go
 2
 3go tool objdump -s "main\.sayHello" ./main
 4TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
 5  main.go:3             0x468f60                493b6610               CMPQ SP, 0x10(R14)
 6  main.go:3             0x468f64                7636                   JBE 0x468f9c
 7  main.go:3             0x468f66                55                     PUSHQ BP
 8  main.go:3             0x468f67                4889e5                 MOVQ SP, BP
 9  main.go:3             0x468f6a                4883ec10               SUBQ $0x10, SP
10  main.go:5             0x468f6e                4889442420             MOVQ AX, 0x20(SP)
11  main.go:5             0x468f73                48895c2428             MOVQ BX, 0x28(SP)
12  main.go:4             0x468f78                e8c3a3fcff             CALL runtime.printlock(SB)
13  main.go:4             0x468f7d                488b442420             MOVQ 0x20(SP), AX
14  main.go:4             0x468f82                488b5c2428             MOVQ 0x28(SP), BX
15  main.go:4             0x468f87                e834acfcff             CALL runtime.printstring(SB)
16  main.go:4             0x468f8c                e8efa5fcff             CALL runtime.printnl(SB)
17  main.go:4             0x468f91                e80aa4fcff             CALL runtime.printunlock(SB)
18  main.go:5             0x468f96                4883c410               ADDQ $0x10, SP
19  main.go:5             0x468f9a                5d                     POPQ BP
20  main.go:5             0x468f9b                c3                     RET
21  main.go:3             0x468f9c                4889442408             MOVQ AX, 0x8(SP)
22  main.go:3             0x468fa1                48895c2410             MOVQ BX, 0x10(SP)
23  main.go:3             0x468fa6                e8d5afffff             CALL runtime.morestack_noctxt.abi0(SB)
24  main.go:3             0x468fab                488b442408             MOVQ 0x8(SP), AX
25  main.go:3             0x468fb0                488b5c2410             MOVQ 0x10(SP), BX
26  main.go:3             0x468fb5                eba9                   JMP main.sayHello(SB)
27

При отключении инлайнинга вставляются операции MOVQ, которые повторно загружают значения в смещения указателя стека, такие как 0x20(SP), для сохранения параметров (AX, BX) в соответствии со спецификацией вызова функций. Таким образом, подтверждено, что целью оптимизации компилятора являются именно такие избыточные операции перемещения данных в памяти и накладные расходы на вызовы.

В следующий раз

В следующий раз мы рассмотрим операторы if и switch в языке Go. Если будет время, планируется также проанализировать секции runtime языка Go.