---
id: 191ebec04cf97317ba418a120521d30b
author: Lee Yunjin
title: Анализ структуры времени выполнения управляемых языков и ассемблерного кода AMD64 в бинарных файлах Go
description: Сравнивается модель выполнения управляемых (managed) и неуправляемых (unmanaged) языков, а также с помощью AMD64-ассемблера, извлечённого командой go tool objdump, анализируются проверка стека и структура вызовов функций среды выполнения в бинарных файлах Go.
language: ru
date: 2026-05-17T12:00:00Z
path: /blog/posts/managed-languages-intro-656b7470
category: lowlevel
---

## Что такое управляемый язык?

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

### Структура бинарного файла Go

| .text                | .data              | .gopclntab, .typelink и др. |
| -------------------- | ------------------ | --------------------------- |
| Машинный код для исполнения | Данные для хранения | Секции языкового runtime    |

Поскольку Go не выполняет прямую поэлементную трансляцию в машинный код в том виде, в котором пользователь его написал, логика секции .text тесно связана с секцией языкового runtime.
Кроме того, в ассемблерный код секции .text добавляются функции, которые пользователь не писал, например runtime.printnl().
Благодаря такому автоматическому внедрению кода Go помогает освободить разработчика от ручного управления памятью.

### Рассмотрение только функции main в Go

Сначала создадим простой пример исходного файла main.go и посмотрим на функцию main **на машине AMD64**.

```go
package main

func sayHello(msg string) {
    println(msg)
}

func main() {
    sayHello("Hello World")
}

```

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

```bash
go build main.go

```

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

```bash
go tool objdump -s "main\.main" ./main

```

### Ассемблер

```go
TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
  main.go:7             0x468f60                493b6610                CMPQ SP, 0x10(R14)
  main.go:7             0x468f64                762f                    JBE 0x468f95
  main.go:7             0x468f66                55                      PUSHQ BP
  main.go:7             0x468f67                4889e5                  MOVQ SP, BP
  main.go:7             0x468f6a                4883ec10                SUBQ $0x10, SP
  main.go:8             0x468f6e                90                      NOPL
  main.go:4             0x468f6f                e8cca3fcff              CALL runtime.printlock(SB)
  main.go:4             0x468f74                488d05da290100          LEAQ 0x129da(IP), AX
  main.go:4             0x468f7b                bb0b000000              MOVL $0xb, BX
  main.go:4             0x468f80                e83bacfcff              CALL runtime.printstring(SB)
  main.go:4             0x468f85                e8f6a5fcff              CALL runtime.printnl(SB)
  main.go:4             0x468f8a                e811a4fcff              CALL runtime.printunlock(SB)
  main.go:9             0x468f8f                4883c410                ADDQ $0x10, SP
  main.go:9             0x468f93                5d                      POPQ BP
  main.go:9             0x468f94                c3                      RET
  main.go:7             0x468f95                e8e6afffff              CALL runtime.morestack_noctxt.abi0(SB)
  main.go:7             0x468f9a                ebc4                    JMP main.main(SB)

```

- Сравнивается значение защиты стека во внутреннем управляющем блоке горутины (регистр R14) с текущим указателем стека (SP) с помощью CMPQ; если места для кадра стека недостаточно, выполняется переход (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 автоматически встроил (инлайнинг) две отдельные функции и оптимизировал их. Однако для учебных целей мы запретим инлайнинг функции sayHello.
Для этого скомпилируем исходный код со следующим флагом:

```bash
go build -gcflags="-l" main.go

```

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

```bash
yjlee@elegant:~/compare-assembly/go$ go build -gcflags="-l" main.go

go tool objdump -s "main\.sayHello" ./main
TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
  main.go:3             0x468f60                493b6610               CMPQ SP, 0x10(R14)
  main.go:3             0x468f64                7636                   JBE 0x468f9c
  main.go:3             0x468f66                55                     PUSHQ BP
  main.go:3             0x468f67                4889e5                 MOVQ SP, BP
  main.go:3             0x468f6a                4883ec10               SUBQ $0x10, SP
  main.go:5             0x468f6e                4889442420             MOVQ AX, 0x20(SP)
  main.go:5             0x468f73                48895c2428             MOVQ BX, 0x28(SP)
  main.go:4             0x468f78                e8c3a3fcff             CALL runtime.printlock(SB)
  main.go:4             0x468f7d                488b442420             MOVQ 0x20(SP), AX
  main.go:4             0x468f82                488b5c2428             MOVQ 0x28(SP), BX
  main.go:4             0x468f87                e834acfcff             CALL runtime.printstring(SB)
  main.go:4             0x468f8c                e8efa5fcff             CALL runtime.printnl(SB)
  main.go:4             0x468f91                e80aa4fcff             CALL runtime.printunlock(SB)
  main.go:5             0x468f96                4883c410               ADDQ $0x10, SP
  main.go:5             0x468f9a                5d                     POPQ BP
  main.go:5             0x468f9b                c3                     RET
  main.go:3             0x468f9c                4889442408             MOVQ AX, 0x8(SP)
  main.go:3             0x468fa1                48895c2410             MOVQ BX, 0x10(SP)
  main.go:3             0x468fa6                e8d5afffff             CALL runtime.morestack_noctxt.abi0(SB)
  main.go:3             0x468fab                488b442408             MOVQ 0x8(SP), AX
  main.go:3             0x468fb0                488b5c2410             MOVQ 0x10(SP), BX
  main.go:3             0x468fb5                eba9                   JMP main.sayHello(SB)

```

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

### Следующая часть

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