В работе сборка Golang и Plan9

Go Язык программирования

Краткий обзор содержания этой статьи:

через предыдущую статьюВ принцип сборки Golang, мы знаем, что генерация объектного кода прошла через эти процессы. Сегодня давайте узнаем, как сгенерированный объектный код выполняется на компьютере. и консультируясьGolangКомпиляция Plan9 для изучения некоторых внутренних секретов Голанга.

Среда выполнения Golang

Когда мы запустим скомпилированный код Go, он появится в системе как процесс. Затем начните обрабатывать запросы и данные, и мы увидим такую ​​информацию, как потребление памяти, соотношение ЦП и т. Д., Используемые этим процессом. В этой статье объясняется, как память, ЦП, операционная система (конечно, другое оборудование, не относящееся к делу в тексте, не будет обсуждаться) взаимодействуют для выполнения задач, указанных нашим кодом, во время выполнения программы.

ОЗУ

Во-первых, давайте поговорим о памяти. Давайте сначала посмотрим на процесс Go, который мы бегаем.

код показывает, как показано ниже:

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", sayHello)

	err := http.ListenAndServe(":9999", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

func sayHello(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("fibonacci: %d\n", fibonacci(1000))

	_, _ = fmt.Fprint(w, "Hello World!")
}

func fibonacci(num int) int {
	if num < 2 {
		return 1
	}
	return fibonacci(num-1) + fibonacci(num-2)
}

Посмотрим на реализацию

dayu.com >ps aux

USER               PID   %CPU  %MEM      VSZ     RSS    TT  STAT   STARTED    TIME     COMMAND
xxxxx              3584  99.2  0.1     4380456  4376   s003  R+    8:33下午   0:05.81  ./myhttp

Здесь не будем сначала обращать внимание на другие показатели, посмотрим сначала на негоVSZиRSS.

  • VSZ: относится к виртуальному адресу, который является фактической памятью программы. Содержит выделенную память, которая еще не использовалась.
  • RSS: это фактическая физическая память, включая память стека и память кучи.

Каждый процесс работает в собственной памяти Sandbox, и адреса, выделенные программой, являются «виртуальной памятью». Физическая память на самом деле невидима для разработчика программы, а виртуальный адрес выше, чем фактический физический адрес процесса. Гораздо больше Отказ Адрес, соответствующий указателю, мы часто принимаем в программировании, на самом деле является виртуальным адресом. Здесь мы должны обратить внимание на различение виртуальной памяти и физической памяти. Сделайте фотографию, чтобы почувствовать это.

虚拟内存与物理内存

Эта картинка в основном иллюстрирует два вопроса:

  1. Программа использует виртуальную память, но операционная система отображает виртуальную память в физическую память, вы обнаружите, что VSZ всех процессов на вашей машине намного больше;
  2. Физическая память может совместно использоваться несколькими процессами, и даже разные адреса внутри процесса могут отображаться на один и тот же адрес физической памяти.

Выше разобрались, к чему конкретно относится память в программе.Далее объясню, как программа использует память (виртуальную память).Короче говоря, память - это аппаратная часть, которая работает быстрее, чем доступ к жесткому диску.Для облегчения управление памятью, операционная система Разделите память, выделенную процессу, на разные функциональные блоки. Как мы часто говорим: область кода, область статических данных, область кучи, область стека и т. д.

Вот картинка из интернета для ознакомления.

go语言内存的划分

Вот распределение нашего приложения (процесс) в виртуальной памяти.

Кодовая площадь: Store - это машинный код, который мы составили. Как правило, эта область может быть только для чтения только.

Область статических данных: хранит глобальные переменные и константы. Адреса этих переменных определяются при компиляции (это тоже преимущество использования виртуальных адресов, если это физические адреса, то эти адреса невозможно определить при компиляции). И данные, и BSS относятся к этой части. Эта часть будет уничтожена только при завершении программы (kill, crasg и т. д.).

Площадь стека: в основномGolangВнутри функций, методов и где хранятся их локальные переменные. Эта часть выделяется, когда функция и метод начинают выполняться, и освобождается после запуска.Обратите особое внимание на освобождение здесь и не будет очищать память. В последующих статьях будет подробно рассказано о распределении памяти; есть еще один момент, который следует помнить, что стек обычно распределяется от старших адресов к младшим, другими словами: старшие адреса принадлежат младшему стеку, а младшие адреса принадлежат низу стека. стек, кучи наоборот.

область кучи: нравитсяC/C++Язык, куча полностью под контролем программиста. ноGolangИз-за механизма GC нам не нужно заботиться, выделяется ли память на стеке или куча, когда мы пишем код.GolangОн сам рассудит, что если жизненный цикл переменной не может быть уничтожен после выхода из функции или ресурсов в стеке недостаточно выделено и т. д., то она будет помещена в кучу. Производительность кучи будет хуже, чем у стека. Причина также оставлена ​​на рассмотрение статей, связанных с распределением памяти.

Структура памяти понятна, и наша программа должна получить команду от операционной системы, чтобы она работала правильно, когда она загружается в память.

Добавьте более важное понятие:

Адресное пространство: обычно относится к способности ЦП адресовать память.С точки зрения непрофессионала, это вопрос о том, сколько памяти может быть использовано максимально. Например: 32 адресных строки (32-разрядная машина), тогда общее адресное пространство составляет 2^32, а если это 64-разрядная машина, это адресное пространство 2^64. можно использоватьuname -aчтобы увидеть количество цифр, поддерживаемых вашей системой.

Операционная система, ЦП, память взаимодействуют друг с другом

Чтобы прояснить работу и вызов программы, мы должны сначала прояснить взаимосвязь между операционной системой, памятью, процессором и регистрами.

  • CPU: мозг компьютера, он может понять и выполнять инструкции;
  • Регистр: Строго говоря, регистр является неотъемлемой частью ЦП.В основном он отвечает за временное хранение данных ЦП во время расчета.Конечно, ЦП также имеет многоуровневый кеш, который к нам не относится здесь, поэтому я проигнорирую его.Все знают, что его цель - восполнить разрыв между памятью и скоростью процессора;
  • Память: Как и выше, память разделена на разные области, и каждая часть хранит разные данные, конечно, разделение этих областей и отображение виртуальной памяти и физической памяти выполняются операционной системой;
  • Операционная система: контролирует различные аппаратные ресурсы, предоставляет операционные интерфейсы (системные вызовы) и управление другими работающими программами.

Операционная система здесь — это программное обеспечение, а ЦП, регистры и память (физическая память) — это реальное оборудование. Хотя операционная система тоже куча написанного кода. Но это интерфейс аппаратного обеспечения для других приложений. Как правило, операционная система управляет всеми аппаратными ресурсами посредством системных вызовов.Она назначает выполнение других программ ЦП для выполнения других программ.Однако для того, чтобы каждая программа могла использовать ЦП, ЦП передает управление ЦП. ЦП через временные прерывания к операционной системе.

Пусть операционная система управляет нашими программами, а программы, которые мы пишем, должны следовать правилам операционной системы. Это позволяет операционной системе контролировать выполнение программы, переключать процессы и многое другое.

最后我们的代码被编译成机器码之后,本质就是一条条的指令。我们期望的就是CPU去执行完这些指令进而完成任务。而操作系统又能够帮助我们让CPU来执行代码以及提供所需资源的调用接口(系统调用)。 Это очень просто?

Соглашения о вызовах программ Go

Выше мы знаем, что вся виртуальная память делится на: область кода, область статических данных, область стека, область кучи. Соглашение о вызовах программы Go (фактически правила запуска функций и методов), которое будет обсуждаться далее, в основном касается части стека, упомянутой выше (часть кучи будет обсуждаться в статье о распределении памяти). И как аппаратная и программная части компьютера работают вместе. Далее давайте посмотрим, как основные функции модулей и методы программы выполняются и вызывают друг друга.

Распределение функций в стеке

В этой части мы сначала разбираемся в некоторых теориях, а затем анализируем на практическом примере. Давайте сначала посмотрим на картинкуGolangКак функции распределяются в стеке.

Несколько профессиональных терминов:

  • Стэк: Стэк сказал здесь, согласуется с приведенной выше интерпретацией смысла. Будь то процесс, поток, горутина имеет свой собственный стек вызовов;
  • Кадр стека: его можно понимать как область, выделенную для функции в стеке при вызове функции;
  • Caller: вызывающая сторона, например: функция вызывает функцию b, тогда a является вызывающей стороной
  • Callee: вызываемый абонент, или в приведенном выше примере b - вызываемый абонент

栈帧

На этой картинке栈帧Структура. Также можно сказать, что кадр стека — это пространство стека, выделенное стеком для функции, которое включает адрес вызывающей функции, локальные переменные, адрес возвращаемого значения, параметры вызывающей стороны и другую информацию.

Здесь следует отметить несколько моментов, т.BP,SPОба представляют соответствующие регистры.

  • BP: Регистр расширенного базового указателя (расширенный базовый указатель), также известный как указатель кадра, хранит указатель, представляющий начало стека функций.
  • SP: регистр указателя стека (расширенный указатель стека), в котором хранится указатель, в котором хранится верхняя часть пространства стека функций, где заканчивается выделение пространства стека функций.Обратите внимание, что это аппаратный регистр, а не псевдорегистр в Plan9. .

BPиSPСоберите вместе, один для начала (верхняя часть стека) и один для конца (нижняя часть стека).

С вышеуказанными базовыми знаниями, давайте будем использовать практические примеры для его проверки.

Вызов экземпляра Go

Вначале мы начнем с простой функции, чтобы проанализировать процесс вызова всей функции (следующее включаетPlan9Сборка, прошу не паниковать, большинство из них можно понять, комментарии тоже напишу).

package main

func main() {
	a := 3
	b := 2
	returnTwo(a, b)
}

func returnTwo(a, b int) (c, d int) {
	tmp := 1 // 这一行的主要目的是保证栈桢不为0,方便分析
	c = a + b
	d = b - tmp
	return
}

Выше есть две функции,mainопределить две локальные переменные, затем вызватьreturnTwoфункция.returnTwoФункция имеет два параметра и два возвращаемых значения. Проектирование двух возвращаемых значений в основном заключается в том, чтобы смотреть на них вместеgolangКак реализовано множественное возвращаемое значение. Далее мы показываем ассемблерный код, соответствующий приведенному выше коду.

main函数

Есть несколько строк кода, которые нуждаются в особом объяснении,

0x0000 00000 (test1.go:3)       TEXT    "".main(SB), ABIInternal, $56-0

Важная информация в этой строке:$56-0.56Указывает размер кадра стека функции (две локальные переменные, два параметра типа int, два возвращаемых значения типа int, 1 сохраняет базовый указатель, всего 7 * 8 = 56); 0 означаетmianРазмер параметров и возвращаемое значение функции. позже может быть вreturnTwoДавайте посмотрим, каково его возвращаемое значение.

Далее давайте посмотрим, как компьютер распределяет размер в стеке.

0x000f 00015 (test1.go:3)       SUBQ    $56, SP // 分配,56的大小在上面第一行定义了
... ...
0x004b 00075 (test1.go:7)       ADDQ    $56, SP // 释放掉,但是并未清空

Эти две строки, одна для выделения и одна для освобождения. зачем использоватьSUBQМожно ли выделить инструкции? иADDQвыпуск? Помните, что мы говорили ранее?SPЭто регистр-указатель, который указывает на вершину стека, а стек распределяется от старшего адреса к младшему. Так сделайте к нему вычитание, значит ли это, что указатель перемещается со старшего адреса на младший адрес? То же самое верно и для выпуска, операция сложения помещаетSPвосстановить в исходное состояние.

Давайте посмотрим наBPработа с реестром.

0x0013 00019 (test1.go:3)       MOVQ    BP, 48(SP) // 保存BP
0x0018 00024 (test1.go:3)       LEAQ    48(SP), BP // BP存放了新的地址
... ...
0x0046 00070 (test1.go:7)       MOVQ    48(SP), BP // 恢复BP的地址

Эти три строчки кода кажутся запутанными? Писать и писать путают. Опишу сначала словами, а потом объясню в картинках.

Сделаем следующие предположения: В это время ВР указывает наценностьДа: 0x00ff, 48 (СП)адресДа: 0x0008.

  • Первая инструкцияMOVQ BP, 48(SP)Это для0x00ffнаписать48(SP)положение;
  • вторая инструкцияLEAQ 48(SP), BPзаключается в обновлении указателя регистра, пустьBPспасти48(SP)Обратитесь к этой позиции, т.0x00ffэто значение.
  • третья инструкцияMOVQ 48(SP), BP, потому что изначально48(SP)сохранить первыйBPсохраненное значение0x00ff, так вот опятьBPвосстановлена ​​назад.

Роль этих строк кода очень важна, благодаря этому при выполнении мы можем найти место начала функции и вернуться к месту вызова функции, чтобы она могла продолжать выполняться (если вы чувствуете себя ущемленным , пускай сначала, а потом есть фотки, вернусь разбираться после прочтения). Давайте взглянемreturnTwoфункция.

returnTwo函数汇编

здесьNOSPLIT|ABIInternal, $0-32Обратите внимание, что размер кадра стека этой функции равен 0. Поскольку имеется два параметра типа int и два возвращаемых значения типа int, общее значение равно4*8 = 32Размер в байтах такой же, как указано выше?mainправильно функционировать? .

Здесь нетreturnTwoРазмер кадра стека является функцией 0 указывает на то, что путают его? Эта функция не требует места в стеке? На самом деле основная причина в том, что для использования стека требуется передача параметров golang и возвращаемые значения (именно поэтому перейдите на поддержку причины многопараметрического возврата). Поэтому пространство обязательных параметров и возвращаемых значений поcallerпредоставлять.

Затем мы используем полную диаграмму, чтобы продемонстрировать этот процесс вызова.

函数调用

На то, чтобы нарисовать эту картинку, ушло около часа, надеюсь, она поможет вам понять.

Весь процесс таков: инициализация ----> вызов основной функции ----> вызов функции returnTwo ----> возврат returnTwo ----> возврат main.

Благодаря этой картинке в сочетании с моим текстовым объяснением выше, я думаю, все смогут понять. Но вот еще несколько моментов, на которые стоит обратить внимание:

  • BPиSPэто регистр, который хранит адрес в стеке, поэтому он можетSPВыполните операцию, чтобы найти позицию следующей инструкции;
  • стек восстановленADDQ $56, SP, только что изменилSPМесто, на которое указано, данные в памяти не будут очищены, только в следующий раз, когда они будут выделены и использованы;
  • Память параметров и возвращаемых значений вызываемого объекта выделяется вызывающим;
  • Когда returnTwo рет,Следующая инструкция вызова returnTwoПозиция стека будет вытолкнута, то есть на рисунке0x0d00Адрес — это хранимая инструкция, поэтому после возврата из функции returnTwo,SPуказал снова0x0d08адрес.

Поскольку некоторые из вышеперечисленныхPlan9Попутно я познакомлю с некоторыми его грамматиками, говорить непосредственно о грамматике было бы очень скучно, далее будет представлено в сочетании с некоторыми ситуациями, которые будут использоваться на практике. Оба получают и изучают грамматику.

План сборки9

Компиляция всей нашей программы в итоге будет переведена в машинный код, а ассемблер можно рассматривать как текстовую форму машинного кода, и они могут находиться во взаимно однозначном соответствии. Итак, если мы немного разберемся в ассемблере, мы сможем проанализировать множество практических задач.

Разработчики языка go все самые ТОПовые программисты в мире.Они предпочитают притворяться вместо стандартныхAT&Tнет нуждыIntelЧто касается ассемблера, то приходится самому делать.Нет возможности. Сборка Golang основана наPlan9Скомпилированный, я лично считаю, что он слишком сложен для полного понимания, потому что он включает в себя много базовых знаний. Но если вы просто хотите понять, вы можете это сделать. Давайте попробуем несколько примеров ниже.

PS: Эту вещь не надо понимать, это слишком мало, чтобы вкладываться в вывод, и это можно понять инженеру-прикладнику.

Перед официальным стартом мы еще добавим некоторую необходимую информацию, часть которой была освещена выше, для полноты картины мы представим ее здесь целиком.

Несколько важных псевдорегистров:

  • SB: виртуальный регистр, содержащий указатель статической базы, который является начальным адресом адресного пространства нашей программы;
  • NOSPLIT: указывает компилятору, что его не следует вставлять.stack-splitНачальная инструкция, используемая для проверки необходимости расширения стека;
  • FP: используйте символ формы + смещение (FP) для обозначения входных параметров функции;
  • SP:Регистр SP в plan9 указывает на начальную позицию локальной переменной текущего кадра стека.Используйте метод символ+смещение(SP) для ссылки на локальную переменную функции.Примечание: Этот регистр отличается от регистра регистр выше.Вот псевдорегистры, и то, что мы показываем, аппаратные регистры.

Есть и некоторые другие инструкции по эксплуатации, большинство из которых видно из названия, поэтому я не буду их больше представлять, просто приступайте к работе.

Просмотр функции перевода, соответствующей коду приложения go

package main

func main() {
}

func test() []string {
    a := make([]string, 10)
    return a
}

--------

"".test STEXT size=151 args=0x18 locals=0x40
        0x0000 00000 (test1.go:6)       TEXT    "".test(SB), ABIInternal, $64-24 // 栈帧大小,与参数、返回值大小
        0x0000 00000 (test1.go:6)       MOVQ    (TLS), CX
        0x0009 00009 (test1.go:6)       CMPQ    SP, 16(CX)
        0x000d 00013 (test1.go:6)       JLS     141
        0x000f 00015 (test1.go:6)       SUBQ    $64, SP
        0x0013 00019 (test1.go:6)       MOVQ    BP, 56(SP)
        0x0018 00024 (test1.go:6)       LEAQ    56(SP), BP
        ... ...
        0x001d 00029 (test1.go:6)       MOVQ    $0, "".~r0+72(SP)
        0x0026 00038 (test1.go:6)       XORPS   X0, X0
        0x0029 00041 (test1.go:6)       MOVUPS  X0, "".~r0+80(SP)
        0x002e 00046 (test1.go:7)       PCDATA  $2, $1
        0x002e 00046 (test1.go:7)       LEAQ    type.string(SB), AX
        0x0035 00053 (test1.go:7)       PCDATA  $2, $0
        0x0035 00053 (test1.go:7)       MOVQ    AX, (SP)
        0x0039 00057 (test1.go:7)       MOVQ    $10, 8(SP)
        0x0042 00066 (test1.go:7)       MOVQ    $10, 16(SP)
        0x004b 00075 (test1.go:7)       CALL    runtime.makeslice(SB) // 对应的底层runtime function
        ... ...
        0x008c 00140 (test1.go:8)       RET
        0x008d 00141 (test1.go:8)       NOP
        0x008d 00141 (test1.go:6)       PCDATA  $0, $-1
        0x008d 00141 (test1.go:6)       PCDATA  $2, $-1
        0x008d 00141 (test1.go:6)       CALL    runtime.morestack_noctxt(SB)
        0x0092 00146 (test1.go:6)       JMP     0

По соответствующему номеру строки кода и имени очевидно, что прикладной уровень написалmakeСоответствующий нижний слойmakeslice.

анализ побега

Давайте сначала поговорим о концепции анализа побега. Это связано с проблемами распределения стека и кучи. Если переменная размещена в стеке, она будет автоматически освобождена с окончанием вызова функции, и эффективность выделения очень высока; во-вторых, если она размещена в куче, GC нужно пометить для восстановления. Так называемый эскейп означает, что переменная убегает из стека в кучу (многим непонятно это понятие, они говорят об анализе побега, и встречали несколько интервью 😓).

package main

func main() {
}

func test() *int {
	t := 3
	return &t
}

------

"".test STEXT size=98 args=0x8 locals=0x20
        0x0000 00000 (test1.go:6)       TEXT    "".test(SB), ABIInternal, $32-8
        0x0000 00000 (test1.go:6)       MOVQ    (TLS), CX
        0x0009 00009 (test1.go:6)       CMPQ    SP, 16(CX)
        0x000d 00013 (test1.go:6)       JLS     91
        0x000f 00015 (test1.go:6)       SUBQ    $32, SP
        0x0013 00019 (test1.go:6)       MOVQ    BP, 24(SP)
        0x0018 00024 (test1.go:6)       LEAQ    24(SP), BP
        ... ...
        0x001d 00029 (test1.go:6)       MOVQ    $0, "".~r0+40(SP)
        0x0026 00038 (test1.go:7)       PCDATA  $2, $1
        0x0026 00038 (test1.go:7)       LEAQ    type.int(SB), AX
        0x002d 00045 (test1.go:7)       PCDATA  $2, $0
        0x002d 00045 (test1.go:7)       MOVQ    AX, (SP)
        0x0031 00049 (test1.go:7)       CALL    runtime.newobject(SB) // 堆上分配空间,表示逃逸了
        ... ...

вот если даsliceEscape-анализ с использованием ассемблера не очень интуитивен. Потому что вижу только звонокruntime.makesliceфункция, которая на самом деле вызывается снова внутри функцииruntime.mallocgcфункция, память, выделенная этой функцией, на самом деле является памятью в куче (если памяти в стеке достаточно, вы не увидите правильнуюruntime.maksliceФункция вызова).

Actual go также предоставляет более удобные команды для анализа побегов:go build -gcflags="-m", если вы действительно хотите выполнить escape-анализ, рекомендуется использовать эту команду вместо сборки.

Передать по значению или передать указатель

Про базовые типы в golang: строки, целые числа и булевы, я не буду говорить больше, они должны передаваться по значению, а для структур и указателей это передача по значению или по указателю?

package main

type Student struct {
    name string
    age  int
}

func main() {
    jack := &Student{"jack", 30}
    test(jack)
}

func test(s *Student) *Student {
    return s
}

-------

"".test STEXT nosplit size=20 args=0x10 locals=0x0
        0x0000 00000 (test1.go:14)      TEXT    "".test(SB), NOSPLIT|ABIInternal, $0-16
        ... ...
        0x0000 00000 (test1.go:14)      MOVQ    $0, "".~r1+16(SP) // 初始返回值为0
        0x0009 00009 (test1.go:15)      PCDATA  $2, $1
        0x0009 00009 (test1.go:15)      PCDATA  $0, $1
        0x0009 00009 (test1.go:15)      MOVQ    "".s+8(SP), AX // 将引用地址复制到 AX 寄存器
        0x000e 00014 (test1.go:15)      PCDATA  $2, $0
        0x000e 00014 (test1.go:15)      PCDATA  $0, $2
        0x000e 00014 (test1.go:15)      MOVQ    AX, "".~r1+16(SP) // 将 AX 的引用地址又复制到返回地址
        0x0013 00019 (test1.go:15)      RET

Здесь видно, что в go передается только значение, потому что нижний слой по-прежнему копирует соответствующее значение.

Суммировать

На этом мы заканчиваем сегодняшнюю статью, в основном речь пойдет о нескольких пунктах ниже:

  1. Взаимная кооперация между программными и аппаратными ресурсами компьютера;
  2. GolangНаписанный код, как функции и методы выполняются, в основном при распределении стека и связанных с ним вызовах;
  3. использоватьPlan9Анализируются некоторые общие проблемы.

Я надеюсь, что эта статья поможет вам понять и изучить Go.

использованная литература

Личный общедоступный номер:dayuTalk