5. Go assembler и внутренний ассемблерный синтаксис
В предыдущей лекции 4. SSA в Go. Как компилятор оптимизирует код мы подошли к фазе Lowering, когда универсальный математический граф SSA опускается на уровень физического кремния. Конечным продуктом этого этапа компиляции становится код на Go Assembler.
Для инженера, стремящегося к сеньор-грейду и выше, умение бегло читать ассемблерный выхлоп компилятора — это фундаментальный профессиональный навык. Это единственный способ со стопроцентной объективностью доказать: сработал ли инлайнинг функции, устранил ли компилятор проверки выхода за границы срезов (BCE), спас ли Escape Analysis переменную от аллокации в куче и не наплодил ли оптимизатор скрытых задержек в критическом горячем цикле программы (hot path).
Иллюзия нативного ассемблера
Первое, что обескураживает программистов, пришедших из C/C++ и привыкших к классическому ассемблеру Intel или AT&T — ассемблерный код Go выглядит принципиально иначе.
Ассемблер Go — это псевдо-ассемблер. Он унаследовал свой минималистичный и строгий синтаксис из операционной системы Plan 9, которую в Bell Labs создавали отцы Unix и Go — Кен Томпсон, Роб Пайк и Деннис Ритчи. Компилятор Go cmd/compile не формирует сразу текстовый машинный код под конкретный чипсет x86-64 или ARM64. Вместо этого он генерирует промежуточную абстрактную ассемблерную модель, которую затем ассемблер cmd/asm и линковщик cmd/link преобразуют в физические опкоды (opcodes) целевого процессора.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
SSA["Оптимизированный SSA граф"]:::process -->|"Lowering"| GOASM["Go Assembler<br>Абстрактный синтаксис Plan 9"]
GOASM -->|"Ассемблирование (cmd/asm)"| OBJ["Объектный файл (.o)<br>Внутренние опкоды"]
OBJ -->|"Статическая линковка (cmd/link)"| MACHINE["Машинный код CPU<br>01010110... (ELF / Mach-O / PE)"]
Такая элегантная двухуровневая архитектура дала компилятору Go феноменальную портируемость: разработчикам языка не пришлось переписывать заново текстовый парсер и кодогенератор под каждую новую процессорную микроархитектуру (x86, ARM64, RISC-V, MIPS, WebAssembly).
Четыре всадника рантайма: Псевдо-регистры
Для достижения аппаратной независимости Go Assembler вводит четыре псевдо-регистра. В физическом кремнии вашего процессора таких регистров нет; это виртуальные концепции компилятора, которые линковщик транслирует в физические аппаратные регистры (RSP, RBP, RIP, RAX и др.) в зависимости от целевой платформы.
Понимание этих четырех регистров критически необходимо для чтения дампов стека горутин:
SB(Static Base): Указатель на начало глобального виртуального адресного пространства программы. Используется для адресации глобальных переменных, таблиц типов и вызовов функций. Любой вызов функции в Go ассемблере выглядит как обращение к символу со смещением относительноSB, напримерCALL pkg.MyFunc(SB).FP(Frame Pointer): Указатель на аргументы текущей функции. Используется компилятором для безопасного обращения к входящим параметрам и возвращаемым значениям функции, когда они передаются через память стека.SP(Stack Pointer): Указатель на локальный стек текущей функции. Используется для доступа к локальным переменным, размещенным во фрейме.PC(Program Counter): Программный счетчик (аналог регистраRIPв x86-64), указывающий на адрес следующей исполняемой инструкции. В прикладном коде почти никогда не модифицируется вручную.
Синтаксис и суффиксы инструкций
В отличие от синтаксиса Intel (ОПЕРАЦИЯ ПРИЕМНИК, ИСТОЧНИК), ассемблер Plan 9 следует правилу порядка AT&T: ОПЕРАЦИЯ ИСТОЧНИК, ПРИЕМНИК.
Размер операндов задается строгим буквенным суффиксом в названии инструкции:
MOVB(Byte) — 8 бит (1 байт);MOVW(Word) — 16 бит (2 байта);MOVL(Long) — 32 бита (4 байта);MOVQ(Quadword) — 64 бита (8 байт).
Пример: инструкция MOVQ $10, AX буквально означает: «записать 64-битную константу 10 в аппаратный регистр AX». Знак доллара $ всегда обозначает непосредственное числовое значение (литерал).
Анатомия ассемблерной функции
Рассмотрим простейшую функцию сложения двух чисел и исследуем ее ассемблерный листинг. Для этого выполним команду дизассемблирования:
go tool compile -S main.go
// main.go
package main
func Add(a, b int) int {
return a + b
}
Выхлоп компилятора (упрощенный для наглядности):
TEXT "".Add(SB), NOSPLIT|ABIInternal, $0-24
FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
MOVQ "".a+8(SP), AX
MOVQ "".b+16(SP), CX
ADDQ CX, AX
MOVQ AX, "".~r2+24(SP)
RET
Разберем заглавную директиву по частям: TEXT "".Add(SB), NOSPLIT|ABIInternal, $0-24
TEXT: Ассемблерная директива, объявляющая новую функцию как исполняемый сегмент памяти (Text segment)."".Add(SB): Полное квалифицированное имя функции. Пустые кавычки""заменяются компилятором на имя текущего пакета (main). Метка(SB)указывает, что адрес функции привязан к глобальной базеStatic Base.NOSPLIT: Флаг оптимизации рантайма. Он приказывает компилятору: «Не вставляй пролог проверки переполнения стека (stack check)». Это допустимо только для сверхмалых функций, фрейм которых гарантированно помещается в защитную зону стека горутины, что экономит несколько драгоценных тактов процессора на вызове (подробнее см. 11. Стек горутины. Рост и shrink стека).ABIInternal: Флаг, сообщающий, какая конвенция вызовов используется функцией (современный регистровый ABI).$0-24: Фрейм-дескриптор функции:$0— размер локального стекового фрейма для внутренних переменных в байтах (у нашей функции нет локальных переменных, поэтому 0);24— суммарный размер аргументов и возвращаемых значений: аргументa(8 байт int) + аргументb(8 байт int) + возвращаемый результатint(8 байт) = 24 байта.
Последующие строки демонстрируют логику работы инструкций:
MOVQ "".a+8(SP), AX— загрузка первого аргументаaиз памяти стека в регистрAX;MOVQ "".b+16(SP), CX— загрузка второго аргументаbв регистрCX;ADDQ CX, AX— сложение содержимого регистровCXиAXс сохранением суммы вAX;MOVQ AX, "".~r2+24(SP)— сохранение результата из регистраAXв область возвращаемого значения в памяти;RET— возврат управления вызывающей функции.
Mechanical Sympathy. Стоит ли писать на Asm вручную?
В исходном коде стандартной библиотеки Go (пакеты crypto, math/bits, runtime, sync/atomic) вы обнаружите тысячи строк рукописного ассемблера в файлах с расширением .s. Это делается ради инструкций, недоступных компилятору напрямую: аппаратного ускорения шифрования AES-NI, векторных вычислений AVX-512 или тонких атомарных барьеров памяти.
Однако писать бизнес-логику или прикладные сервисы на чистом ассемблере в 99.9% случаев категорически противопоказано.
Итог
- Ассемблер Go — это платформонезависимый псевдо-ассемблер, унаследованный из лаборатории Bell Labs и ОС Plan 9, использующий синтаксис AT&T (источник, затем приемник).
- Он опирается на 4 виртуальных псевдо-регистра (
SB,FP,SP,PC), позволяющих компилятору и линковщику абстрагироваться от деталей аппаратных платформ. - Анализ ассемблера (
go tool compile -S) — ключевой инструмент для подтверждения инлайнинга, анализа побега переменных и проверки устранения проверок границ срезов (BCE). - Ручные ассемблерные вставки запрещают компилятору инлайнить функции и усложняют построение карт указателей для GC.
В приведенном выше примере ассемблерной функции аргументы передавались через стековую память (SP), что исторически использовалось в Go. Однако начиная с Go 1.17 произошла тектоническая революция: переход на регистровую конвенцию вызовов ABIInternal.
В следующей лекции мы детально разберем: 6. ABIInternal и ABI0. Как Go вызывает функции.