Определение языка для Go не описывает действия в терминах сегментов, стеков, куч и т. Д. Таким образом, все это - детали реализации, которые могут измениться от одной реализации Go к другой.
В общем, хотя компиляторы Go выполняют анализ диапазона в реальном времени для переменных и используют escape-анализ , чтобы определить, следует ли выделять что-либо в памяти G-1095 * («куча») или в автоматически освобожденном хранилище («стек»). Строковые литералы могут, в зависимости от слишком большого количества вещей, быть выделенными во время компиляции в виде текста и ссылаться непосредственно оттуда, или копироваться в некоторую область данных, которая может быть либо heap-i sh, либо stack-i sh.
Предположим, для аргумента, что вы написали:
func f() {
var fruits [5]string
fruits[0] = "Apple"
}
Эта функция вообще ничего не делает, поэтому она просто исключается из сборки. Строковая константа "Apple"
вообще нигде не появляется. Давайте добавим еще немного, чтобы он действительно существовал:
package main
import "fmt"
func f() {
var fruits [5]string
fruits[0] = "Apple"
fmt.Println(fruits[0])
}
func main() {
f()
fmt.Println("foo")
}
Вот некоторая (подрезанная / очищенная) разборка main.f
в полученном двоичном файле. Обратите внимание, что реализация почти наверняка будет отличаться в других версиях Go. Это было построено с Go 1.13.5 (для amd64).
main.f:
mov %fs:0xfffffffffffffff8,%rcx
cmp 0x10(%rcx),%rsp
jbe 2f
Все до вот пример: точка входа для функции проверяет, нужно ли ей вызывать среду выполнения, чтобы выделить больше стекового пространства, потому что она собирается использовать здесь 0x58 байт стекового пространства:
1: sub $0x58,%rsp
mov %rbp,0x50(%rsp)
Это конец шаблон: после следующих нескольких инструкций мы сможем вернуться с f
с простым retq
. Теперь мы освобождаем место в стеке для массива fruits
, а также другое место, которое компилятор считает целесообразным по любой причине, и обновляем %rbp
. Затем мы сохраняем заголовок строки в %(rsp)
и %8%(rsp)
для вызова convTstring
в пакете runtime
:
lea 0x50(%rsp),%rbp
lea 0x35305(%rip),%rax # <go.string.*+0x24d> - the string is here
mov %rax,(%rsp)
movq $0x5,0x8(%rsp) # this is the length of the string
callq 408da0 <runtime.convTstring>
mov 0x10(%rsp),%rax
Функция runtime.convTstring
фактически выделяет пространство (16 байт на этом компьютере ) для другой копии заголовка строки, в «куче», затем копирует заголовок на место. Теперь эта копия готова для хранения в fruits[0]
или в другом месте. Соглашение о вызовах для Go в x86_64 немного нечетное , поэтому возвращаемое значение равно 0x10(%rsp)
, которое мы теперь скопировали в %rax
. Мы увидим, где это будет использовано в данный момент:
xorps %xmm0,%xmm0
movups %xmm0,0x40(%rsp)
Эти инструкции обнуляют 16 байтов, начиная с 0x40(%rsp)
. Мне не ясно, для чего это нужно, тем более, что мы немедленно их перезаписываем.
lea 0x11a92(%rip),%rcx # <type.*+0x11140>
mov %rcx,0x40(%rsp)
mov %rax,0x48(%rsp)
mov 0xd04a1(%rip),%rax # <os.Stdout>
lea 0x4defa(%rip),%rcx # <go.itab.*os.File,io.Writer>
mov %rcx,(%rsp)
mov %rax,0x8(%rsp)
lea 0x40(%rsp),%rax
mov %rax,0x10(%rsp)
movq $0x1,0x18(%rsp)
movq $0x1,0x20(%rsp)
callq <fmt.Fprintln>
Это, кажется, вызов fmt.Println
: так как мы передаем значение интерфейса, мы должны упаковать его как тип и указатель на значение (возможно, именно поэтому сначала вызывается runtime.convTstring
). У нас также есть os.stdout
и его интерфейсный дескриптор, вставленный непосредственно в вызов здесь, посредством некоторой вставки (обратите внимание, что этот вызов идет непосредственно к fmt.Fprintln
).
В любом случае мы передали заголовок строки, выделенный в runtime.convTstring
здесь, чтобы функционировать fmt.Println
.
mov 0x50(%rsp),%rbp
add $0x58,%rsp
retq
2: callq <runtime.morestack_noctxt>
jmpq 1b
Вот как мы возвращаемся из функции - константы 0x50 и 0x58 зависят от того, сколько стекового пространства мы распределили - и после метки, которая начало функции может перейти к остальной части шаблона ввода функции.
В любом случае, смысл всего вышеперечисленного состоит в том, чтобы показать, что:
Пятибайтовая последовательность Apple
вообще не выделяется во время выполнения. Вместо этого он существует в сегменте rodata
, известном как go.string.*
. Этот rodata
-элемент фактически является программным текстом: операционная система помещает его в постоянную память, если это вообще возможно. Он просто отделен от исполняемых инструкций для организационных целей.
Массив fruits
вообще никогда не использовался. Компилятор мог видеть, что, пока мы писали в него, мы не использовали , кроме одного вызова, поэтому он нам не понадобился.
Но заголовок строки, с помощью которого можно найти как длину строки, так и данные (в этом rodata
сегменте), did получает выделенную кучу.
не нужно , поскольку fmt.Println
не собирается сохранять этот указатель, но компилятор этого не заметил. В конце концов, среда выполнения g c освободит данные заголовка строки, выделенные в куче, если только программа не завершится полностью первой.