сборка plan9 полностью решена
Как мы все знаем, Go использует старую Unix (ошибочно придуманную) сборку plan9.Даже если вы немного разбираетесь в сборке x86, есть небольшая разница в plan9.Возможно, когда вы смотрите код, вы наткнетесь на SP в код, который выглядит как SP, но это безумие, когда на самом деле это не SP, ха-ха.
Эта статья представляет собой всестороннее введение в сборку plan9 и ответит на большинство вопросов, которые могут у вас возникнуть при подходе к сборке plan9.
В этой статье используется платформа linux amd 64. Поскольку разные платформы имеют разные наборы инструкций и регистры, нет возможности обсуждать их вместе. Это также определяется характером самой сборки.
основные инструкции
корректировка стека
Сборка Intel или AT&T предоставляет семейство инструкций push и pop В Plan9 нет push и pop Регулировка стека осуществляется за счет работы с аппаратным регистром SP, например:
SUBQ $0x18, SP // 对 SP 做减法,为函数分配函数栈帧
... // 省略无用代码
ADDQ $0x18, SP // 对 SP 做加法,清除函数栈帧
Общие инструкции аналогичны инструкциям для платформы IA64 и подробно описаны в следующих подразделах.
обработка данных
Константы представлены $num в сборке plan9, могут быть отрицательными и десятичными по умолчанию. Шестнадцатеричные числа могут быть представлены в виде $0x123.
MOVB $1, DI // 1 byte
MOVW $0x10, BX // 2 bytes
MOVD $1, DX // 4 bytes
MOVQ $-10, AX // 8 bytes
Видно, что длина хода определяется суффиксом MOV, который немного отличается от сборки Intel.Взгляните на аналогичную сборку IA64:
mov rax, 0x1 // 8 bytes
mov eax, 0x100 // 4 bytes
mov ax, 0x22 // 2 bytes
mov ah, 0x33 // 1 byte
mov al, 0x44 // 1 byte
Направление операндов сборки plan9 противоположно сборке Intel, аналогично AT&T.
MOVQ $0x10, AX ===== mov rax, 0x10
| |------------| |
|------------------------|
Но всегда есть исключения, если вы хотите понять этот сюрприз, вы можете обратиться к [1] в разделе «Ссылки».
Общие инструкции по расчету
ADDQ AX, BX // BX += AX
SUBQ AX, BX // BX -= AX
IMULQ AX, BX // BX *= AX
Подобно инструкциям по обработке данных, также можно изменить суффикс инструкции, чтобы он соответствовал операндам разной длины. Например, ADDQ/ADDW/ADDL/ADDB.
Условный переход/безусловный переход
// 无条件跳转
JMP addr // 跳转到地址,地址可为代码中的地址,不过实际上手写不会出现这种东西
JMP label // 跳转到标签,可以跳转到同一函数内的标签位置
JMP 2(PC) // 以当前指令为基础,向前/后跳转 x 行
JMP -2(PC) // 同上
// 有条件跳转
JNZ target // 如果 zero flag 被 set 过,则跳转
Набор инструкций
можно обратиться к исходному кодуarchчасть.
Кроме того, в Go 1.10 добавили много поддержки SIMD-инструкций, так что в этой версии и выше писать не так больно, как раньше, то есть не нужно заполнять байт человечиной.
регистр
регистр общего назначения
Регистры общего назначения для amd64:
(lldb) reg read
General Purpose Registers:
rax = 0x0000000000000005
rbx = 0x000000c420088000
rcx = 0x0000000000000000
rdx = 0x0000000000000000
rdi = 0x000000c420088008
rsi = 0x0000000000000000
rbp = 0x000000c420047f78
rsp = 0x000000c420047ed8
r8 = 0x0000000000000004
r9 = 0x0000000000000000
r10 = 0x000000c420020001
r11 = 0x0000000000000202
r12 = 0x0000000000000000
r13 = 0x00000000000000f1
r14 = 0x0000000000000011
r15 = 0x0000000000000001
rip = 0x000000000108ef85 int`main.main + 213 at int.go:19
rflags = 0x0000000000000212
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
Его можно использовать в ассемблере plan9.Регистры общего назначения, используемые на уровне кода приложения, в основном следующие: rax, rbx, rcx, rdx, rdi, rsi, r8~r15 Эти 14 регистров, хотя rbp и rsp также могут использоваться, Однако bp и sp будут использоваться для управления верхом и низом стека, и лучше их не использовать для операций.
Регистры, используемые в plan9, не должны иметь префикс r или e, например rax, просто напишите AX:
MOVQ $101, AX = mov rax, 101
Ниже приведено соответствие между названиями регистров общего назначения в IA64 и plan9:
IA64 | rax | rbx | rcx | rdx | rdi | rsi | rbp | rsp | r8 | r9 | r10 | r11 | r12 | r13 | r14 | rip |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Plan9 | AX | BX | CX | DX | DI | SI | BP | SP | R8 | R9 | R10 | R11 | R12 | R13 | R14 | PC |
псевдорегистр
Сборка Go также вводит 4 псевдорегистра, ссылаясь на описание официальной документации:
FP
: Frame pointer: arguments and locals.PC
: Program counter: jumps and branches.SB
: Static base pointer: global symbols.SP
: Stack pointer: top of stack.
В официальном описании есть некоторые проблемы, мы немного расширим эту инструкцию:
- ФП: используйте форму
symbol+offset(FP)
способ ссылки на входные параметры функции. Напримерarg0+0(FP)
,arg1+8(FP)
,когда ФП используется без символов,то он не компилируется.На уровне сборки символы бесполезны.Добавление символов в основном для улучшения читаемости кода. Кроме того, хотя в официальном документе псевдорегистр FP называется указателем фрейма, на самом деле он вовсе не является указателем фрейма. В соглашении x86 указатель кадра указывает на регистр BP в нижней части всего кадра стека. Если текущая вызываемая функция добавлена, в коде добавления имеется ссылка на FP, а местоположение, на которое указывает FP, находится не в кадре стека вызываемого объекта, а в кадре стека вызывающего объекта. Для получения подробной информации см. следующиеструктура стекаГлава. - ПК: Фактически, это обычный регистр ПК в знании архитектуры, который соответствует регистру ip на платформе x86 и рипу на платформе amd64. Помимо отдельных переходов, рукописный код plan9 реже имеет дело с регистрами ПК.
- SB: глобальный статический базовый указатель, который обычно используется для объявления функций или глобальных переменных. Конкретное использование будет показано в разделе знаний о функциях и примерах позже.
- SP: этот регистр SP Plan9 указывает на начальную позицию локальных переменных текущего кадра стека, используя форму
symbol+offset(SP)
способ обращения к локальным переменным функции. Допустимое значение смещения: [-framesize, 0. Обратите внимание, что это интервал, который закрыт слева и открыт справа. Если все локальные переменные имеют размер 8 байт, то можно использовать первую локальную переменную.localvar0-8(SP)
Представлять. Это тоже регистр, который не означает ни слова. Это две разные вещи от аппаратного регистра SP.В случае, когда размер кадра стека равен 0, псевдорегистр SP и аппаратный регистр SP указывают на одно и то же место. При написании ассемблерного кода от руки, если онsymbol+offset(SP)
форме, он представляет собой псевдорегистр SP. еслиoffset(SP)
Имеется в виду аппаратный регистр SP. Обязательно обратите внимание. Для вывода скомпилированного кода (go tool compile -S / go tool objdump) все SP в настоящее время являются SP аппаратного регистра, с символами или без них.
Здесь мы кратко объясним несколько моментов путаницы:
- Псевдо-SP и аппаратный SP-это не одно и то же.При написании кода способ различить псевдо-SP и аппаратный SP-это посмотреть, есть ли символ перед SP. Если есть символ, то это псевдорегистр, если нет, то это аппаратный регистр SP.
- Относительное положение SP и FP изменится, поэтому не следует пытаться использовать псевдорегистр SP для поиска значений, на которые ссылается FP + смещение, таких как входные параметры функции и возвращаемые значения.
- Псевдо-SP, упомянутый в официальной документации, указывает на вершину стека, что проблематично. Локальная переменная, на которую она указывает, на самом деле является нижней частью всего стека (кроме вызывающего BP), поэтому более уместно говорить «нижняя».
- В коде, выводимом с помощью go tool objdump/go tool compile -S, нет псевдорегистров SP и FP.Упомянутый выше метод различения псевдоSP и аппаратных регистров SP не может использоваться для вывода результатов двух вышеуказанных команд. из. В результате компиляции и дизассемблирования остается только реальный регистр SP.
- Указатель фрейма в официальном исходном коде FP и Go — это не одно и то же.Указатель фрейма в исходном коде относится к значению регистра BP вызывающей стороны, который здесь равен псевдо-SP вызывающей стороны.
Не имеет значения, если вы не понимаете приведенные выше инструкции.После того, как вы ознакомитесь со структурой стека функции, вы сможете понять ее, проверяя ее снова и снова. Личное мнение, это ямы, выкопанные Go. .
объявление переменной
Так называемая переменная в ассемблере обычно представляет собой значение только для чтения, хранящееся в разделе .rodata или .data. В соответствии с прикладным уровнем это инициализированная глобальная константа, переменная, статические переменные/константы.
Используйте DATA в сочетании с GLOBL для определения переменной. Использование ДАННЫХ:
DATA symbol+offset(SB)/width, value
Большинство параметров воспринимаются буквально, но это смещение требует небольшого внимания. Его значением является смещение значения относительно символа символа, а не смещение относительно глобального адреса.
Используйте директиву GLOBL, чтобы объявить переменную глобальной и получить два дополнительных параметра, один из которых является флагом, а другой — общим размером переменной.
GLOBL divtab(SB), RODATA, $64
GLOBL должен следовать за директивой DATA.Ниже приведен полный пример глобальной переменной с несколькими определенными только для чтения:
DATA age+0x00(SB)/4, $18 // forever 18
GLOBL age(SB), RODATA, $4
DATA pi+0(SB)/8, $3.1415926
GLOBL pi(SB), RODATA, $8
DATA birthYear+0(SB)/4, $1988
GLOBL birthYear(SB), RODATA, $4
Как упоминалось ранее, все символы объявляются со смещением 0.
Иногда вам может понадобиться определить массивы или строки в глобальных переменных, в этом случае вам нужно использовать ненулевое смещение, например:
DATA bio<>+0(SB)/8, $"oh yes i"
DATA bio<>+8(SB)/8, $"am here "
GLOBL bio<>(SB), RODATA, $16
Большинство из них относительно легко понять, но здесь мы ввели новый тег<>
, который следует за именем символа, указывая, что глобальная переменная действует только в текущем файле, аналогично статической переменной в языке C. Если на переменную ссылаются в другом файле, она сообщитrelocation target not found
ошибка.
Флаг, упомянутый в этом разделе, также может иметь другие значения:
NOPROF
= 1
(ForTEXT
items.) Don't profile the marked function. This flag is deprecated.DUPOK
= 2
It is legal to have multiple instances of this symbol in a single binary. The linker will choose one of the duplicates to use.NOSPLIT
= 4
(ForTEXT
items.) Don't insert the preamble to check if the stack must be split. The frame for the routine, plus anything it calls, must fit in the spare space at the top of the stack segment. Used to protect routines such as the stack splitting code itself.RODATA
= 8
(ForDATA
andGLOBL
items.) Put this data in a read-only section.NOPTR
= 16
(ForDATA
andGLOBL
items.) This data contains no pointers and therefore does not need to be scanned by the garbage collector.WRAPPER
= 32
(ForTEXT
items.) This is a wrapper function and should not count as disablingrecover
.NEEDCTXT
= 64
(ForTEXT
items.) This function is a closure so it uses its incoming context register.
При использовании литералов этих флагов он должен быть в файле сборки#include "textflag.h"
.
объявление функции
Давайте взглянем на определение типичной функции сборки plan9:
// func add(a, b int) int
// => 该声明定义在同一个 package 下的任意 .go 文件中
// => 只有函数头,没有实现
TEXT pkgname·add(SB), NOSPLIT, $0-8
MOVQ a+0(FP), AX
MOVQ a+8(FP), BX
ADDQ AX, BX
MOVQ BX, ret+16(FP)
RET
Почему он называется ТЕКСТ? Если вы хоть немного разбираетесь в сегментации данных программы в файлах и памяти, то должны знать, что наш код хранится в сегменте .text в бинарных файлах, что является традиционным методом именования. На самом деле в plan9 TEXT — это инструкция, используемая для определения функции. В дополнение к TEXT, есть также DATA/GLOBL, упомянутые в предыдущем объявлении переменной.
Часть pkgname в определении может быть опущена, и ее можно написать, если вы этого не хотите. Однако, если вы пишете pkgname, вам нужно изменить код после переименования пакета, поэтому рекомендуется его не писать.
середина·
Он особенный, это середина юникода, метод ввода этой точки под макинтошoption+shift+9
. После связывания программы все промежуточные точки·
заменяются точками.
, например, ваш методruntime·main
, символы в скомпилированной программеruntime.main
. Хм, выглядит извращенно. Кратко резюмируя:
参数及返回值大小
|
TEXT pkgname·add(SB),NOSPLIT,$32-32
| | |
包名 函数名 栈帧大小(局部变量+可能需要的额外调用函数的参数空间的总大小,但不包括调用其它函数时的 ret address 的大小)
структура стека
Ниже приведен типичный набор функций:
-----------------
current func arg0
----------------- <----------- FP(pseudo FP)
caller ret addr
+---------------+
| caller BP(*) |
----------------- <----------- SP(pseudo SP,实际上是当前栈帧的 BP 位置)
| Local Var0 |
-----------------
| Local Var1 |
-----------------
| Local Var2 |
----------------- -
| ........ |
-----------------
| Local VarN |
-----------------
| |
| |
| temporarily |
| unused space |
| |
| |
-----------------
| call retn |
-----------------
| call ret(n-1)|
-----------------
| .......... |
-----------------
| call ret1 |
-----------------
| call argn |
-----------------
| ..... |
-----------------
| call arg3 |
-----------------
| call arg2 |
|---------------|
| call arg1 |
----------------- <------------ hardware SP 位置
| return addr |
+---------------+
BP вызывающей стороны на рисунке относится к значению регистра BP вызывающей стороны.Некоторые люди называют BP вызывающей стороны указателем кадра вызывающей стороны.На самом деле эта привычка унаследована от архитектуры x86. В ассемблерной документации Go псевдорегистр FP также называется указателем фрейма, но эти два указателя фрейма — совсем не одно и то же.
Кроме того, следует отметить, что BP вызывающей стороны вставляется компилятором во время компиляции.Когда пользователь пишет код, часть BP вызывающей стороны не учитывается при расчете размера кадра. Основным основанием для принятия решения о том, следует ли вставлять BP вызывающего абонента, является:
- Размер кадра стека функции больше 0
- Следующая функция возвращает true
func Framepointer_enabled(goos, goarch string) bool {
return framepointer_enabled != 0 && goarch == "amd64" && goos != "nacl"
}
Если компилятор не вставит БП вызывающей стороны (так называемый указатель фрейма в исходном коде) в итоговый результат сборки, между псевдо ИП и псевдо FP останется только 8-байтный адрес возврата вызывающей стороны, а БП вставляется. , будут добавлены дополнительные 8 байтов. То есть относительное положение псевдо-SP и псевдо-FP не является фиксированным, оно может быть разнесено на 8 или 16 байтов. И основа суждения будет варьироваться в зависимости от платформы и версии Go.
Как видно из рисунка, псевдорегистр ФП указывает на начальную позицию входящих параметров функции, т.к. стек растет в сторону нижнего адреса, для облегчения ссылки параметров через регистры направление размещения параметры и направление роста стека противоположны, а именно:
FP
high ----------------------> low
argN, ... arg3, arg2, arg1, arg0
Предполагая, что все параметры имеют размер 8 байт, мы можем получить доступ к первому параметру с symname+0(FP), ко второму параметру с symname+8(FP) и так далее. Использование псевдо-SP для ссылки на локальную переменную в принципе аналогично, но поскольку псевдо-SP указывает на конец локальной переменной, symname-8(SP) означает первую локальную переменную, а symname-16(SP) означает первую локальную переменную. второй и так далее. Конечно, это предполагает, что все локальные переменные занимают 8 байт.
Адрес возврата вызывающей стороны и текущая функция arg0 в верхней части графика распределяются вызывающей стороной. Не включен в текущий кадр стека.
Поскольку сам официальный документ довольно расплывчатый, давайте рассмотрим вызовы функций, чтобы увидеть, какова связь между этими истинными и ложными SP/FP/BP:
caller
+------------------+
| |
+----------------------> --------------------
| | |
| | caller parent BP |
| BP(pseudo SP) --------------------
| | |
| | Local Var0 |
| --------------------
| | |
| | ....... |
| --------------------
| | |
| | Local VarN |
--------------------
caller stack frame | |
| callee arg2 |
| |------------------|
| | |
| | callee arg1 |
| |------------------|
| | |
| | callee arg0 |
| ----------------------------------------------+ FP(virtual register)
| | | |
| | return addr | parent return address |
+----------------------> +------------------+--------------------------- <-------------------------------+
| caller BP | |
| (caller frame pointer) | |
BP(pseudo SP) ---------------------------- |
| | |
| Local Var0 | |
---------------------------- |
| |
| Local Var1 |
---------------------------- callee stack frame
| |
| ..... |
---------------------------- |
| | |
| Local VarN | |
SP(Real Register) ---------------------------- |
| | |
| | |
| | |
| | |
| | |
+--------------------------+ <-------------------------------+
callee
правила вычисления argsize и framesize
argsize
В объявлении функции:
TEXT pkgname·add(SB),NOSPLIT,$16-32
Ранее было сказано, что $16-32 означает $framesize-argsize. Когда Go вызывает функцию, вызывающая сторона должна подготовить как параметры, так и возвращаемые значения в своем фрейме стека. вызываемому по-прежнему необходимо знать этот argsize при его объявлении. Метод вычисления argsize — это сумма размера параметра + сумма размера возвращаемого значения. Например, если входной параметр имеет 3 типа int64, а возвращаемое значение — 1 тип int64, то argsize = sizeof(int64) * 4 здесь.
Однако реальный мир никогда не бывает таким прекрасным, как мы предполагаем, параметры функций часто смешиваются с несколькими типами, и необходимо учитывать проблемы выравнивания памяти.
Если вы не уверены, сколько argsize требуется вашей сигнатуре функции, вы можете просто реализовать пустую функцию с той же сигнатурой и использовать инструмент objdump для обратного проектирования того, сколько места должно быть выделено.
framesize
Размер кадра функции немного сложнее, размер кадра написанного от руки кода не должен учитывать вставленный компилятором БП вызывающего абонента, он должен учитывать:
- Локальные переменные и размер каждой переменной.
- Когда в функции есть вызовы других функций, если да, то при вызове необходимо учитывать параметры и возвращаемые значения callee. Хотя значение адреса возврата (рип) также сохраняется во фрейме стека вызывающей стороны, процесс завершается инструкцией CALL и инструкцией RET для сохранения и восстановления регистра ПК.При рукописной сборке это также не нужно учитывать. Регистр ПК В стеке требуется 8 байт.
- В принципе, пока вы не перезаписываете локальные переменные при вызове функции, все в порядке. Выделение еще нескольких байтов размера кадра тоже не умрет.
- Это нормально, если вы хотите перезаписать локальные переменные, если логика в порядке. Просто убедитесь, что вызывающий и вызываемый при входе и выходе из функции сборки могут правильно получить возвращаемое значение.
Пример
add/sub/mul
math.go:
package main
import "fmt"
func add(a, b int) int // 汇编函数声明
func sub(a, b int) int // 汇编函数声明
func mul(a, b int) int // 汇编函数声明
func main() {
fmt.Println(add(10, 11))
fmt.Println(sub(99, 15))
fmt.Println(mul(11, 12))
}
math.s:
#include "textflag.h" // 因为我们声明函数用到了 NOSPLIT 这样的 flag,所以需要将 textflag.h 包含进来
// func add(a, b int) int
TEXT ·add(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX // 参数 a
MOVQ b+8(FP), BX // 参数 b
ADDQ BX, AX // AX += BX
MOVQ AX, ret+16(FP) // 返回
RET
// func sub(a, b int) int
TEXT ·sub(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX
MOVQ b+8(FP), BX
SUBQ BX, AX // AX -= BX
MOVQ AX, ret+16(FP)
RET
// func mul(a, b int) int
TEXT ·mul(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX
MOVQ b+8(FP), BX
IMULQ BX, AX // AX *= BX
MOVQ AX, ret+16(FP)
RET
// 最后一行的空行是必须的,否则可能报 unexpected EOF
Поместите эти два файла в любой каталог и выполнитеgo build
И запустите его, чтобы увидеть эффект.
Псевдорегистр SP, псевдорегистр FP и аппаратный регистр SP
Давайте напишем простой код, чтобы доказать позиционную связь между псевдо-SP, псевдо-FP и аппаратным SP.
spspfp.s:
#include "textflag.h"
// func output(int) (int, int, int)
TEXT ·output(SB), $8-48
MOVQ 24(SP), DX // 不带 symbol,这里的 SP 是硬件寄存器 SP
MOVQ DX, ret3+24(FP) // 第三个返回值
MOVQ perhapsArg1+16(SP), BX // 当前函数栈大小 > 0,所以 FP 在 SP 的上方 16 字节处
MOVQ BX, ret2+16(FP) // 第二个返回值
MOVQ arg1+0(FP), AX
MOVQ AX, ret1+8(FP) // 第一个返回值
RET
spspfp.go:
package main
import (
"fmt"
)
func output(int) (int, int, int) // 汇编函数声明
func main() {
a, b, c := output(987654321)
fmt.Println(a, b, c)
}
Выполните приведенный выше код, чтобы получить вывод:
987654321 987654321 987654321
В сочетании с кодом мы можем узнать, что наша текущая структура стека выглядит следующим образом:
------
ret2 (8 bytes)
------
ret1 (8 bytes)
------
ret0 (8 bytes)
------
arg0 (8 bytes)
------ FP
ret addr (8 bytes)
------
caller BP (8 bytes)
------ pseudo SP
frame content (8 bytes)
------ hardware SP
Размер кадра в примерах в этом разделе больше 0. Читатель может попытаться изменить размер кадра на 0, а затем настроить смещение при обращении к псевдо-SP и аппаратному SP в коде для изучения, когда размер кадра равен 0. псевдо-FP, псевдо-SP и аппаратный SP.относительное положение между ними.
Пример в этом разделе должен сообщить вам, что относительные положения псевдо-SP и псевдо-FP будут меняться.Вы не должны использовать псевдо-SP и смещение> 0 для ссылки на данные при рукописном вводе, иначе результат может быть неожиданным.
Ассемблер вызывает неассемблерные функции
output.s:
#include "textflag.h"
// func output(a,b int) int
TEXT ·output(SB), NOSPLIT, $24-8
MOVQ a+0(FP), DX // arg a
MOVQ DX, 0(SP) // arg x
MOVQ b+8(FP), CX // arg b
MOVQ CX, 8(SP) // arg y
CALL ·add(SB) // 在调用 add 之前,已经把参数都通过物理寄存器 SP 搬到了函数的栈顶
MOVQ 16(SP), AX // add 函数会把返回值放在这个位置
MOVQ AX, ret+16(FP) // return result
RET
output.go:
package main
import "fmt"
func add(x, y int) int {
return x + y
}
func output(a, b int) int
func main() {
s := output(10, 13)
fmt.Println(s)
}
расширенная тема
Тема расширения больше не обновляется в блоге и медленно суммируется на github:
Отдельное спасибо
В процессе исследования я в основном беспокою Чжуо Джуджу, когда сталкиваюсь с людьми, которых не очень хорошо понимаю.mzh.io/Здорово. Отдельное спасибо ему за то, что дал много подсказок и советов.
использованная литература
- Перейдите на asliat.GitHub.IO/blog/post/…
- davidwong.fr/goasm
- woohoo .do уступает .net/blog/go-stable…
- GitHub.com/golang/go/post…
Ссылка [4] требует особого внимания, фрейм стека вызываемого абонента, приведенный на слайде, также включает обратный адрес вызывающего, что лично я считаю не очень подходящим.