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 不是整数值减去 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 寄存器。

SPPC 传统上是物理编号寄存器的别名的机器上,在 Go 汇编器中,名称 SPPC 仍然受到特殊处理;例如,对 SP 的引用需要一个符号,很像 FP。要访问实际的硬件寄存器,请使用真正的 R 名称。例如,在 ARM 架构上,硬件 SPPC 可以分别作为 R13R15 访问。

分支和直接跳转始终写为 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(伪)指令。(如果不是,链接器将附加一条跳转到自身指令;TEXTs 中没有穿透。)在符号之后,参数是标志(见下文)和帧大小,一个常量(但见下文)

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 的偏移量。此函数将使用简单名称 profileloop 从包 runtime 的 Go 源代码中调用。

全局数据符号由一系列初始化 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 应使用名称 ·Syscall,而不是其 TEXT 指令中的等效名称 syscall·Syscall)。对于更复杂的情况,需要明确注释。这些注释使用标准 #include 文件 funcdata.h 中定义的伪指令。

如果函数没有参数和结果,则可以省略指针信息。这通过在 TEXT 指令上使用 $n-0 的参数大小注释来表示。否则,即使不是直接从 Go 调用的汇编函数,也必须通过 Go 源文件中的 Go 原型为函数提供指针信息。(原型还将允许 go vet 检查参数引用。)在函数开始时,假定参数已初始化,但假定结果未初始化。如果结果将在调用指令期间保存活动指针,则函数应首先将结果清零,然后执行伪指令 GO_RESULTS_INITIALIZED。此指令记录结果现在已初始化,并且应在堆栈移动和垃圾回收期间对其进行扫描。通常更容易安排汇编函数不返回指针或不包含调用指令;标准库中没有汇编函数使用 GO_RESULTS_INITIALIZED

如果函数没有本地堆栈帧,则可以省略指针信息。这通过在 TEXT 指令上使用 $0-n 的本地帧大小注释来表示。如果函数不包含调用指令,也可以省略指针信息。否则,本地堆栈帧不得包含指针,并且汇编必须通过执行伪指令 NO_LOCAL_POINTERS 来确认此事实。由于堆栈调整是通过移动堆栈来实现的,因此堆栈指针在任何函数调用期间都可能更改:即使指向堆栈数据的指针也不得保存在局部变量中。

始终应为汇编函数提供 Go 原型,既可以为参数和结果提供指针信息,又可以允许 go vet 检查用于访问它们的偏移量是否正确。

特定于体系结构的详细信息

列出每台机器的所有指令和其他详细信息是不切实际的。要查看为给定机器(例如 ARM)定义的指令,请在位于目录 src/cmd/internal/obj/arm 中的该架构的 obj 支持库的源代码中查找。在该目录中有一个文件 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_tlsget_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