Go 彙編器快速指南

Go 彙編器快速指南

本文件簡要概述了 gc Go 編譯器使用的組合語言的特殊形式。本文件不全面。

彙編器基於 Plan 9 彙編器的輸入樣式,該樣式已在其他地方詳細記錄。如果您打算編寫組合語言,您應該閱讀該文件,儘管其中大部分內容是 Plan 9 特定的。本文件總結了該文件中解釋的語法和差異,並描述了在編寫與 Go 互動的彙編程式碼時適用的特殊性。

關於 Go 彙編器最重要的一點是,它不是底層機器的直接表示。某些細節與機器精確對應,但有些則不對應。這是因為編譯器套件(參見此描述)在通常的流水線中不需要彙編器階段。相反,編譯器操作的是一種半抽象指令集,指令選擇部分發生在程式碼生成之後。彙編器在半抽象形式上工作,因此當您看到像 MOV 這樣的指令時,工具鏈實際為該操作生成的可能根本不是移動指令,也許是清除或載入。或者它可能與具有該名稱的機器指令完全對應。通常,機器特定的操作傾向於以它們自己的形式出現,而更一般的概念,如記憶體移動、子例程呼叫和返回,則更抽象。細節因架構而異,我們對這種不精確性表示歉意;情況並未得到很好的定義。

彙編器程式是一種解析半抽象指令集描述並將其轉換為要輸入給連結器的指令的方式。如果您想了解給定架構(例如 amd64)的彙編指令是什麼樣子,標準庫的原始碼中有很多示例,例如在 runtimemath/big 包中。您還可以檢查編譯器作為彙編程式碼發出的內容(實際輸出可能與您在此處看到的有所不同)

$ cat x.go
package main

func main() {
	println(3)
}
$ GOOS=linux GOARCH=amd64 go tool compile -S x.go        # or: 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	8(SP), BP
	0x003e 00062 (x.go:5)	ADDQ	$16, SP
	0x0042 00066 (x.go:5)	RET
	0x0043 00067 (x.go:5)	NOP
	0x0043 00067 (x.go:3)	PCDATA	$1, $-1
	0x0043 00067 (x.go:3)	PCDATA	$0, $-1
	0x0043 00067 (x.go:3)	CALL	runtime.morestack_noctxt(SB)
	0x0048 00072 (x.go:3)	JMP	0
...

FUNCDATAPCDATA 指令包含供垃圾收集器使用的資訊;它們由編譯器引入。

要檢視連結後二進位制檔案中包含的內容,請使用 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 位整數。這種區別很少重要,但為了避免歧義,當右運算元的高位被設定時,除法或右移操作將被拒絕。

符號

一些符號,例如 R1LR,是預定義的並引用暫存器。確切的集合取決於架構。

有四個預定義的符號引用偽暫存器。這些不是真實的暫存器,而是工具鏈維護的虛擬暫存器,例如幀指標。偽暫存器的集合對於所有架構都是相同的

所有使用者定義的符號都寫成偽暫存器 FP(引數和區域性變數)和 SB(全域性變數)的偏移量。

可以將 SB 偽暫存器看作是記憶體的起點,因此符號 foo(SB) 是記憶體中地址為 foo 的名稱。這種形式用於命名全域性函式和資料。在名稱後新增 <>,如 foo<>(SB),使該名稱僅在當前原始檔中可見,類似於 C 檔案中的頂級 static 宣告。在名稱後新增偏移量指的是從符號地址開始的該偏移量,因此 foo+4(SB)foo 開頭之後的四個位元組。

FP 偽暫存器是一個虛擬幀指標,用於引用函式引數。編譯器維護一個虛擬幀指標,並引用棧上作為該偽暫存器偏移量的引數。因此,0(FP) 是函式的第一個引數,8(FP) 是第二個引數(在 64 位機器上),依此類推。但是,當以這種方式引用函式引數時,需要在開頭放置一個名稱,例如 first_arg+0(FP)second_arg+8(FP)。(偏移量的含義——從幀指標的偏移量——與其在 SB 中的使用不同,在 SB 中它是從符號的偏移量。)彙編器強制執行此約定,拒絕簡單的 0(FP)8(FP)。實際名稱在語義上無關緊要,但應用於記錄引數的名稱。值得強調的是,FP 始終是一個偽暫存器,而不是硬體暫存器,即使在具有硬體幀指標的架構上也是如此。

對於帶有 Go 原型的彙編函式,go vet 將檢查引數名稱和偏移量是否匹配。在 32 位系統上,64 位值的低 32 位和高 32 位透過在名稱後新增 _lo_hi 字尾來區分,例如 arg_lo+0(FP)arg_hi+4(FP)。如果 Go 原型沒有命名其結果,則預期的彙編名稱是 ret

SP 偽暫存器是一個虛擬堆疊指標,用於引用幀區域性變數以及為函式呼叫準備的引數。它指向區域性堆疊幀中的最高地址,因此引用應使用範圍 [−framesize, 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 訪問。

分支和直接跳轉總是寫成 PC 的偏移量,或者寫成跳轉到標籤

label:
	MOVW $0, R1
	JMP label

每個標籤僅在其定義的函式內可見。因此,檔案中允許多個函式定義和使用相同的標籤名稱。直接跳轉和呼叫指令可以以文字符號為目標,例如 name(SB),但不能以符號偏移量為目標,例如 name+4(SB)

指令、暫存器和彙編器指令始終使用大寫字母,以提醒您彙編程式設計是一項充滿挑戰的工作。(例外:ARM 上的 g 暫存器重新命名。)

在 Go 物件檔案和二進位制檔案中,符號的完整名稱是包路徑後跟一個點和符號名稱:fmt.Printfmath/rand.Int。因為彙編器的解析器將點和斜槓視為標點符號,所以這些字串不能直接用作識別符號名稱。相反,彙編器允許在識別符號中使用中點字元 U+00B7 和除號斜槓 U+2215,並將其重寫為普通點和斜槓。在彙編器原始檔中,上面的符號寫為 fmt·Printfmath∕rand·Int。編譯器在使用 -S 標誌時生成的彙編列表直接顯示點和斜槓,而不是彙編器所需的 Unicode 替換。

大多數手寫的彙編檔案不包含符號名稱中的完整包路徑,因為連結器會將當前物件檔案的包路徑插入到任何以點開頭的名稱的開頭:在 math/rand 包實現中的彙編原始檔中,該包的 Int 函式可以被稱為 ·Int。這個約定避免了在自己的原始碼中硬編碼包的匯入路徑的需要,使得程式碼更容易從一個位置移動到另一個位置。

指令

彙編器使用各種指令將文字和資料繫結到符號名稱。例如,這是一個簡單的完整函式定義。TEXT 指令宣告符號 runtime·profileloop,並且後面的指令構成函式體。TEXT 塊中的最後一條指令必須是某種跳轉,通常是 RET(偽)指令。(如果不是,連結器將附加一個跳轉到自身的指令;TEXT 中沒有直通。)在符號之後,引數是標誌(見下文)和幀大小,一個常量(但見下文)

TEXT runtime·profileloop(SB),NOSPLIT,$8
	MOVQ	$runtime·profileloop1(SB), CX
	MOVQ	CX, 0(SP)
	CALL	runtime·externalthreadhandler(SB)
	RET

在一般情況下,幀大小後面是引數大小,由減號分隔。(這不是減法,只是特殊的語法。)幀大小 $24-8 表示該函式有一個 24 位元組的幀,並用 8 位元組的引數呼叫,這些引數位於呼叫者的幀上。如果 TEXT 沒有指定 NOSPLIT,則必須提供引數大小。對於帶有 Go 原型的彙編函式,go vet 將檢查引數大小是否正確。

請注意,符號名稱使用一箇中間點來分隔元件,並指定為從靜態基偽暫存器 SB 的偏移量。該函式將從包 runtime 的 Go 原始碼中呼叫,使用簡單名稱 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<>,一個只讀的 64 位元組表,包含 4 位元組整數值,並宣告 runtime·tlsoffset,一個 4 位元組的隱式清零變數,不包含指標。

指令可以有一個或兩個引數。如果有兩個,第一個是標誌的位掩碼,可以寫成數字表達式,相加或相或,或者可以符號化設定以便於人類理解。它們的值在標準 #include 檔案 textflag.h 中定義,如下所示

特殊指令

PCALIGN 偽指令用於指示下一個指令應透過使用空操作指令填充來對齊到指定邊界。

它目前支援 arm64、amd64、ppc64、loong64 和 riscv64。例如,下面的 MOVD 指令的開始對齊到 32 位元組

PCALIGN $32
MOVD $2, R0

與 Go 型別和常量的互動

如果一個包有任何 .s 檔案,那麼 go build 將指示編譯器發出一個名為 go_asm.h 的特殊標頭檔案,.s 檔案隨後可以 #include。該檔案包含 Go 結構欄位偏移量、Go 結構型別大小以及當前包中定義的大多數 Go const 宣告的符號 #define 常量。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_bufreader_r。因此,如果暫存器 R1 包含指向 reader 的指標,彙編可以引用 r 欄位為 reader_r(R1)

如果這些 #define 名稱中有任何一個含糊不清(例如,一個帶有 _size 欄位的結構體),#include "go_asm.h" 將因“宏重定義”錯誤而失敗。

執行時協調

為了垃圾收集器正確執行,執行時必須知道所有全域性資料和大多數棧幀中指標的位置。Go 編譯器在編譯 Go 原始檔時會發出此資訊,但彙編程式必須顯式定義它。

標記有 NOPTR 標誌(見上文)的資料符號被視為不包含指向執行時分配資料的指標。帶有 RODATA 標誌的資料符號被分配在只讀記憶體中,因此被視為隱式標記為 NOPTR。總大小小於指標的資料符號也被視為隱式標記為 NOPTR。無法在彙編原始檔中定義包含指標的符號;此類符號必須在 Go 原始檔中定義。即使沒有 DATAGLOBL 指令,彙編源仍然可以透過名稱引用該符號。一個好的經驗法則是,在 Go 中而不是在彙編中定義所有非 RODATA 符號。

每個函式還需要註釋,以提供其引數、結果和區域性堆疊幀中活動指標的位置。對於沒有指標結果且沒有區域性堆疊幀或沒有函式呼叫的彙編函式,唯一的要求是在同一包的 Go 原始檔中為該函式定義一個 Go 原型。彙編函式的名稱不能包含包名稱元件(例如,syscall 包中的函式 Syscall 在其 TEXT 指令中應使用名稱 ·Syscall,而不是等效的名稱 syscall·Syscall)。對於更復雜的情況,需要顯式註釋。這些註釋使用標準 #include 檔案 funcdata.h 中定義的偽指令。

如果函式沒有引數且沒有結果,則可以省略指標資訊。這由 TEXT 指令上的引數大小注釋 $n-0 表示。否則,即使對於未直接從 Go 呼叫的彙編函式,也必須透過 Go 原始檔中的函式原型提供指標資訊。(原型還將允許 go vet 檢查引數引用。)在函式開始時,假設引數已初始化,但結果未初始化。如果結果將在呼叫指令期間持有活動指標,則函式應首先將結果清零,然後執行偽指令 GO_RESULTS_INITIALIZED。此指令記錄結果現在已初始化,並應在堆疊移動和垃圾收集期間進行掃描。通常,最好安排彙編函式不返回指標或不包含呼叫指令;標準庫中沒有彙編函式使用 GO_RESULTS_INITIALIZED

如果函式沒有區域性堆疊幀,則可以省略指標資訊。這由 TEXT 指令上區域性幀大小注釋 $0-n 表示。如果函式不包含呼叫指令,也可以省略指標資訊。否則,區域性堆疊幀不得包含指標,並且彙編必須透過執行偽指令 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。列舉主要按字母順序排列。(在 cmd/internal/obj 包中定義的與架構無關的 AXXX 表示無效指令)。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 結構的指標透過 MMU 中一個(就 Go 而言)未使用的暫存器的值來維護。在執行時包中,彙編程式碼可以包含 go_tls.h,它定義了一個作業系統和架構相關的宏 get_tls 用於訪問此暫存器。get_tls 宏接受一個引數,該引數是將 g 指標載入到的暫存器。

例如,使用 CX 載入 gm 的序列如下所示

#include "go_tls.h"
#include "go_asm.h"
...
get_tls(CX)
MOVL	g(CX), AX     // Move g into AX.
MOVL	g_m(AX), BX   // Move g.m into BX.

get_tls 宏也在 amd64 上定義。

定址模式

當使用編譯器和彙編器的 -dynlink-shared 模式時,對固定記憶體位置(例如全域性變數)的任何載入或儲存都必須假定會覆蓋 CX。因此,為了安全地與這些模式一起使用,彙編源通常應避免使用 CX,除非在記憶體引用之間。

64 位 Intel 386 (又名 amd64)

在彙編器級別,這兩種架構的行為大致相同。64 位版本上訪問 mg 指標的彙編程式碼與 32 位 386 上相同,只是它使用 MOVQ 而不是 MOVL

get_tls(CX)
MOVQ	g(CX), AX     // Move g into AX.
MOVQ	g_m(AX), BX   // Move g.m into BX.

暫存器 BP 是被呼叫者儲存的。當幀大小大於零時,彙編器會自動插入 BP 儲存/恢復。允許將 BP 用作通用暫存器,但這可能會干擾基於取樣的效能分析。

ARM

暫存器 R10R11 由編譯器和連結器保留。

R10 指向 g(goroutine)結構體。在彙編原始碼中,此指標必須稱為 g;名稱 R10 不被識別。

為了方便人們和編譯器編寫彙編,ARM 連結器允許使用通用定址形式和偽操作,例如 DIVMOD,這些可能無法用單個硬體指令表示。它將這些形式實現為多條指令,通常使用 R11 暫存器來儲存臨時值。手寫的彙編可以使用 R11,但這樣做需要確保連結器也沒有使用它來實現函式中的任何其他指令。

定義 TEXT 時,指定幀大小 $-4 告訴連結器這是一個葉子函式,不需要在入口處儲存 LR

名稱 SP 始終指代前面描述的虛擬棧指標。對於硬體暫存器,請使用 R13

條件碼語法是在指令後附加一個點和一個或兩個字母的程式碼,例如 MOVW.EQ。可以附加多個程式碼:MOVM.IA.W。程式碼修飾符的順序無關緊要。

定址模式

ARM64

R18 是“平臺暫存器”,在 Apple 平臺上保留。為了防止意外濫用,該暫存器命名為 R18_PLATFORMR27R28 由編譯器和連結器保留。R29 是幀指標。R30 是連結暫存器。

指令修飾符在指令後以句點附加。唯一修飾符是 P(後增量)和 W(預增量):MOVW.PMOVW.W

定址模式

參考:Go ARM64 彙編指令參考手冊

PPC64

此彙編器用於 GOARCH 值 ppc64 和 ppc64le。

參考:Go PPC64 彙編指令參考手冊

IBM z/Architecture,又名 s390x

暫存器 R10R11 是保留的。彙編器在彙編某些指令時使用它們來儲存臨時值。

R13 指向 g (goroutine) 結構。該暫存器必須稱為 g;名稱 R13 不被識別。

R15 指向堆疊幀,通常應僅使用虛擬暫存器 SPFP 訪問。

載入和儲存多指令對一系列暫存器進行操作。暫存器範圍由起始暫存器和結束暫存器指定。例如,LMG (R9), R5, R7 將分別用 0(R9)8(R9)16(R9) 處的 64 位值載入 R5R6R7

儲存-儲存指令,如 MVCXC,將長度作為第一個引數寫入。例如,XC $8, (R9), (R9) 將清除 R9 中指定地址的八個位元組。

如果向量指令將長度或索引作為引數,則它將是第一個引數。例如,VLEIF $1, $16, V2 將值 16 載入到 V2 的索引 1 中。使用向量指令時應注意確保它們在執行時可用。要使用向量指令,機器必須同時具有向量功能(功能列表中的第 129 位)和核心支援。如果沒有核心支援,向量指令將無效(它將等同於 NOP 指令)。

定址模式

MIPS, MIPS64

通用暫存器名為 R0R31,浮點暫存器為 F0F31

R30 保留用於指向 gR23 用作臨時暫存器。

TEXT 指令中,MIPS 的幀大小 $-4 或 MIPS64 的幀大小 $-8 指示連結器不要儲存 LR

SP 指的是虛擬堆疊指標。對於硬體暫存器,請使用 R29

定址模式

GOMIPS 環境變數的值(hardfloatsoftfloat)透過預定義 GOMIPS_hardfloatGOMIPS_softfloat 提供給彙編程式碼。

GOMIPS64 環境變數的值(hardfloatsoftfloat)透過預定義 GOMIPS64_hardfloatGOMIPS64_softfloat 提供給彙編程式碼。

不支援的操作碼

彙編器旨在支援編譯器,因此並非所有硬體指令都為所有架構定義:如果編譯器不生成它,它可能就不存在。如果您需要使用缺失的指令,有兩種方法。一種是更新彙編器以支援該指令,這很簡單,但只有當該指令可能再次使用時才值得。對於簡單的一次性情況,可以使用 BYTEWORD 指令將顯式資料放入 TEXT 中的指令流。以下是 386 執行時定義 64 位原子載入函式的方式。

// uint64 atomicload64(uint64 volatile* addr);
// so actually
// 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