Глубокая расшифровка небезопасного языка Go

Go

ПредыдущийстатьяМы подробно проанализировали базовую реализацию map.Если вы также читали исходный код, вы должны быть правы.unsafe.PointerНе чужой, карта широко используется при поиске ключа.

unsafe.Pointerродыunsafe 包, в этой статье мы подробно рассмотрим небезопасный пакет. Позвольте мне сначала объяснить, что эта статья не такая длинная, как раньше, вы можете прочитать ее относительно легко, и таких случаев не так уж много.

В последний раз, когда я публиковал статью, в том числе код с более чем 5w словами, опыт работы с фоновым редактором был очень плохим, что однажды заставило меня усомниться в своей жизни. Как я уже говорил, такой длинный текст, как карта, вероятно, можно прочитать не более чем1 %. Комментарии, подобные следующим ученикам, встречаются редко.

wechat

Лично я считаю, что само по себе обучение — дело непростое и приятное, а обучать и развлекать — хорошее желание. Если вы хотите глубоко понять, вы должны приложить усилия, невидимые для других. Учиться никогда не бывает легко, скука — это нормально. Наберитесь терпения, глубоко изучите тот или иной вопрос, читайте книги, читайте статьи, пишите блоги и будьте сосредоточенным человеком в бурные времена!

тип указателя

Прежде чем мы официально представим пакет unsafe, нам нужно сосредоточиться на типах указателей в языке Go.

Когда я начал программировать будучи студентом, моим первым языком был Си. После этого я один за другим выучил C++, Java и Python, эти языки довольно мощные, но они не такие «простые», как язык Си. Только когда я начал осваивать язык Go, я снова обрел это чувство. Кен Томпсон, один из авторов языка Go, также является автором языка C. Таким образом, Go можно рассматривать как язык, основанный на C, и многие его функции аналогичны C, и указатели являются одной из них.

Однако указатели языка Go имеют много ограничений по сравнению с указателями C. Конечно, это из соображений безопасности, если вы знаете современные языки, такие как Java/Python, из-за страха перед ошибками программиста, какие могут быть указатели (здесь речь идет о явных указателях)? Не говоря уже о необходимости для программистов очищать "мусор" вроде C/C++. Поэтому для Go удобно иметь указатели, несмотря на многочисленные ограничения.

Зачем нужны типы указателей? Ссылка go101.org приводит такой пример:

package main

import "fmt"

func double(x int) {
	x += x
}

func main() {
	var a = 3
	double(a)
	fmt.Println(a) // 3
}

Очень просто, я хочу удвоить a в функции double, но функция в примере не может этого сделать. Почему? Поскольку все функциональные параметры языка Go值传递. x в функции double — это просто копия фактического параметра a, и операция x внутри функции не может быть возвращена фактическому параметру a.

Если в это время есть указатель на решение проблемы! Это также наш часто используемый «трюк».

package main

import "fmt"

func double(x *int) {
	*x += *x
	x = nil
}

func main() {
	var a = 3
	double(&a)
	fmt.Println(a) // 6
	
	p := &a
	double(p)
	fmt.Println(a, p == nil) // 12 false
}

Очень распространенная операция, объяснять не нужно. Единственное, что может сбить с толку, это фраза:

x = nil

Нужно немного подумать, чтобы сделать вывод, что эта строка кода никак на него не влияет. Поскольку он передается по значению, x также является просто копией &a.

*x += *x

Это предложение удваивает значение, на которое указывает x (то есть значение, на которое указывает &a, являющееся переменной a). Но операции над самим x (указателем) не влияют на внешний a, поэтомуx = nilНельзя создавать большие волны.

Следующая картина может быть «самовиновной»:

pointer copy

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

Ограничение первое:Go 的指针不能进行数学运算.

Давайте посмотрим на простой пример:

a := 5
p := &a

p++
p = &a + 3

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

Ограничение второе:不同类型的指针不能相互转换.

Возьмем следующий короткий пример:

func main() {
	a := int(100)
	var f *float64
	
	f = &a
}

Также будет сообщено об ошибке компиляции:

cannot use &a (type *int) as type *float64 in assignment

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

Ограничение три:不同类型的指针不能使用 == 或 != 比较.

Два указателя можно сравнивать, только если они одного типа или могут быть преобразованы друг в друга. Кроме того, указатели могут передаваться через==а также!=прямо иnilв сравнении с.

Ограничение четыре:不同类型的指针变量不能相互赋值.

Это то же самое, что и третье ограничение.

что небезопасно

Вышеупомянутый указатель типобезопасен, но имеет много ограничений. В Go также есть небезопасные указатели, что небезопасно. Указатель, предоставляемый пакетом unsafe. В некоторых случаях это делает код более эффективным и, конечно же, более опасным.

Пакет unsafe используется компилятором Go и используется на этапе компиляции. Как видно из названия, он не безопасен и официально не рекомендуется к использованию. У меня неприятное ощущение при использовании небезопасного пакета, возможно, это замысел разработчиков языка.

Но как может высокоуровневый Gopher не использовать небезопасный пакет? Он может обходить систему типов языка Go и напрямую манипулировать памятью. Например, обычно мы не можем манипулировать неэкспортированными элементами структуры, но можем сделать это с помощью пакета unsafe. Пакет unsafe позволяет мне напрямую читать и записывать память, независимо от того, что вы экспортируете или нет.

почему небезопасно

Система типов языка Go разработана для безопасности и эффективности, и иногда безопасность может привести к неэффективности. С помощью пакета unsafe продвинутые программисты могут использовать его для обхода неэффективности системы типов. Поэтому он имеет смысл существовать, и, читая исходный код Go, вы найдете множество примеров использования пакета unsafe.

Небезопасный принцип реализации

Давайте посмотрим на исходный код:

type ArbitraryType int

type Pointer *ArbitraryType

Что касается именования,ArbitraryЭто означает произвольный, то есть Pointer может указывать на любой тип, на самом деле это похоже на язык Cvoid*.

Пакет unsafe имеет еще три функции:

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

SizeofВозвращает количество байтов, занятых типом x, исключая размер содержимого, на которое указывает x. Например, для указателя размер, возвращаемый функцией, равен 8 байтам (на 64-разрядных машинах), а размер слайса равен размеру заголовка слайса.

OffsetofВозвращает количество байтов от начала структуры в ячейке памяти члена структуры.Переданный параметр должен быть членом структуры.

AlignofВозвратите m, где m означает, что когда тип выровнен по памяти, адрес памяти, который он выделяет, делится на m.

Обратите внимание, что все результаты, возвращаемые тремя вышеупомянутыми функциями, являются типами uintptr, которые можно преобразовать в и из unsafe.Pointer. Все три функции выполняются во время компиляции, и их результаты могут быть непосредственно назначеныconst 型变量. Кроме того, поскольку результаты выполнения трех функций связаны с операционной системой и компилятором, они не переносимы.

Подводя итог, можно сказать, что пакет unsafe предоставляет две важные возможности:

  1. Указатели любого типа и unsafe.Pointer конвертируются друг в друга.
  2. Тип uintptr и unsafe.Pointer могут быть преобразованы друг в друга.

type pointer uintptr

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

// uintptr 是一个整数类型,它足够大,可以存储
type uintptr uintptr

Еще один момент, который следует отметить, заключается в том, что uintptr не имеет семантики указателя, а это означает, что объект, на который указывает uintptr, будет безжалостно возвращен gc. И unsafe.Pointer имеет семантику указателя, которая может защитить объект, на который он указывает, от сборки мусора, когда он «полезен».

Несколько функций в пакете unsafe выполняются при компиляции, ведь компилятор «знает» эти операции по выделению памяти. существует/usr/local/go/src/cmd/compile/internal/gc/unsafe.goПод путем вы можете увидеть, как Go обрабатывает функции в небезопасном пакете во время компиляции.

Более глубокий принцип требует изучения исходного кода компилятора, поэтому я не буду вдаваться в него здесь. Давайте сосредоточимся на его использовании и посмотрим ниже.

Как использовать небезопасно

Получить длину среза

Через предыдущий раздел на срезестатья, мы знаем определение структуры заголовка слайса:

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指针
    len   int // 长度 
    cap   int // 容量
}

Вызовите функцию make, чтобы создать новый слайс, нижний слой вызывает функцию makeslice и возвращает структуру слайса:

func makeslice(et *_type, len, cap int) slice

Таким образом, мы можем преобразовать через unsafe.Pointer и uintptr, чтобы получить значение поля slice.

func main() {
	s := make([]int, 9, 20)
	var Len = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8)))
	fmt.Println(Len, len(s)) // 9 9

	var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))
	fmt.Println(Cap, cap(s)) // 20 20
}

Процесс преобразования Len и cap выглядит следующим образом:

Len: &s => pointer => uintptr => pointer => *int => int
Cap: &s => pointer => uintptr => pointer => *int => int

получить длину карты

Давайте взглянем на карту, о которой мы говорили в предыдущей статье:

type hmap struct {
	count     int
	flags     uint8
	B         uint8
	noverflow uint16
	hash0     uint32

	buckets    unsafe.Pointer
	oldbuckets unsafe.Pointer
	nevacuate  uintptr

	extra *mapextra
}

В отличие от slice функция makemap возвращает указатель на hmap, обратите внимание на указатель:

func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap

Мы по-прежнему можем конвертировать через unsafe.Pointer и uintptr, чтобы получить значение поля hamp, но теперь count становится вторичным указателем:

func main() {
	mp := make(map[string]int)
	mp["qcrao"] = 100
	mp["stefno"] = 18

	count := **(**int)(unsafe.Pointer(&mp))
	fmt.Println(count, len(mp)) // 2 2
}

Процесс преобразования count:

&mp => pointer => **int => int

Приложение в исходном коде карты

В исходном коде карты функции mapaccess1, mapasign и mapdelete должны найти положение ключа, и ключ будет хеширован первым.

Например:

b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))

h.bucketsЯвляетсяunsafe.Pointer, преобразовать его вuintptr, затем добавьте(hash&m)*uintptr(t.bucketsize), результат сложения двух снова преобразуется вunsafe.Pointer, и, наконец, преобразовать вbmap 指针, получить положение ведра, куда падает ключ. Если вы не знакомы с этой формулой, вы можете прочитать предыдущую статью, в которой легко разобраться.

Приведенный выше пример относительно прост, давайте рассмотрим немного более сложный пример присваивания:

// store new key/value at insert position
if t.indirectkey {
	kmem := newobject(t.key)
	*(*unsafe.Pointer)(insertk) = kmem
	insertk = kmem
}
if t.indirectvalue {
	vmem := newobject(t.elem)
	*(*unsafe.Pointer)(val) = vmem
}

typedmemmove(t.key, insertk, key)

Этот код выполняет операцию «присваивания» после нахождения позиции, в которую должен быть вставлен ключ. insertk и val представляют собой адрес, по которому должны быть «помещены» ключ и значение соответственно. Если t.indirectkey истинно, это означает, что указатель ключа хранится в ведре, поэтому вставку нужно рассматривать как指针的指针, так что значение соответствующей позиции в корзине может быть установлено равным значению адреса, указывающему на реальный ключ, то есть ключ хранит указатель.

На следующем рисунке показана вся операция установки ключа:

map assign

obj - это место, где хранится настоящий ключ. Рисунок №4, obj указывает на то, что выполнение завершеноtypedmemmoveПосле того, как функция будет успешно назначена.

Offsetof Получить смещение члена

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

Вот факт о распределении памяти: структуре выделяется непрерывный участок памяти, и адрес структуры также представляет собой адрес первого члена.

Давайте посмотрим на пример:

package main

import (
	"fmt"
	"unsafe"
)

type Programmer struct {
	name string
	language string
}

func main() {
	p := Programmer{"stefno", "go"}
	fmt.Println(p)
	
	name := (*string)(unsafe.Pointer(&p))
	*name = "qcrao"

	lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language)))
	*lang = "Golang"

	fmt.Println(p)
}

Запустив код, вывод:

{stefno go}
{qcrao Golang}

name является первым элементом структуры, поэтому &p можно преобразовать непосредственно в *string. Это тот же принцип, который использовался при получении члена карты count ранее.

Для закрытых членов структур теперь есть способ изменить их значение с помощью unsafe.Pointer .

Я обновил структуру Programmer, добавив еще одно поле:

type Programmer struct {
	name string
	age int
	language string
}

И поместите его в другие пакеты, чтобы в основной функции три его поля были закрытыми переменными-членами и не могли быть изменены напрямую. Но я могу получить размер члена с помощью функции unsafe.Sizeof(), а затем вычислить адрес члена и напрямую изменить память.

func main() {
	p := Programmer{"stefno", 18, "go"}
	fmt.Println(p)

	lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(string(""))))
	*lang = "Golang"

	fmt.Println(p)
}

вывод:

{stefno 18 go}
{stefno 18 Golang}

Преобразование между строкой и фрагментом

Это очень классический пример. Реализовать преобразование между строковыми и байтовыми срезами, требованияzero-copy. Подумайте об этом, в общем случае вам нужно пройти по срезу строки или байтов, а затем присвоить значения одно за другим.

Чтобы выполнить эту задачу, нам нужно понять базовые структуры данных срезов и строк:

type StringHeader struct {
	Data uintptr
	Len  int
}

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

Выше показана структура пакета отражения, путь: src/reflect/value.go. Этого можно достичь, просто поделившись базовым массивом []bytezero-copy.

func string2bytes(s string) []byte {
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

	bh := reflect.SliceHeader{
		Data: stringHeader.Data,
		Len:  stringHeader.Len,
		Cap:  stringHeader.Len,
	}

	return *(*[]byte)(unsafe.Pointer(&bh))
}

func bytes2string(b []byte) string{
	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))

	sh := reflect.StringHeader{
		Data: sliceHeader.Data,
		Len:  sliceHeader.Len,
	}

	return *(*string)(unsafe.Pointer(&sh))
}

Код относительно прост и подробно объясняться не будет. Преобразование между строковым и байтовым фрагментом путем создания заголовка фрагмента и заголовка строки.

Суммировать

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

Пакет unsafe определяет Pointer и три функции:

type ArbitraryType int
type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

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

uintptr можно преобразовать в unsafe.Pointer и обратно, а uintptr можно использовать для математических операций. Таким образом, с помощью комбинации uintptr и unsafe.Pointer устраняется ограничение, заключающееся в том, что указатели Go не могут выполнять математические операции.

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

Кстати, после частого использования пакета unsafe я уже не считаю его имя таким уж «красивым». Наоборот, поскольку он использует то, что официально не пропагандируется, он кажется немного крутым. Вот что такое бунт.

Наконец, нажмитечитать оригинал, вы станете свидетелями роста тысячезвездного проекта, вы это заслужили!

QR

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

【Безжалостный блог Fixue】woohoo.fly snow.org/2017/07/06/…

【Перевод сведений о небезопасном пакете】перейти cn.VIP/вопрос/37…

【Официальный документ】golang.org/pkg/unsafe/

【пример】у-у-у-у, оп второй. info/go wave_UN SA…

【Блог босса жареной рыбы】сегмент fault.com/ah/119000001…

【Языковая Библия】У-у-у. Видите лазейки. Способность/сила волшебника…

【указатель и системные вызовы】blog.gopher academy.com/advent-2017…

【указатель и uintptr】No.OSCHINA.net/Уверенность брат тоже...

【небезопасный.указатель】go101.org/article/ООН…

[тип указателя]go101.org/article/POI…

[Дыра в коде, быстро выучить язык Go небезопасно]nuggets.capable/post/684490…

【Официальный документ】golang.org/pkg/unsafe/

【яшмовое гнездо】у-у-у-у, оп второй. info/go wave_UN SA…