Краткое руководство по ассемблеру Go
Краткое руководство по ассемблеру Go
Данный документ представляет собой краткий обзор необычной формы языка ассемблера, используемого компилятором gc Go.
Документ не является полным.
Ассемблер основан на стиле ввода ассемблеров Plan 9, который подробно описан здесь. Если вы планируете писать ассемблерный код, рекомендуется ознакомиться с этим документом, хотя большая часть информации относится исключительно к Plan 9. Настоящий документ предоставляет краткое описание синтаксиса и различий по сравнению с описанием в указанном выше документе, а также описывает особенности, применяемые при написании ассемблерного кода для взаимодействия с Go.
Самое важное, что нужно знать об ассемблере Go, это то, что он не является прямым представлением базовой машины.
Некоторые детали точно соответствуют машине, но некоторые — нет.
Это связано с тем, что компилятор (см.
это описание)
не требует ассемблерного прохода в обычной цепочке компиляции.
Вместо этого компилятор работает с полуабстрактным набором инструкций,
и выбор инструкций происходит частично после генерации кода.
Ассемблер работает с полуабстрактной формой, поэтому
когда вы видите инструкцию вроде MOV,
то то, что на самом деле генерирует инструментарий для этой операции,
может не быть инструкцией перемещения вообще, возможно, это очистка или загрузка.
Или же она может точно соответствовать машинной инструкции с таким же названием.
В общем, машинно-специфичные операции чаще всего отображаются как есть, тогда как более общие понятия, такие как
перемещение памяти, вызов и возврат подпрограммы, абстрагируются.
Детали зависят от архитектуры, и мы приносим извинения за неточность; ситуация не очень чётко определена.
Программа ассемблера — это способ разбора описания этого
полуабстрактного набора инструкций и преобразования его в инструкции для
связывания. Если вы хотите увидеть, как выглядят инструкции в ассемблере для заданной архитектуры, например amd64,
примеры можно найти во многих местах стандартной библиотеки, в пакетах таких как
runtime и
math/big.
Также можно просмотреть, какой ассемблерный код генерирует компилятор
(фактический вывод может отличаться от того, что вы видите здесь):
$ cat x.go
package main
func main() {
println(3)
}
$ GOOS=linux GOARCH=amd64 go tool compile -S x.go # или: go build -gcflags -S x.go
"".main STEXT size=74 args=0x0 locals=0x10
0x0000 00000 (x.go:3) TEXT "".main(SB), $16-0
0x0000 00000 (x.go:3) MOVQ (TLS), CX
0x0009 00009 (x.go:3) CMPQ SP, 16(CX)
0x000d 00013 (x.go:3) JLS 67
0x000f 00015 (x.go:3) SUBQ $16, SP
0x0013 00019 (x.go:3) MOVQ BP, 8(SP)
0x0018 00024 (x.go:3) LEAQ 8(SP), BP
0x001d 00029 (x.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (x.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (x.go:3) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (x.go:4) PCDATA $0, $0
0x001d 00029 (x.go:4) PCDATA $1, $0
0x001d 00029 (x.go:4) CALL runtime.printlock(SB)
0x0022 00034 (x.go:4) MOVQ $3, (SP)
0x002a 00042 (x.go:4) CALL runtime.printint(SB)
0x002f 00047 (x.go:4) CALL runtime.printnl(SB)
0x0034 00052 (x.go:4) CALL runtime.printunlock(SB)
0x0039 00057 (x.go:5) MOVQ 0x8(SP), BP
0x003e 00062 (x.go:5) ADDQ $0x10, SP
0x0042 00066 (x.go:5) RET
0x0047 00071 (x.go:3) CALL runtime.morestack_noctxt(SB)
0x004c 00076 (x.go:3) JMP main.main(SB)
Чтобы увидеть, что попадает в исполняемый файл после связывания, используйте go tool objdump:
$ go build -o x.exe x.go $ go tool objdump -s main.main x.exe TEXT main.main(SB) /tmp/x.go x.go:3 0x10501c0 65488b0c2530000000 MOVQ GS:0x30, CX x.go:3 0x10501c9 483b6110 CMPQ 0x10(CX), SP x.go:3 0x10501cd 7634 JBE 0x1050203 x.go:3 0x10501cf 4883ec10 SUBQ $0x10, SP x.go:3 0x10501d3 48896c2408 MOVQ BP, 0x8(SP) x.go:3 0x10501d8 488d6c2408 LEAQ 0x8(SP), BP x.go:4 0x10501dd e86e45fdff CALL runtime.printlock(SB) x.go:4 0x10501e2 48c7042403000000 MOVQ $0x3, 0(SP) x.go:4 0x10501ea e8e14cfdff CALL runtime.printint(SB) x.go:4 0x10501ef e8ec47fdff CALL runtime.printnl(SB) x.go:4 0x10501f4 e8d745fdff CALL runtime.printunlock(SB) x.go:5 0x10501f9 488b6c2408 MOVQ 0x8(SP), BP x.go:5 0x10501fe 4883c410 ADDQ $0x10, SP x.go:5 0x1050202 c3 RET x.go:3 0x1050203 e83882ffff CALL runtime.morestack_noctxt(SB) x.go:3 0x1050208 ebb6 JMP main.main(SB)
Константы
Хотя ассемблер получает указания от ассемблеров Plan 9, это отдельная программа, поэтому существуют некоторые различия.
Одно из них касается оценки констант.
Константные выражения в ассемблере анализируются с использованием приоритета операторов Go, а не C-подобного приоритета оригинала.
Таким образом, 3&1<<2 равно 4, а не 0 — это интерпретируется как (3&1)<<2, а не 3&(1<<2).
Кроме того, константы всегда вычисляются как 64-битные беззнаковые целые числа.
Следовательно, -2 — это не целочисленное значение минус два,
а беззнаковое 64-битное целое число с тем же битовым шаблоном.
Различие редко имеет значение, но
для избежания неоднозначности деление или сдвиг вправо, когда старший бит операнда справа установлен, отклоняется.
Символы
Некоторые символы, такие как R1 или LR,
предопределены и ссылаются на регистры.
Точный набор зависит от архитектуры.
Существует четыре предопределённых символа, которые ссылаются на псевдорегистры. Эти не являются настоящими регистрами, а представляют собой виртуальные регистры, поддерживаемые инструментарием, такие как указатель кадра. Набор псевдорегистров одинаков для всех архитектур:
-
FP: Указатель кадра: аргументы и локальные переменные. -
PC: Счётчик программ: переходы и ветвления. -
SB: Статический базовый указатель: глобальные символы. -
SP: Указатель стека: максимальный адрес внутри локального кадра стека.
Все пользовательские символы записываются как смещения относительно псевдорегистров
FP (аргументы и локальные переменные) и SB (глобальные).
Псевдорегистр SB можно рассматривать как начало памяти, поэтому символ foo(SB)
означает имя foo как адрес в памяти.
Этот формат используется для именования глобальных функций и данных.
Добавление <> к имени, как в foo<>(SB), делает имя
видимым только в текущем исходном файле, подобно объявлению уровня верхнего уровня static в C-файле.
Добавление смещения к имени ссылается на это смещение от адреса символа, поэтому
foo+4(SB) — это четыре байта после начала foo.
Псевдорегистр FP является виртуальным указателем кадра,
используемым для ссылки на аргументы функции.
Компиляторы поддерживают виртуальный указатель кадра и обращаются к аргументам на стеке как к смещениям от этого псевдорегистра.
Таким образом, 0(FP) — это первый аргумент функции,
8(FP) — второй (на 64-битной машине), и так далее.
Однако при ссылке на аргумент функции таким образом необходимо указывать имя
в начале, как в first_arg+0(FP) и second_arg+8(FP).
(Значение смещения — смещение от указателя кадра — отличается
от его использования с SB, где оно представляет собой смещение от символа.)
Ассемблер принудительно применяет это соглашение, отклоняя простые 0(FP) и 8(FP).
Фактическое имя семантически несущественно, но должно использоваться для документирования
имени аргумента.
Следует подчеркнуть, что FP всегда является
псевдорегистром, а не аппаратным
регистром, даже на архитектурах с аппаратным указателем кадра.
Для ассемблерных функций с прототипами Go, go vet проверит, что имена аргументов
и смещения совпадают.
На 32-битных системах младшие и старшие 32 бита 64-битного значения различаются добавлением
суффикса _lo или _hi к имени, как в arg_lo+0(FP) или arg_hi+4(FP).
Если прототип Go не называет результат, ожидаемое имя ассемблера — ret.
Псевдорегистр SP является виртуальным указателем стека,
используемым для ссылки на локальные переменные кадра и аргументы, подготовленные
для вызова функции.
Он указывает на максимальный адрес внутри локального кадра стека, поэтому ссылки должны использовать отрицательные смещения
в диапазоне [−размер_кадра, 0):
x-8(SP), y-4(SP), и так далее.
На архитектурах, где имеется аппаратный регистр с именем SP,
префикс имени используется для различения ссылок на виртуальный указатель стека
от ссылок на архитектурный регистр SP.
То есть, x-8(SP) и -8(SP) обозначают разные адреса памяти:
первый ссылается на псевдорегистр виртуального указателя стека,
в то время как второй ссылается на аппаратный регистр SP.
На машинах, где SP и PC традиционно являются псевдонимами физического пронумерованного регистра,
в ассемблере Go имена SP и PC по-прежнему обрабатываются особым образом;
например, ссылки на SP требуют символ, подобно FP.
Для доступа к реальному аппаратному регистру следует использовать истинное имя R.
Например, на архитектуре ARM аппаратные SP и PC доступны как R13 и R15.
Переходы и прямые jmp-инструкции всегда записываются как смещения относительно PC или как переходы к меткам:
label: MOVW $0, R1 JMP label
Каждая метка видна только внутри функции, в которой она определена.
Поэтому разрешено, чтобы несколько функций в файле определяли и использовали одинаковые имена меток.
Прямые jmp-инструкции и вызовы могут использовать текстовые символы,
такие как name(SB), но не смещения от символов,
такие как name+4(SB).
Инструкции, регистры и директивы ассемблера всегда пишутся заглавными буквами, чтобы напомнить,
что программирование на ассемблере — это трудный процесс.
(Исключение: переименование регистра g на ARM.)
В объектных файлах и двоичных файлах Go полное имя символа состоит из пути пакета,
за которым следует точка и имя символа:
fmt.Printf или math/rand.Int.
Поскольку парсер ассемблера рассматривает точку и косую черту как знаки препинания,
эти строки не могут быть использованы напрямую как идентификаторы.
Вместо этого ассемблер позволяет использовать среднюю точку U+00B7 и косую черту деления U+2215 в идентификаторах,
и заменяет их обычной точкой и косой чертой.
Внутри исходного файла ассемблера символы выше записываются как fmt·Printf и math∕rand·Int.
Списки ассемблера, генерируемые компиляторами при использовании флага -S,
показывают точку и косую черту напрямую, а не заменяющие их символы Юникода,
требуемые ассемблером.
Большинство ручных ассемблерных файлов не включают полный путь пакета в именах символов,
потому что компоновщик вставляет путь пакета текущего объектного файла в начало любого имени, начинающегося с точки:
в ассемблерном исходном файле внутри реализации пакета math/rand,
функция Int пакета может быть указана как ·Int.
Эта конвенция избегает необходимости жестко кодировать путь импорта пакета в его собственный исходный код,
что облегчает перемещение кода из одного места в другое.
Директивы
Ассемблер использует различные директивы для связывания текста и данных с именами символов.
Например, ниже приведена простая полная функция. Директива TEXT
объявляет символ runtime·profileloop, а последующие инструкции формируют тело функции.
Последняя инструкция в блоке TEXT должна быть каким-либо видом перехода, обычно RET (псевдо-)инструкцией.
(Если это не так, компоновщик добавит инструкцию перехода к себе же; в TEXT нет fallthrough.)
После символа аргументы представляют собой флаги (см. ниже)
и размер кадра — константу (но см. ниже):
TEXT runtime·profileloop(SB),NOSPLIT,$8 MOVQ $runtime·profileloop1(SB), CX MOVQ CX, 0(SP) CALL runtime·externalthreadhandler(SB) RET
В общем случае размер кадра следует за размером аргумента, разделённым минусом.
(Это не вычитание, а специфическая синтаксическая конструкция.)
Размер кадра $24-8 означает, что функция имеет 24-байтовый кадр
и вызывается с 8 байтами аргументов, которые находятся в кадре вызывающей функции.
Если NOSPLIT не указан для TEXT,
необходимо указать размер аргумента.
Для ассемблерных функций с прототипами Go, go vet проверит,
что размер аргумента корректен.
Следует отметить, что имя символа использует среднюю точку для разделения компонентов и указывается как смещение от псевдо-регистра статической базы SB.
Эта функция будет вызываться из исходного кода на Go в пакете runtime с использованием простого имени profileloop.
Глобальные данные описываются последовательностью инициализирующих директив DATA, за которыми следует директива GLOBL.
Каждая директива DATA инициализирует часть соответствующей памяти.
Память, которая не была явно инициализирована, заполняется нулями.
Общий вид директивы DATA следующий:
DATA symbol+offset(SB)/width, value
которая инициализирует память символа по указанному смещению и ширине заданным значением.
Директивы DATA для одного и того же символа должны быть записаны с возрастающими смещениями.
Директива GLOBL объявляет символ как глобальный.
Аргументы являются необязательными флагами и размером данных, объявленных как глобальные,
которые будут иметь начальное значение, равное нулю, если только директива DATA не инициализировала их.
Директива GLOBL должна следовать за соответствующими директивами DATA.
Например,
DATA divtab<>+0x00(SB)/4, $0xf4f8fcff DATA divtab<>+0x04(SB)/4, $0xe6eaedf0 ... DATA divtab<>+0x3c(SB)/4, $0x81828384 GLOBL divtab<>(SB), RODATA, $64 GLOBL runtime·tlsoffset(SB), NOPTR, $4
объявляет и инициализирует divtab<>, read-only таблицу из 64 байт, содержащую 4-байтные целочисленные значения,
а также объявляет runtime·tlsoffset, 4-байтовую неинициализированную переменную, которая не содержит указателей.
Директивы могут иметь один или два аргумента.
Если аргументов два, первый представляет собой маску флагов,
которые могут быть записаны как числовые выражения, сложенные или объединенные с помощью оператора OR,
или могут быть заданы символически для лучшего восприятия человеком.
Их значения, определенные в стандартном файле заголовка #include textflag.h, следующие:
-
NOPROF= 1
(Для элементовTEXT.) Не профилировать помеченную функцию. Этот флаг устарел. -
DUPOK= 2
Разрешено иметь несколько экземпляров этого символа в одном двоичном файле. Связующий модуль выберет один из дубликатов для использования. -
NOSPLIT= 4
(Для элементовTEXT.) Не вставлять преамбулу для проверки необходимости разделения стека. Размер кадра процедуры, а также вызываемых ею функций, должен уместиться в оставшемся пространстве текущего сегмента стека. Используется для защиты процедур, таких как сам код разделения стека. -
RODATA= 8
(Для элементовDATAиGLOBL.) Разместить данные в секции только для чтения. -
NOPTR= 16
(Для элементовDATAиGLOBL.) Эти данные не содержат указателей и поэтому не требуют сканирования сборщиком мусора. -
WRAPPER= 32
(Для элементовTEXT.) Это обёртка функции, и она не должна считаться как отключениеrecover. -
NEEDCTXT= 64
(Для элементовTEXT.) Эта функция является замыканием, поэтому она использует входной регистр контекста. -
LOCAL= 128
Этот символ локален для динамической разделяемой библиотеки. -
TLSBSS= 256
(Для элементовDATAиGLOBL.) Разместить данные в локальном для потока хранилище. -
NOFRAME= 512
(Для элементовTEXT.) Не вставлять инструкции для выделения кадра стека и сохранения/восстановления адреса возврата, даже если это не листовая функция. Допустимо только для функций, которые объявляют размер кадра равным 0. -
TOPFRAME= 2048
(Для элементовTEXT.) Функция является внешним кадром стека вызовов. Отслеживание трассировки должно остановиться на этой функции.
Специальные инструкции
Псевдо-инструкция PCALIGN используется для указания, что следующая инструкция должна быть выровнена
по заданной границе путем заполнения инструкциями без операций.
В настоящее время поддержка осуществляется на arm64, amd64, ppc64, loong64 и riscv64.
Например, начало инструкции MOVD ниже выравнивается по 32 байтам:
PCALIGN $32 MOVD $2, R0
Взаимодействие с типами и константами Go
Если в пакете есть файлы с расширением .s, то go build направит компилятор
создать специальный заголовочный файл под названием go_asm.h,
который затем могут включать файлы с расширением .s с помощью #include.
Этот файл содержит символические константы #define для смещений полей структур Go,
размеров типов структур Go и большинства объявлений const, определённых в текущем пакете.
Код на ассемблере Go должен избегать предположений о макете типов Go
и вместо этого использовать эти константы.
Это повышает читаемость ассемблерного кода и делает его устойчивым к изменениям
макета данных как в определениях типов Go, так и в правилах макетирования, используемых компилятором Go.
Константы имеют вид const_name.
Например, при объявлении Go const bufSize = 1024,
ассемблерный код может ссылаться на значение этой константы как на const_bufSize.
Смещения полей имеют вид type_field.
Размеры структур имеют вид type__size.
Например, рассмотрим следующее определение Go:
type reader struct {
buf [bufSize]byte
r int
}
Ассемблерный код может ссылаться на размер этой структуры
как на reader__size, а смещения двух полей
как на reader_buf и reader_r.
Следовательно, если регистр R1 содержит указатель на
reader, ассемблерный код может ссылаться на поле r
как на reader_r(R1).
Если какие-либо из этих имён #define являются неоднозначными (например,
структура с полем _size), то #include "go_asm.h"
завершится с ошибкой "redefinition of macro".
Координация среды выполнения
Для корректной работы сборщика мусора среда выполнения должна знать местоположение указателей во всех глобальных данных и в большинстве кадров стека. Компилятор Go передаёт эту информацию при компиляции исходных файлов Go, но ассемблерные программы должны определять её явно.
Символ данных, помеченный флагом NOPTR (см. выше),
считается содержащим указатели на данные, выделенные в среде выполнения.
Символ данных с флагом RODATA
выделяется в памяти только для чтения и, следовательно, считается
неявно помеченным NOPTR.
Символ данных с общим размером меньше размера указателя
также считается неявно помеченным NOPTR.
Невозможно определить символ, содержащий указатели, в исходном файле ассемблера;
такой символ должен быть определён в исходном файле Go.
Ассемблерный исходный файл всё ещё может ссылаться на символ по имени,
даже без директив DATA и GLOBL.
Хорошим общим правилом является определение всех символов, кроме RODATA,
в Go, а не в ассемблере.
Каждая функция также требует аннотаций, указывающих местоположение
живых указателей в её аргументах, результатах и локальном кадре стека.
Для ассемблерной функции без результатов, содержащих указатели,
и либо без локального кадра стека, либо без вызовов функций,
единственным требованием является определение прототипа Go для функции
в исходном файле Go в том же пакете. Имя ассемблерной функции
не должно содержать компонент имени пакета (например,
функция Syscall в пакете syscall должна использовать имя
·Syscall вместо эквивалентного имени syscall·Syscall
в её директиве TEXT).
Для более сложных ситуаций требуется явная аннотация.
Эти аннотации используют псевдо-инструкции, определённые в стандартном
файле #include funcdata.h.
Если у функции нет аргументов и нет результатов,
информация об указателях может быть опущена.
Это обозначается аннотацией размера аргументов $n-0
в инструкции TEXT.
В противном случае информация об указателях должна быть предоставлена
с помощью прототипа Go для функции в исходном файле Go,
даже для ассемблерных функций, которые не вызываются напрямую из Go.
(Прототип также позволит go vet проверить ссылки на аргументы.)
В начале функции предполагается, что аргументы инициализированы,
а результаты — не инициализированы.
Если результаты будут содержать живые указатели во время инструкции вызова,
функция должна начинаться с обнуления результатов, а затем
выполнять псевдо-инструкцию GO_RESULTS_INITIALIZED.
Эта инструкция записывает, что результаты теперь инициализированы
и должны быть просканированы при перемещении стека и сборке мусора.
Обычно проще организовать так, чтобы ассемблерные функции не
возвращали указатели или не содержали инструкций вызова;
ни одна функция в стандартной библиотеке не использует
GO_RESULTS_INITIALIZED.
Если у функции нет локального стекового фрейма, информацию об указателях можно опустить.
Это обозначается аннотацией размера локального фрейма $0-n
в инструкции TEXT.
Информация об указателях также может быть опущена, если функция не содержит инструкций вызова.
В противном случае, локальный стековый фрейм не должен содержать указателей,
и ассемблерный код должен подтвердить это факт, выполнив псевдо-инструкцию NO_LOCAL_POINTERS.
Поскольку изменение размера стека реализуется перемещением стека,
указатель стека может изменяться во время любого вызова функции:
даже указатели на данные стека не должны храниться в локальных переменных.
Ассемблерные функции всегда должны иметь соответствующие прототипы на Go,
чтобы предоставить информацию об указателях для аргументов и результатов,
а также чтобы go vet мог проверить,
что смещения, используемые для доступа к ним, являются корректными.
Архитектурно-специфичные детали
Невозможно перечислить все инструкции и другие детали для каждой машины.
Чтобы увидеть, какие инструкции определены для заданной машины, например ARM,
нужно обратиться к исходному коду библиотеки поддержки obj для этой архитектуры,
расположенной в каталоге src/cmd/internal/obj/arm.
В этом каталоге находится файл a.out.go; он содержит
длинный список констант, начинающихся с A, например:
const ( AAND = obj.ABaseARM + obj.A_ARCHSPECIFIC + iota AEOR ASUB ARSB AADD ...
Это список инструкций и их написание, известные ассемблеру и линкеру для этой архитектуры.
Каждая инструкция начинается с начальной заглавной буквы A в этом списке,
так AAND представляет инструкцию побитового И,
AND (без ведущей A),
и пишется в исходном коде ассемблера как AND.
Перечисление в основном находится в алфавитном порядке.
(Архитектурно-независимый AXXX, определённый в пакете
cmd/internal/obj,
представляет недопустимую инструкцию).
Последовательность имён A не имеет никакого отношения к фактическому
кодированию машинных инструкций.
Пакет cmd/internal/obj заботится об этом детале.
Инструкции для архитектур 386 и AMD64 перечислены в
cmd/internal/obj/x86/a.out.go.
Архитектуры разделяют синтаксис для общих режимов адресации, таких как
(R1) (косвенная адресация через регистр),
4(R1) (косвенная адресация через регистр с смещением), и
$foo(SB) (абсолютный адрес).
Ассемблер также поддерживает некоторые (не обязательно все) режимы адресации,
специфичные для каждой архитектуры.
В разделах ниже перечислены эти режимы.
Одна деталь, заметная в примерах из предыдущих разделов, заключается в том,
что данные в инструкциях передаются слева направо:
MOVQ $0, CX очищает CX.
Это правило применяется даже на архитектурах, где традиционная запись использует противоположное направление.
Ниже приводятся описания ключевых деталей, специфичных для Go, для поддерживаемых архитектур.
32-битный Intel 386
Указатель на структуру g в среде выполнения поддерживается
через значение регистра, который иначе не используется (с точки зрения Go) в MMU.
В пакете runtime ассемблерный код может включать go_tls.h,
который определяет макрос get_tls, зависящий от ОС и архитектуры,
для доступа к этому регистру.
Макрос get_tls принимает один аргумент, который является регистром,
в который нужно загрузить указатель g.
Например, последовательность загрузки g и m
с использованием CX выглядит следующим образом:
#include "go_tls.h" #include "go_asm.h" ... get_tls(CX) MOVL g(CX), AX // Переместить g в AX. MOVL g_m(AX), BX // Переместить g.m в BX.
Макрос get_tls также определён на amd64.
Режимы адресации:
-
(DI)(BX*2): Расположение по адресуDIплюсBX*2. -
64(DI)(BX*2): Расположение по адресуDIплюсBX*2плюс 64. Эти режимы принимают только 1, 2, 4 и 8 в качестве множителей масштаба.
При использовании компилятора и ассемблера в режимах
-dynlink или -shared,
любое обращение к фиксированному месту в памяти, например, к глобальной переменной,
должно считаться перезаписывающим CX.
Поэтому, чтобы обеспечить безопасность при использовании этих режимов,
исходные тексты на ассемблере обычно следует избегать использования CX, кроме случаев между обращениями к памяти.
64-битный Intel 386 (также известный как amd64)
Две архитектуры ведут себя в основном одинаково на уровне ассемблера.
Код на ассемблере для доступа к указателям m и g
на 64-битной версии такой же, как и на 32-битной 386,
за исключением того, что используется MOVQ вместо MOVL:
get_tls(CX) MOVQ g(CX), AX // Переместить g в AX. MOVQ g_m(AX), BX // Переместить g.m в BX.
Регистр BP сохраняется вызываемой функцией.
Ассемблер автоматически вставляет сохранение/восстановление BP, если размер кадра больше нуля.
Использование BP как общего регистра разрешено,
однако это может мешать профилированию на основе выборки.
ARM
Регистры R10 и R11
зарезервированы компилятором и линкером.
R10 указывает на структуру g (горутина).
Внутри исходного кода на ассемблере этот указатель должен обозначаться как g;
название R10 не распознаётся.
Чтобы облегчить написание ассемблера людьми и компиляторами, линкер ARM
позволяет использовать общие формы адресации и псевдо-операции, такие как DIV или MOD,
которые могут быть не выражены одной аппаратной инструкцией.
Он реализует эти формы как несколько инструкций, часто используя регистр R11
для хранения временных значений.
Ручной ассемблерный код может использовать R11, но при этом необходимо
убедиться, что линкер не использует его также для реализации других
инструкций в функции.
При определении TEXT, указание размера кадра $-4
сообщает линкеру, что это функция-лист, которая не требует сохранения LR при входе.
Название SP всегда ссылается на виртуальный указатель стека, описанный ранее.
Для аппаратного регистра используйте R13.
Синтаксис кодов условий заключается в добавлении точки и однобуквенного или двубуквенного кода к инструкции,
например MOVW.EQ.
Можно добавлять несколько кодов: MOVM.IA.W.
Порядок модификаторов кодов не имеет значения.
Режимы адресации:
-
R0->16
R0>>16
R0<<16
R0@>16: Для<<— левый сдвигR0на 16 бит. Другие коды:->(арифметический правый сдвиг),>>(логический правый сдвиг) и@>(циклический сдвиг вправо). -
R0->R1
R0>>R1
R0<<R1
R0@>R1: Для<<— левый сдвигR0на количество бит, заданное вR1. Другие коды:->(арифметический правый сдвиг),>>(логический правый сдвиг) и@>(циклический сдвиг вправо). -
[R0,g,R12-R15]: Для многорегистровых инструкций — множество, состоящее изR0,gиR12черезR15включительно. -
(R5, R6): Пара регистров назначения.
ARM64
R18 — «платформенный регистр», зарезервирован на платформе Apple.
Чтобы предотвратить случайное использование, регистр называется R18_PLATFORM.
R27 и R28 зарезервированы компилятором и линкером.
R29 — указатель кадра.
R30 — регистр связи.
Модификаторы инструкций добавляются к инструкции после точки.
Единственные модификаторы: P (постинкремент) и W
(претинкремент): MOVW.P, MOVW.W
Режимы адресации:
-
R0->16
R0>>16
R0<<16
R0@>16: Эти режимы аналогичны режимам на 32-битном ARM. -
$(8<<12): Выполнить левый сдвиг непосредственного значения8на12бит. -
8(R0): Сложить значениеR0и8. -
(R2)(R0): Расположение по адресуR0плюсR2. -
R0.UXTB
R0.UXTB<<imm:UXTB: извлечь 8-битное значение из младших битовR0и дополнить его нулями до размераR0.R0.UXTB<<imm: выполнить левый сдвиг результатаR0.UXTBнаimmбит. Значениеimmможет быть 0, 1, 2, 3 или 4. Другие расширения включаютUXTH(16-битное),UXTW(32-битное) иUXTX(64-битное). -
R0.SXTB
R0.SXTB<<imm:SXTB: извлечь 8-битное значение из младших битовR0и дополнить его знаком до размераR0.R0.SXTB<<imm: выполнить левый сдвиг результатаR0.SXTBнаimmбит. Значениеimmможет быть 0, 1, 2, 3 или 4. Другие расширения включаютSXTH(16-битное),SXTW(32-битное) иSXTX(64-битное). -
(R5, R6): Пара регистров дляLDAXP/LDP/LDXP/STLXP/STP/STP.
Ссылка: Справочное руководство по инструкциям ассемблера Go для ARM64
PPC64
Данный ассемблер используется значениями GOARCH ppc64 и ppc64le.
Ссылка: Справочное руководство по инструкциям ассемблера Go для PPC64
IBM z/Architecture, также известная как s390x
Регистры R10 и R11 зарезервированы.
Ассемблер использует их для хранения временных значений при сборке некоторых инструкций.
R13 указывает на структуру g (горутина).
Этот регистр должен быть указан как g; имя R13 не распознаётся.
R15 указывает на кадр стека и обычно должен быть доступен только через виртуальные регистры SP и FP.
Инструкции загрузки и сохранения нескольких значений работают с диапазоном регистров.
Диапазон регистров указывается начальным и конечным регистром.
Например, LMG (R9), R5, R7 загрузит
R5, R6 и R7 со значениями 64 бит по адресам
0(R9), 8(R9) и 16(R9) соответственно.
Инструкции работы с памятью, такие как MVC и XC, записываются
с длиной как первым аргументом.
Например, XC $8, (R9), (R9) очистит
восемь байт по адресу, указанному в R9.
Если векторная инструкция принимает длину или индекс в качестве аргумента, то это будет
первый аргумент.
Например, VLEIF $1, $16, V2 загрузит
значение шестнадцать в индекс один V2.
Следует проявлять осторожность при использовании векторных инструкций, чтобы убедиться,
что они доступны во время выполнения.
Чтобы использовать векторные инструкции, машина должна иметь как векторную функциональность (бит 129 в
списке возможностей), так и поддержку ядра.
Без поддержки ядра векторная инструкция не будет иметь эффекта (она будет эквивалентна
инструкции NOP).
Режимы адресации:
-
(R5)(R6*1): Расположение по адресуR5плюсR6. Это масштабируемый режим, как на x86, но допускается только масштаб1.
MIPS, MIPS64
Регистры общего назначения именуются R0 через R31,
плавающие регистры — F0 через F31.
R30 зарезервирован для указания на g.
R23 используется как временный регистр.
В директиве TEXT размер кадра $-4 для MIPS или
$-8 для MIPS64 указывает компоновщику не сохранять LR.
SP обозначает виртуальный указатель стека.
Для регистра процессора используйте R29.
Режимы адресации:
-
16(R1): ячейка памяти по адресуR1плюс 16. -
(R1): псевдоним для0(R1).
Значение переменной окружения GOMIPS (hardfloat или
softfloat) доступно в ассемблерном коде путем предварительного определения
либо GOMIPS_hardfloat, либо GOMIPS_softfloat.
Значение переменной окружения GOMIPS64 (hardfloat или
softfloat) доступно в ассемблерном коде путем предварительного определения
либо GOMIPS64_hardfloat, либо GOMIPS64_softfloat.
Неподдерживаемые коды операций
Ассемблеры разработаны так, чтобы поддерживать компилятор, поэтому не все инструкции процессора
определяются для всех архитектур: если компилятор не генерирует такую инструкцию, она может отсутствовать.
Если требуется использовать отсутствующую инструкцию, существует два способа продолжить работу.
Один из них — обновить ассемблер для поддержки этой инструкции, что является простым,
но имеет смысл только в случае, если инструкция, вероятно, будет использоваться снова.
В противном случае, для простых одноразовых случаев возможно использование директив
BYTE и WORD,
чтобы вставить данные напрямую в поток инструкций внутри TEXT.
Вот как 64-битная функция атомарной загрузки определяется в runtime для архитектуры 386.
// uint64 atomicload64(uint64 volatile* addr); // на самом деле // void atomicload64(uint64 *res, uint64 volatile *addr); TEXT runtime·atomicload64(SB), NOSPLIT, $0-12 MOVL ptr+0(FP), AX TESTL $7, AX JZ 2(PC) MOVL 0, AX // crash with nil ptr deref LEAL ret_lo+4(FP), BX // MOVQ (%EAX), %MM0 BYTE $0x0f; BYTE $0x6f; BYTE $0x00 // MOVQ %MM0, 0(%EBX) BYTE $0x0f; BYTE $0x7f; BYTE $0x03 // EMMS BYTE $0x0F; BYTE $0x77 RET