亭亭五月天在线观看,亭亭五月天在线观看,国产最新av一区二区,国产 高清 中文字幕,99re热久久亚洲综合精品成人,熟妇 一区二区三区,一级做a爰片性色毛片武则天,美女的骚穴视频播放,国产美女午夜免费视频

首頁(yè)>國(guó)內(nèi) > 正文

Go 匯編詳解

2023-04-17 19:15:01來(lái)源:字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)

1、 Go 匯編基礎(chǔ)知識(shí)1.1、通用寄存器

不同體系結(jié)構(gòu)的 CPU,其內(nèi)部寄存器的數(shù)量、種類以及名稱可能大不相同,這里我們只介紹 AMD64 的寄存器。AMD64 有 20 多個(gè)可以直接在匯編代碼中使用的寄存器,其中有幾個(gè)寄存器在操作系統(tǒng)代碼中才會(huì)見(jiàn)到,而應(yīng)用層代碼一般只會(huì)用到如下三類寄存器。


【資料圖】

上述這些寄存器除了段寄存器是 16 位的,其它都是 64 位的,也就是 8 個(gè)字節(jié),其中的 16 個(gè)通用寄存器還可以作為 32/16/8 位寄存器使用,只是使用時(shí)需要換一個(gè)名字,比如可以用 EAX 這個(gè)名字來(lái)表示一個(gè) 32 位的寄存器,它使用的是 RAX 寄存器的低 32 位。

AMD64 的通用通用寄存器的名字在 plan9 中的對(duì)應(yīng)關(guān)系:

AMD64

RAX

RBX

RCX

RDX

RDI

RSI

RBP

RSP

R8

R9

R10

R11

R12

R13

R14

RIP

Plan9

AX

BX

CX

DX

DI

SI

BP

SP

R8

R9

R10

R11

R12

R13

R14

PC

Go 語(yǔ)言中寄存器一般用途:

1.2、偽寄存器

偽寄存器是 plan9 偽匯編中的一個(gè)助記符, 也是 Plan9 比較有個(gè)性的語(yǔ)法之一。常見(jiàn)偽寄存器如下表所示:

SB:指向全局符號(hào)表。相對(duì)于寄存器,SB 更像是一個(gè)聲明標(biāo)識(shí),用于標(biāo)識(shí)全局變量、函數(shù)等。通過(guò) symbol(SB) 方式使用,symbol<>(SB)表示 symbol 只在當(dāng)前文件可見(jiàn),跟 C 中的 static 效果類似。此外可以在引用上加偏移量,如 symbol+4(SB) 表示 symbol+4bytes 的地址。

PC:程序計(jì)數(shù)器(Program Counter),指向下一條要執(zhí)行的指令的地址,在 AMD64 對(duì)應(yīng) rip 寄存器。個(gè)人覺(jué)得,把他歸為偽寄存器有點(diǎn)令人費(fèi)解,可能是因?yàn)槊總€(gè)平臺(tái)對(duì)應(yīng)的物理寄存器名字不一樣。

SP:SP 寄存器比較特殊,既可以當(dāng)做物理寄存器也可以當(dāng)做偽寄存器使用,不過(guò)這兩種用法的使用語(yǔ)法不同。其中,偽寄存器使用語(yǔ)法是 symbol+offset(SP),此場(chǎng)景下 SP 指向局部變量的起始位置(高地址處);x-8(SP) 表示函數(shù)的第一個(gè)本地變量;物理 SP(硬件SP) 的使用語(yǔ)法則是 +offset(SP),此場(chǎng)景下 SP 指向真實(shí)棧頂?shù)刂罚畹偷刂诽帲?/p>

FP:用于標(biāo)識(shí)函數(shù)參數(shù)、返回值。被調(diào)用者(callee)的 FP 實(shí)際上是調(diào)用者(caller)的棧頂,即 callee.SP(物理SP) == caller.FP;x+0(FP) 表示第一個(gè)請(qǐng)求參數(shù)(參數(shù)返回值從右到左入棧)。

實(shí)際上,生成真正可執(zhí)行代碼時(shí),偽 SP、FP 會(huì)由物理 SP 寄存器加上偏移量替換。所以執(zhí)行過(guò)程中修改物理 SP,會(huì)引起偽 SP、FP 同步變化,比如執(zhí)行 SUBQ $16, SP 指令后,偽 SP 和偽 FP 都會(huì) -16。而且,反匯編二進(jìn)制而生成的匯編代碼中,只有物理 SP 寄存器。即 go tool objdump/go tool compile -S 輸出的匯編代碼中,沒(méi)有偽 SP 和 偽 FP 寄存器,只有物理 SP 寄存器。

另外還有 1 個(gè)比較特殊的偽寄存器:TLS:存儲(chǔ)當(dāng)前 goroutine 的 g 結(jié)構(gòu)體的指針。實(shí)際上,X86 和 AMD64 下的 TLS 是通過(guò)段寄存器 FS 或 GS 實(shí)現(xiàn)的線程本地存儲(chǔ)基地址,而當(dāng)前 g 的指針是線程本地存儲(chǔ)的第一個(gè)變量。

比如 github.com/petermattis/goid.Get 函數(shù)的匯編實(shí)現(xiàn)如下:

// func Get() int64TEXT ·Get(SB),NOSPLIT,$0-8    MOVQ (TLS), R14    MOVQ g_goid(R14), R13    MOVQ R13, ret+0(FP)    RET

編譯成二進(jìn)制之后,再通過(guò) go tool objdump 反編譯成匯編(Go 1.18),得到如下代碼:

TEXT github.com/petermattis/goid.Get.abi0(SB) /Users/bytedance/go/pkg/mod/github.com/petermattis/goid@v0.0.0-20221215004737-a150e88a970d/goid_go1.5_amd64.s  goid_go1.5_amd64.s:28 0x108adc0   654c8b342530000000  MOVQ GS:0x30, R14   goid_go1.5_amd64.s:29 0x108adc9   4d8bae98000000    MOVQ 0x98(R14), R13   goid_go1.5_amd64.s:30 0x108add0   4c896c2408    MOVQ R13, 0x8(SP)   goid_go1.5_amd64.s:31 0x108add5   c3      RET

可以知道 MOVQ (TLS), R14 指令最終編譯成了 MOVQ GS:0x30, R14 ,使用了 GS 段寄存器實(shí)現(xiàn)相關(guān)功能。

操作系統(tǒng)對(duì)內(nèi)存的一般劃分如下圖所示:

高地址 +------------------+        |                  |        |     內(nèi)核空間      |        |                  |        --------------------        |                  |        |       棧         |        |                  |        --------------------        |                  |        |     .......      |        |                  |        --------------------        |                  |        |       堆         |        |                  |        --------------------        |     全局?jǐn)?shù)據(jù)      |        |------------------|        |                  |        |     靜態(tài)代碼      |        |                  |        |------------------|        |     系統(tǒng)保留      |  低地址 |------------------|

這里提個(gè)疑問(wèn),我們知道協(xié)程分為有棧協(xié)程和無(wú)棧協(xié)程,go 語(yǔ)言是有棧協(xié)程。那你知道普通 gorutine 的調(diào)用棧是在哪個(gè)內(nèi)存區(qū)嗎?

1.3、函數(shù)調(diào)用棧幀

我們先熟悉幾個(gè)名詞。

caller:函數(shù)調(diào)用者。callee:函數(shù)被調(diào)用者。比如函數(shù) main 中調(diào)用 sum 函數(shù),那么 main 就是 caller,而 sum 函數(shù)就是 callee。棧幀:stack frame,即執(zhí)行中的函數(shù)所持有的、獨(dú)立連續(xù)的棧區(qū)段。一般用來(lái)保存函數(shù)參數(shù)、返回值、局部變量、返回 PC 值等信息。golang 的 ABI 規(guī)定,由 caller 管理函數(shù)參數(shù)和返回值。

下圖是 golang 的調(diào)用棧,源于曹春暉老師的 github 文章《匯編 is so easy》 ,做了簡(jiǎn)單修改:

caller                                                                                                                  +------------------+                                                                                                          |                  |                                                                                +---------------------->  +------------------+                                                                               |                         |                  |                                                                                |                         | caller parent BP |                                                                                |           BP(pseudo SP) +------------------+                                                                                |                         |                  |                                                                                |                         |   Local Var0     |                                                                                |                         +------------------+                                                                                |                         |                  |                                                                                |                         |   .......        |                                                                                |                         +------------------+                                                                                |                         |                  |                                                                                |                         |   Local VarN     |                                                                                                          +------------------+                                                                          caller stack frame              |                  |                                                                                                          |   callee arg2    |                                                                                |                         +------------------+                                                                                |                         |                  |                                                                                |                         |   callee arg1    |                                                                                |                         +------------------+                                                                                |                         |                  |                                                                                |                         |   callee arg0    |                                                                                |   SP(Real Register) ->  +------------------+--------------------------+   FP(virtual register)                              |                         |                  |                          |                                                     |                         |   return addr    |  parent return address   |                                                     +---------------------->  +------------------+--------------------------+    <-----------------------+                                                             |  caller BP               |                            |                                                             |  (caller frame pointer)  |                            |                                              BP(pseudo SP)  +--------------------------+                            |                                                             |                          |                            |                                                             |     Local Var0           |                            |                                                             +--------------------------+                            |                                                             |                          |                                                                                          |     Local Var1           |                                                                                          +--------------------------+                    callee stack frame                                                    |                          |                                                                                          |       .....              |                                                                                          +--------------------------+                            |                                                             |                          |                            |                                                             |     Local VarN           |                            |              High                         SP(Real Register) +--------------------------+                            |               ^                                             |                          |                            |               |                                             |                          |                            |               |                                             |                          |                            |               |                                             |                          |                            |               |                                             |                          |                            |               |                                             +--------------------------+    <-----------------------+              Low                                                                                                                                                                                    callee

需要指出的是,上圖中的 CALLER BP 是在編譯期由編譯器在符合條件時(shí)自動(dòng)插入。所以手寫(xiě)匯編時(shí),計(jì)算 framesize 時(shí)不應(yīng)包括 CALLER BP 的空間。是否插入 CALLER BP 的主要判斷依據(jù)如下:

// Must agree with internal/buildcfg.FramePointerEnabled.const framepointer_enabled = GOARCH == "amd64" || GOARCH == "arm64"

以下是 Go 語(yǔ)言函數(shù)棧展開(kāi)邏輯的一段代碼,它側(cè)面驗(yàn)證了 BP 插入的條件:

函數(shù)的棧幀大小大于 0;常量 framepointer_enabled 值為 true。
// For architectures with frame pointers, if there"s  // a frame, then there"s a saved frame pointer here.  //  // NOTE: This code is not as general as it looks.  // On x86, the ABI is to save the frame pointer word at the  // top of the stack frame, so we have to back down over it.  // On arm64, the frame pointer should be at the bottom of  // the stack (with R29 (aka FP) = RSP), in which case we would  // not want to do the subtraction here. But we started out without  // any frame pointer, and when we wanted to add it, we didn"t  // want to break all the assembly doing direct writes to 8(RSP)  // to set the first parameter to a called function.  // So we decided to write the FP link *below* the stack pointer  // (with R29 = RSP - 8 in Go functions).  // This is technically ABI-compatible but not standard.  // And it happens to end up mimicking the x86 layout.  // Other architectures may make different decisions.  if frame.varp > frame.sp && framepointer_enabled {    frame.varp -= goarch.PtrSize  }
// Must agree with internal/buildcfg.FramePointerEnabled.const framepointer_enabled = GOARCH == "amd64" || GOARCH == "arm64"
1.4、golang常用匯編指令

參考文檔:

Go支持的 X86 指令

https://github.com/golang/arch/blob/v0.2.0/x86/x86.csv

Go支持的 ARM64 指令

https://github.com/golang/arch/blob/v0.2.0/arm64/arm64asm/inst.json

Go支持的 ARM 指令

https://github.com/golang/arch/blob/v0.2.0/arm/arm.csv?

常用指令:

例如

MOVB $1, DI     // 1 byte; 將 DI 的第一個(gè) Byte 的值設(shè)置為 1MOVW $0x10, BX  // 2bytesMOVD $1, DX     // 4 bytesMOVQ $-10, AX   // 8 bytesSUBQ $0x18, SP  //對(duì)SP做減法,擴(kuò)棧ADDQ $0x18, SP  //對(duì)SP做加法,縮棧ADDQ AX, BX     // BX += AXSUBQ AX, BX     // BX -= AXIMULQ AX, BX    // BX *= AXJMP addr        // 跳轉(zhuǎn)到地址,地址可為代碼中的地址,不過(guò)實(shí)際上手寫(xiě)一般不會(huì)出現(xiàn)JMP label       // 跳轉(zhuǎn)到標(biāo)簽,可以跳轉(zhuǎn)到同一函數(shù)內(nèi)的標(biāo)簽位置JMP 2(PC)       // 向前轉(zhuǎn)2行JMP -2(PC)      // 向后跳轉(zhuǎn)2行JNZ target      // 如果zero flag被set過(guò),則跳轉(zhuǎn)

常用標(biāo)志位:

1.5 全局變量

參考文檔:《Go語(yǔ)言高級(jí)編程》的章節(jié) 3.3 常量和全局變量

??https://github.com/chai2010/advanced-go-programming-book/blob/master/ch3-asm/ch3-03-const-and-var.md??

1.5.1 使用語(yǔ)法

使用 GLOBL 關(guān)鍵字聲明全局變量,用 DATA 定義指定內(nèi)存的值:

// DATA 匯編指令指定對(duì)應(yīng)內(nèi)存中的值; width 必須是 1、2、4、8 幾個(gè)寬度之一DATA    symbol+offset(SB)/width, value // symbol+offset 偏移量,width 寬度, value 初始值// GLOBL 指令聲明一個(gè)變量對(duì)應(yīng)的符號(hào),以及變量對(duì)應(yīng)的內(nèi)存大小GLOBL symbol(SB), flag, width  // 名為 symbol, 內(nèi)存寬度為 width, flag可省略

例子:

DATA age+0x00(SB)/4, $18    // age = 18GLOBL age(SB), RODATA, $4   // 聲明全局變量 age,占用 4Byte 內(nèi)存空間DATA pi+0(SB)/8, $3.1415926GLOBL pi(SB), RODATA, $8DATA bio<>+0(SB)/8, $"hello wo"     // <> 表示只在當(dāng)前文件生效DATA bio<>+8(SB)/8, $"old !!!!"     // bio = "hello world !!!!"GLOBL bio<>(SB), RODATA, $16

其中 flag 的字面量定義在 Go 標(biāo)準(zhǔn)庫(kù)下 src/runtime/textflag.h 文件中,需要在匯編文件中 #include "textflag.h",其類型有有如下幾個(gè):

flag

value

說(shuō)明

NOPROF

1

(TEXT項(xiàng)使用) 不優(yōu)化NOPROF標(biāo)記的函數(shù)。這個(gè)標(biāo)志已廢棄。(For TEXT items.) Don"t profile the marked function. This flag is deprecated.

DUPOK

2

在二進(jìn)制文件中允許一個(gè)符號(hào)的多個(gè)實(shí)例。鏈接器會(huì)選擇其中之一。It is legal to have multiple instances of this symbol in a single binary. The linker will choose one of the duplicates to use.

NOSPLIT

4

(TEXT項(xiàng)使用) 不插入檢測(cè)棧分裂(擴(kuò)張)的前導(dǎo)指令代碼(減少開(kāi)銷,一般用于葉子節(jié)點(diǎn)函數(shù)(函數(shù)內(nèi)部不調(diào)用其他函數(shù)))。程序的棧幀中,如果調(diào)用其他函數(shù)會(huì)增加棧幀的大小,必須在棧頂留出可用空間。用來(lái)保護(hù)程序,例如堆棧拆分代碼本身。(For TEXT items.) Don"t insert the preamble to check if the stack must be split. The frame for the routine, plus anything it calls, must fit in the spare space at the top of the stack segment. Used to protect routines such as the stack splitting code itself.

RODATA

8

(DATA和GLOBAL項(xiàng)使用) 將這個(gè)數(shù)據(jù)放在只讀的塊中。(For DATA and GLOBL items.) Put this data in a read-only section.

NOPTR

16

(用于DATA和GLOBL項(xiàng)目)這個(gè)數(shù)據(jù)不包含指針?biāo)跃筒恍枰占鱽?lái)掃描。(For DATA and GLOBL items.) This data contains no pointers and therefore does not need to be scanned by the garbage collector.

WRAPPER

32

(TEXT項(xiàng)使用)這是包裝函數(shù) (For TEXT items.) This is a wrapper function and should not count as disabling recover.

NEEDCTXT

64

(TEXT項(xiàng)使用)此函數(shù)是一個(gè)閉包,因此它將使用其傳入的上下文寄存器。(For TEXT items.) This function is a closure so it uses its incoming context register.

TLSBSS

256

(用于DATA和GLOBL項(xiàng)目)將此數(shù)據(jù)放入線程本地存儲(chǔ)中。Allocate a word of thread local storage and store the offset from the thread local base to the thread local storage in this variable.

NOFRAME

512

(TEXT項(xiàng)使用)不要插入指令為此函數(shù)分配棧幀。僅在聲明幀大小為0的函數(shù)上有效。(函數(shù)必須是葉子節(jié)點(diǎn)函數(shù),且以0標(biāo)記堆棧函數(shù),沒(méi)有保存幀指針(或link寄存器架構(gòu)上的返回地址))TODO(mwhudson):目前僅針對(duì) ppc64x 實(shí)現(xiàn)。Do not insert instructions to allocate a stack frame for this function. Only valid on functions that declare a frame size of 0. TODO(mwhudson): only implemented for ppc64x at present.

REFLECTMETHOD

1024

函數(shù)可以調(diào)用 reflect.Type.Method 或 reflect.Type.MethodByName。Function can call reflect.Type.Method or reflect.Type.MethodByName.

TOPFRAME

2048

(TEXT項(xiàng)使用)函數(shù)是調(diào)用堆棧的頂部。棧回溯應(yīng)在此功能處停止。Function is the outermost frame of the call stack. Call stack unwinders should stop at this function.

ABIWRAPPER

4096

函數(shù)是一個(gè) ABI 包裝器。Function is an ABI wrapper.

其中 NOSPLIT 需要特別注意,它表示該函數(shù)運(yùn)行不會(huì)導(dǎo)致棧分裂,用戶也可以使用 //go:nosplit 強(qiáng)制給 go 函數(shù)指定 NOSPLIT 屬性。例如:

//go:nosplitfunc someFunc() {}

匯編中直接給函數(shù)標(biāo)記 NOSPLIT 即可:

// 表示someFunc函數(shù)執(zhí)行時(shí)最多需要 24 字節(jié)本地變量和 8 字節(jié)參數(shù)空間TEXT ·someFunc(SB), NOSPLIT, $24-8      RET

鏈接器認(rèn)為標(biāo)記為 NOSPLIT 的函數(shù),最多需要使用 StackLimit 字節(jié)空間,所以不需要插入棧分裂(溢出)檢查,函數(shù)調(diào)用損耗更小。不過(guò),使用該標(biāo)志的時(shí)候要特別小心,萬(wàn)一發(fā)生意外容易導(dǎo)致棧溢出錯(cuò)誤,溢出時(shí)會(huì)在執(zhí)行期報(bào) nosplit stack overflow 錯(cuò)。Go 1.18 標(biāo)準(zhǔn)庫(kù)下 go/src/runtime/HACKING.md 中有如下說(shuō)明:

nosplit functionsMost functions start with a prologue that inspects the stack pointer and the current G"s stack bound and calls morestack if the stack needs to grow.Functions can be marked //go:nosplit (or NOSPLIT in assembly) to indicate that they should not get this prologue. This has several uses: - Functions that must run on the user stack, but must not call into stack growth, for example because this would cause a deadlock, or because they have untyped words on the stack. - Functions that must not be preempted on entry. - Functions that may run without a valid G. For example, functions that run in early runtime start-up, or that may be entered from C code such as cgo callbacks or the signal handler.Splittable functions ensure there"s some amount of space on the stack for nosplit functions to run in and the linker checks that any static chain of nosplit function calls cannot exceed this bound.Any function with a //go:nosplit annotation should explain why it is nosplit in its documentation comment.

另外,當(dāng)函數(shù)處于調(diào)用鏈的葉子節(jié)點(diǎn),且棧幀小于 StackSmall(128)字節(jié)時(shí),則自動(dòng)標(biāo)記為 NOSPLIT。此邏輯的代碼如下:

//const StackSmall  = 128    if ctxt.Arch.Family == sys.AMD64 && autoffset < objabi.StackSmall && !p.From.Sym.NoSplit() {        leaf := true    LeafSearch:        for q := p; q != nil; q = q.Link {            switch q.As {            case obj.ACALL:                // Treat common runtime calls that take no arguments                // the same as duffcopy and duffzero.                if !isZeroArgRuntimeCall(q.To.Sym) {                    leaf = false                    break LeafSearch                }                fallthrough            case obj.ADUFFCOPY, obj.ADUFFZERO:                if autoffset >= objabi.StackSmall-8 {                    leaf = false                    break LeafSearch                }            }        }        if leaf {            p.From.Sym.Set(obj.AttrNoSplit, true)        }    }
1.5.2 Go 語(yǔ)言中的常用用法

在匯編代碼中使用 go 變量:

#include "textflag.h"TEXT ·get(SB), NOSPLIT, $0-8    MOVQ ·a(SB), AX             // 把 go 代碼定義的全局變量讀到 AX 中    MOVQ AX, ret+0(FP)          // 把 AX 的值寫(xiě)入返回值位置    RET
package mainvar a = 999func get() intfunc main() {    println(get())}

go 代碼中使用匯編定義的變量:

// string 定義形式 1: 在 String 結(jié)構(gòu)體后多分配一個(gè) [n]byte 數(shù)組存放靜態(tài)字符串DATA ·Name+0(SB)/8,$·Name+16(SB)    // StringHeader.DataDATA ·Name+8(SB)/8,$6               // StringHeader.LenDATA ·Name+16(SB)/8,$"gopher"       // [6]byte{"g","o","p","h","e","r"}GLOBL ·Name(SB),NOPTR,$24           // struct{Data uintptr, Len int, str [6]byte}// string 定義形式 2:獨(dú)立分配一個(gè)僅當(dāng)前文件可見(jiàn)的 [n]byte 數(shù)組存放靜態(tài)字符串DATA str<>+0(SB)/8,$"Hello Wo"      // str[0:8]={"H","e","l","l","o"," ","W","o"}DATA str<>+8(SB)/8,$"rld!"          // str[9:12]={"r","l","d","!""}GLOBL str<>(SB),NOPTR,$16           // 定義全局?jǐn)?shù)組 var str<> [16]byteDATA ·Helloworld+0(SB)/8,$str<>(SB) // StringHeader.Data = &str<>DATA ·Helloworld+8(SB)/8,$12        // StringHeader.Len = 12GLOBL ·Helloworld(SB),NOPTR,$16     // struct{Data uintptr, Len int}
var Name,Helloworld stringfunc doSth() {    fmt.Printf("Name: %s\n", Name)               // 讀取匯編中初始化的變量 Name    fmt.Printf("Helloworld: %s\n", Helloworld)   // 讀取匯編中初始化的變量 Helloworld}// 輸出: // Name: gopher// Helloworld: Hello World!
1.6 函數(shù)調(diào)用1.6.1 使用語(yǔ)法

Go 語(yǔ)言匯編中,函數(shù)聲明格式如下:

告訴匯編器該數(shù)據(jù)放到TEXT區(qū)  ^                        靜態(tài)基地址指針(告訴匯編器這是基于靜態(tài)地址的數(shù)據(jù))  |                                ^      |                                |   標(biāo)簽   函數(shù)入?yún)?返回值占用空間大小  |                                |    ^      ^  |                                |    |      |TEXT pkgname·funcname(SB),TAG,$16-24     ^         ^        ^                   ^     |         |        |                   |函數(shù)所屬包名  函數(shù)名  表示ABI類型           函數(shù)棧幀大小(本地變量占用空間大小)

一些說(shuō)明:

棧幀大小包括局部變量和可能需要的額外調(diào)用函數(shù)的參數(shù)空間的總大小,但不不包含調(diào)用其他函數(shù)時(shí)的 ret address 的大小。匯編文件中,函數(shù)名以 "·" 開(kāi)頭或連接 pkgname 是固定格式。go 函數(shù)采用的是 caller-save 模式,被調(diào)用者的參數(shù)、返回值、棧位置都由調(diào)用者維護(hù)。

go 語(yǔ)言編譯成匯編:

go tool compile -S xxx.gogo build -gcflags -S xxx.go

從二進(jìn)制反編譯為匯編:

go tool objdump -s "main.main" main.out > main.S
1.6.2 使用例子

Go 函數(shù)調(diào)用匯編函數(shù):

// add.gopackage mainimport "fmt"func add(x, y int64) int64func main() {    fmt.Println(add(2, 3))}
// add_amd64.s// add(x,y) -> x+yTEXT ·add(SB),NOSPLIT,$0    MOVQ x+0(FP), BX    MOVQ y+8(FP), BP    ADDQ BP, BX    MOVQ BX, ret+16(FP)    RET

匯編調(diào)用 go 語(yǔ)言函數(shù):

package mainimport "fmt"func add(x, y int) int {    return x + y}func output(a, b int) intfunc main() {    s := output(10, 13)    fmt.Println(s)}
#include "textflag.h"http:// func output(a,b int) intTEXT ·output(SB), NOSPLIT, $24-24    MOVQ a+0(FP), DX    // arg a    MOVQ DX, 0(SP)      // arg x    MOVQ b+8(FP), CX    // arg b    MOVQ CX, 8(SP)      // arg y    CALL ·add(SB)       // 在調(diào)用 add 之前,已經(jīng)把參數(shù)都通過(guò)物理寄存器 SP 搬到了函數(shù)的棧頂    MOVQ 16(SP), AX     // add 函數(shù)會(huì)把返回值放在這個(gè)位置    MOVQ AX, ret+16(FP) // return result    RET
1.6.1 匯編函數(shù)中用到的一些特殊命令(偽指令)

GO_RESULTS_INITIALIZED:如果 Go 匯編函數(shù)返回值含指針,則該指針信息必須由 Go 源文件中的函數(shù)的 Go 原型提供,即使對(duì)于未直接從 Go 調(diào)用的匯編函數(shù)也是如此。如果返回值將在調(diào)用指令期間保存實(shí)時(shí)指針,則該函數(shù)中應(yīng)首先將結(jié)果歸零, 然后執(zhí)行偽指令 GO_RESULTS_INITIALIZED。表明該堆棧位置應(yīng)該執(zhí)行進(jìn)行 GC 掃描,避免其指向的內(nèi)存地址唄 GC 意外回收。

NO_LOCAL_POINTERS: 就是字面意思,表示函數(shù)沒(méi)有指針類型的局部變量。

PCDATA: Go 語(yǔ)言生成的匯編,利用此偽指令表明匯編所在的原始 Go 源碼的位置(file&line&func),用于生成 PC 表格。runtime.FuncForPC 函數(shù)就是通過(guò) PC 表格得到結(jié)果的。一般由編譯器自動(dòng)插入,手動(dòng)維護(hù)并不現(xiàn)實(shí)。

FUNCDATA: 和 PCDATA 的格式類似,用于生成 FUNC 表格。FUNC 表格用于記錄函數(shù)的參數(shù)、局部變量的指針信息,GC 依據(jù)它來(lái)跟蹤棧中指針指向內(nèi)存的生命周期,同時(shí)棧擴(kuò)縮容的時(shí)候也是依據(jù)它來(lái)確認(rèn)是否需要調(diào)整棧指針的值(如果指向的地址在需要擴(kuò)縮容的棧中,則需要同步修改)。

1.7 條件編譯

Go 語(yǔ)言僅支持有限的條件編譯規(guī)則:

根據(jù)文件名編譯。根據(jù) build 注釋編譯。

根據(jù)文件名編譯類似 *_test.go,通過(guò)添加平臺(tái)后綴區(qū)分,比如: asm_386.s、asm_amd64.s、asm_arm.s、asm_arm64.s、asm_mips64x.s、asm_linux_amd64.s、asm_bsd_arm.s 等.

根據(jù) build 注釋編譯,就是在源碼中加入?yún)^(qū)分平臺(tái)和編譯器版本的注釋。比如:

//go:build (darwin || freebsd || netbsd || openbsd) && gc// +build darwin freebsd netbsd openbsd// +build gc

Go 1.17 之前,我們可以通過(guò)在源碼文件頭部放置 +build 構(gòu)建約束指示符來(lái)實(shí)現(xiàn)構(gòu)建約束,但這種形式十分易錯(cuò),并且它并不支持&&和||這樣的直觀的邏輯操作符,而是用逗號(hào)、空格替代,下面是原 +build 形式構(gòu)建約束指示符的用法及含義:

Go 1.17 引入了 //go:build 形式的構(gòu)建約束指示符,支持&&和||邏輯操作符,如下代碼所示:

//go:build linux && (386 || amd64 || arm || arm64 || mips64 || mips64le || ppc64 || ppc64le)//go:build linux && (mips64 || mips64le)//go:build linux && (ppc64 || ppc64le)//go:build linux && !386 && !arm

考慮到兼容性,Go 命令可以識(shí)別這兩種形式的構(gòu)建約束指示符,但推薦 Go 1.17 之后都用新引入的這種形式。

gofmt 可以兼容處理兩種形式,處理原則是:如果一個(gè)源碼文件只有 // +build 形式的指示符,gofmt 會(huì)將與其等價(jià)的 //go:build 行加入。否則,如果一個(gè)源文件中同時(shí)存在這兩種形式的指示符行,那么 //+build 行的信息將被 //go:build 行的信息所覆蓋。

2、 go 語(yǔ)言 ABI

參考文檔:

Go internal ABI specification

https://go.googlesource.com/go/+/refs/heads/dev.regabi/src/cmd/compile/internal-abi.md

Proposal: Create an undefined internal calling convention

https://go.googlesource.com/proposal/+/master/design/27539-internal-abi.md

名詞解釋:ABI: application binary interface, 應(yīng)用程序二進(jìn)制接口,規(guī)定了程序在機(jī)器層面的操作規(guī)范和調(diào)用規(guī)約。調(diào)用規(guī)約: calling convention, 所謂“調(diào)用規(guī)約”是調(diào)用方和被調(diào)用方對(duì)于函數(shù)調(diào)用的一個(gè)明確的約定,包括:函數(shù)參數(shù)與返回值的傳遞方式、傳遞順序。只有雙方都遵守同樣的約定,函數(shù)才能被正確地調(diào)用和執(zhí)行。如果不遵守這個(gè)約定,函數(shù)將無(wú)法正確執(zhí)行。

Go 從1.17.1版本開(kāi)始支持多 ABI:1. 為了兼容性各平臺(tái)保持通用性,保留歷史版本 ABI,并更名為 ABI0。2. 為了更好的性能,增加新版本 ABI 取名 ABIInternal。ABI0 遵循平臺(tái)通用的函數(shù)調(diào)用約定,實(shí)現(xiàn)簡(jiǎn)單,不用擔(dān)心底層cpu架構(gòu)寄存器的差異;ABIInternal 可以指定特定的函數(shù)調(diào)用規(guī)范,可以針對(duì)特定性能瓶頸進(jìn)行優(yōu)化,在多個(gè) Go 版本之間可以迭代,靈活性強(qiáng),支持寄存器傳參提升性能。Go 匯編為了兼容已存在的匯編代碼,保持使用舊的 ABI0。

Go 為什么在有了 ABI0 之后,還要引入 ABIInternal?當(dāng)然是為了性能!據(jù)官方測(cè)試,寄存器傳參可以帶來(lái) 5% 的性能提升。

我們看一個(gè)例子:

package mainimport _ "fmt"func Print(delta string)func main() {   Print("hello")}
#include "textflag.h"TEXT ·Print(SB), NOSPLIT, $8    CALL fmt·Println(SB)    RET

運(yùn)行上面代碼會(huì)報(bào)錯(cuò):main.Print: relocation target fmt.Println not defined for ABI0 (but is defined for ABIInternal)

原因是,fmt·Println 函數(shù)默認(rèn)使用的 ABI 標(biāo)準(zhǔn)是 ABIInternal,而 Go 語(yǔ)言手寫(xiě)的匯編使用的 ABI 格式是 ABI0,二者標(biāo)準(zhǔn)不一樣不能直接調(diào)用。不過(guò) Go 語(yǔ)言可以通過(guò) //go:linkname 的方式為 ABIInternal 生成 ABI0 包裝。

package mainimport (   "fmt")//go:linkname Println fmt.Printlnfunc Println(a ...any) (n int, err error)func Print(delta interface{})func main() {   Print("hello")}
#include "textflag.h"TEXT ·Print(SB), NOSPLIT, $48-16     LEAQ strp+0(FP),AX  MOVQ AX, 0(SP)        // []interface{} slice 的 pointer  MOVQ $1, BX      MOVQ BX, 8(SP)        // slice 的 len  MOVQ BX, 16(SP)       // slice 的 cap  CALL fmt·Println(SB)  // //go:linkname 為 fmt.Println 生成一個(gè) ABI0 包裝后,匯編可以直接調(diào)用  RET

簡(jiǎn)單說(shuō)明:函數(shù) fmt.Println 是一個(gè)變參函數(shù),變參(a ...any)實(shí)際上是 (a []any)的語(yǔ)法糖。參數(shù)中,slice 占 24Byte,int 占 8Byte,error 是 interface 類型,占 16Byte,加起來(lái)是 48 Byte。所以,調(diào)用此函數(shù)時(shí),caller 需要再棧上準(zhǔn)備 24Byte 空間。而 Print 的入?yún)偤檬且粋€(gè) interface{} 類型,和 any 一致,所以只要把 Print 函數(shù)的入?yún)⒌牡刂焚x給 a 的指針,并把 a 的 len 和 cap 設(shè)置為 1,就可以調(diào)用 fmt·Println 函數(shù)了。如以上代碼所示。

3、內(nèi)存管理和 GC 對(duì)匯編的影響3.1 調(diào)用棧擴(kuò)縮容對(duì)匯編的影響

為了減少對(duì)內(nèi)存的占用,goroutine 啟動(dòng)時(shí) runtime 只給它分配了很少的棧內(nèi)存。所有函數(shù)(標(biāo)記 go:nosplit 的除外)的序言部分(啟動(dòng)指令)會(huì)插入分段檢查,當(dāng)發(fā)現(xiàn)棧溢出(??臻g不足)時(shí),就會(huì)調(diào)用 runtime.morestack,執(zhí)行棧拓展邏輯:

舊版本的 Go 編譯器采用了分段棧機(jī)制實(shí)現(xiàn)棧拓展,當(dāng)一個(gè) goroutine 的執(zhí)行棧溢出時(shí),就增加一個(gè)棧內(nèi)存作為調(diào)用棧的補(bǔ)充,新舊棧彼此沒(méi)有連續(xù)。這種設(shè)計(jì)的缺陷很容易破壞緩存的局部性原理,從而降低程序的運(yùn)行時(shí)性能。Go 1.3 版本開(kāi)始,引入了連續(xù)棧(拷貝棧)機(jī)制,并把 goroutine 的初始棧大小由 8KB 降低到了 2KB。當(dāng)一個(gè)執(zhí)行棧發(fā)生溢出時(shí),新建一個(gè)兩倍于原棧大小的新棧,并將原棧整個(gè)拷貝到新棧上,保證整個(gè)棧是連續(xù)的。

棧的拷貝有些副作用:

如果棧上存在指向當(dāng)前被拷貝棧的指針,當(dāng)棧拷貝執(zhí)行完成后,這個(gè)指針還是指向原棧,需要更新。goroutine 的 g 結(jié)構(gòu)體上的 gobuf 成員也還是指向舊的棧,也需要更新。

除了正在拷貝的棧中可能存在指向自己的的指針外,還有沒(méi)有其他存活中的內(nèi)存有指向即將失效的棧空間的指針呢?答案在 go 逃逸分析源碼 中,代碼如下:

// Escape analysis.//// Here we analyze functions to determine which Go variables// (including implicit allocations such as calls to "new" or "make",// composite literals, etc.) can be allocated on the stack. The two// key invariants we have to ensure are: (1) pointers to stack objects// cannot be stored in the heap, and (2) pointers to a stack object// cannot outlive that object (e.g., because the declaring function// returned and destroyed the object"s stack frame, or its space is// reused across loop iterations for logically distinct variables).//

其中 “(1) pointers to stack objects cannot be stored in the heap” 表明指向棧對(duì)象的指針不能存儲(chǔ)在堆中。

拷貝棧理論上沒(méi)有上限,但是一般都設(shè)置了上限。當(dāng)新的棧大小超過(guò)了 maxstacksize 就會(huì)拋出”stack overflow“的異常。maxstacksize 是在 runtime.main 中設(shè)置的。64 位系統(tǒng)下棧的最大值 1GB、32 位系統(tǒng)是 250MB。參考代碼:

if newsize > maxstacksize || newsize > maxstackceiling {    if maxstacksize < maxstackceiling {        print("runtime: goroutine stack exceeds ", maxstacksize, "-byte limit\n")    } else {        print("runtime: goroutine stack exceeds ", maxstackceiling, "-byte limit\n")    }    print("runtime: sp=", hex(sp), " stack=[", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")    throw("stack overflow")}

由拷貝棧的原理可知,拷貝棧對(duì) Go 匯編是透明的。

3.2 GC 對(duì)匯編的影響

由于 GC 會(huì)動(dòng)態(tài)回收沒(méi)有被引用的堆內(nèi)存,而 goroutine 的調(diào)用棧在堆空間,所以如果調(diào)用棧中存了堆內(nèi)存的指針,就需要告訴 GC 棧中含指針。上文中說(shuō)到的偽指令 FUNCDATA、GO_RESULTS_INITIALIZED、NO_LOCAL_POINTERS 就是干這個(gè)事的。由于 FUNCDATA 偽指令幾乎只能由編譯器維護(hù),所以在手寫(xiě)的匯編函數(shù)本地內(nèi)存棧中保存指向動(dòng)態(tài)內(nèi)存的指針幾乎是一種奢望。

4、 函數(shù)內(nèi)聯(lián)和匯編

參考文檔:

Go: Inlining Strategy & Limitation

https://medium.com/a-journey-with-go/go-inlining-strategy-limitation-6b6d7fc3b1be?

4.1 查看內(nèi)聯(lián)情況

可以通過(guò)執(zhí)行以下命令,輸出被內(nèi)聯(lián)的函數(shù):

go build -gcflags="-m" main.go# 輸出結(jié)果:# ./op.go:3:6: can inline add# ./op.go:7:6: can inline sub# ./main.go:16:11: inlining call to sub# ./main.go:14:11: inlining call to add# ./main.go:7:12: inlining call to fmt.Printf

或者使用參數(shù) -gflags="-m -m" 運(yùn)行,查看編譯器的詳細(xì)優(yōu)化策略:

go build -gcflags="-m -m" main.go

輸出很詳細(xì):

# command-line-arguments./main.go:10:6: cannot inline main: function too complex: cost 106 exceeds budget 80./main.go:20:12: inlining call to fmt.Printf./main.go:23:6: can inline toEface with cost 0 as: func() {  }./main.go:18:2: shlx escapes to heap:./main.go:18:2:   flow: i = &{storage for shlx}:./main.go:18:2:     from shlx (spill) at ./main.go:18:2./main.go:18:2:     from i = shlx (assign) at ./main.go:18:4./main.go:18:2:   flow: {storage for ... argument} = i:./main.go:18:2:     from ... argument (slice-literal-element) at ./main.go:20:12./main.go:18:2:   flow: fmt.a = &{storage for ... argument}:./main.go:18:2:     from ... argument (spill) at ./main.go:20:12./main.go:18:2:     from fmt.format, fmt.a := "%+v", ... argument (assign-pair) at ./main.go:20:12./main.go:18:2:   flow: {heap} = *fmt.a:./main.go:18:2:     from fmt.Fprintf(io.Writer(os.Stdout), fmt.format, fmt.a...) (call parameter) at ./main.go:20:12./main.go:17:2: x escapes to heap:./main.go:17:2:   flow: i = &{storage for x}:./main.go:17:2:     from x (spill) at ./main.go:17:2./main.go:17:2:     from i = x (assign) at ./main.go:17:4./main.go:17:2:   flow: {storage for ... argument} = i:./main.go:17:2:     from ... argument (slice-literal-element) at ./main.go:20:12./main.go:17:2:   flow: fmt.a = &{storage for ... argument}:./main.go:17:2:     from ... argument (spill) at ./main.go:20:12./main.go:17:2:     from fmt.format, fmt.a := "%+v", ... argument (assign-pair) at ./main.go:20:12./main.go:17:2:   flow: {heap} = *fmt.a:./main.go:17:2:     from fmt.Fprintf(io.Writer(os.Stdout), fmt.format, fmt.a...) (call parameter) at ./main.go:20:12./main.go:17:2: x escapes to heap./main.go:18:2: shlx escapes to heap./main.go:20:12: ... argument does not escape

Go 編譯器默認(rèn)將進(jìn)行內(nèi)聯(lián)優(yōu)化,可以通過(guò) -gcflags="-l" 選項(xiàng)全局禁用內(nèi)聯(lián),與一個(gè)-l禁用內(nèi)聯(lián)相反,如果傳遞兩個(gè)或兩個(gè)以上的-l則會(huì)打開(kāi)內(nèi)聯(lián),并啟用更激進(jìn)的內(nèi)聯(lián)策略。例如以下代碼:

// 3.1: var closure = NewClosure()func main() {    // 3.2: var closure func() int    var closure = NewClosure()    closure()    // 3.3: closure = NewClosure()    closure()}func NewClosure() func() int {    i := 0return func() int {        i++        return i    }}

命令 go build -gcflags="-m" main.go 和 go build -gcflags="-m -l -l" main.go 都是輸出:

./main.go:19:6: can inline NewClosure./main.go:21:9: can inline NewClosure.func1./main.go:13:26: inlining call to NewClosure./main.go:21:9: can inline main.func1./main.go:14:9: inlining call to main.func1./main.go:16:9: inlining call to main.func1./main.go:13:26: func literal does not escape./main.go:20:2: moved to heap: i./main.go:21:9: func literal escapes to heap

命令 go build -gcflags="-m" main.go 輸出:

./main.go:20:2: moved to heap: i./main.go:21:9: func literal escapes to heap
4.2 內(nèi)聯(lián)前后性能對(duì)比

首先,看一下函數(shù)內(nèi)聯(lián)與非內(nèi)聯(lián)的性能差異。內(nèi)聯(lián)可以避免函數(shù)調(diào)用過(guò)程中的一些開(kāi)銷:創(chuàng)建棧幀,讀寫(xiě)寄存器。不過(guò),對(duì)函數(shù)體進(jìn)行拷貝也會(huì)增大二進(jìn)制文件的大小。據(jù) Go 官方宣傳,內(nèi)聯(lián)大概會(huì)有 5~6% 的性能提升。

//go:noinlinefunc maxNoinline(a, b int) int {    if a < b {        return b    }    return a}func maxInline(a, b int) int {    if a < b {        return b    }    return a}func BenchmarkInline(b *testing.B) {    x, y := 1, 2    b.Run("BenchmarkNoInline", func(b *testing.B) {        b.ResetTimer()        for i := 0; i < b.N; i++ {            maxNoinline(x, y)        }    })    b.Run("BenchmarkInline", func(b *testing.B) {        b.ResetTimer()        for i := 0; i < b.N; i++ {            maxInline(x, y)        }    })}

在程序代碼中,想要禁止編譯器內(nèi)聯(lián)優(yōu)化很簡(jiǎn)單,在函數(shù)定義前一行添加 //go:noinline 即可。以下是性能對(duì)比結(jié)果:

BenchmarkInline/BenchmarkNoInline-12  886137398  1.248  ns/op  0 B/op  0 allocs/opBenchmarkInline/BenchmarkInline-12   1000000000  0.2506 ns/op  0 B/op  0 allocs/op

因?yàn)楹瘮?shù)體內(nèi)部的執(zhí)行邏輯非常簡(jiǎn)單,此時(shí)內(nèi)聯(lián)與否的性能差異主要體現(xiàn)在函數(shù)調(diào)用的固定開(kāi)銷上。顯而易見(jiàn),該差異是非常大的。

4.3 內(nèi)聯(lián)條件

Go 語(yǔ)言代碼函數(shù)內(nèi)聯(lián)的策略每個(gè)編譯器版本都有細(xì)微差別,比如新版已支持含 for 和 閉包 的函數(shù)內(nèi)聯(lián)。1.18 版本的部分無(wú)法內(nèi)聯(lián)的規(guī)則如下:

函數(shù)標(biāo)注 "go:noinline" 注釋。函數(shù)標(biāo)注 "go:norace" 注釋,且使用 "-gcflags=-d checkptr" 參數(shù)編譯。函數(shù)標(biāo)注 "go:cgo_unsafe_args" 注釋。函數(shù)標(biāo)注 "go:uintptrescapes" 注釋。函數(shù)只有聲明而沒(méi)有函數(shù)體:比如函數(shù)實(shí)體在匯編文件 xxx.s 中。超過(guò)小代碼量邊界的函數(shù):內(nèi)聯(lián)的小代碼量邊界是 80 個(gè)節(jié)點(diǎn)(抽象語(yǔ)法樹(shù)AST的節(jié)點(diǎn))。函數(shù)中含某些關(guān)鍵字的函數(shù):比如 select、defer、go、recover 等。一些特殊的內(nèi)部函數(shù):比如 runtime.getcallerpc、runtime.getcallersp (這倆太特殊了)。函數(shù)內(nèi)部使用 type 關(guān)鍵字重定義了類型:比如 "type Int int" 或 "type Int = int"。作為尾遞歸調(diào)用時(shí)。

此外,還有一些編譯器覺(jué)得內(nèi)聯(lián)成本很低,所以必然內(nèi)聯(lián)的函數(shù):

"runtime" package 下的 "heapBits.nextArena" 和 "builtin" package 下的 "append"。"encoding/binary" package 下的:"littleEndian.Uint64", "littleEndian.Uint32", "littleEndian.Uint16","bigEndian.Uint64", "bigEndian.Uint32", "bigEndian.Uint16","littleEndian.PutUint64", "littleEndian.PutUint32", "littleEndian.PutUint16","bigEndian.PutUint64", "bigEndian.PutUint32", "bigEndian.PutUint16", "append"。

由規(guī)則 5 可知,Go 語(yǔ)言匯編是無(wú)法內(nèi)聯(lián)的。

此外,關(guān)于閉包內(nèi)聯(lián)是一個(gè)比較復(fù)雜的話題,據(jù)筆者測(cè)試,1.18 有如上規(guī)則:

滿足條件的閉包可以內(nèi)聯(lián)。閉包通用部分在內(nèi)聯(lián)統(tǒng)計(jì)的時(shí)候,占用函數(shù)的 15 個(gè) AST 節(jié)點(diǎn)。變量保存的閉包,如果是局部變量且沒(méi)有重新賦值過(guò),則可以被內(nèi)聯(lián)。

關(guān)于閉包內(nèi)聯(lián)的第 3 條規(guī)則,有如下例子:

// 3.1: var closure = NewClosure()func main() {    // 3.2: var closure func() int    var closure = NewClosure()    closure()    // 3.3: closure = NewClosure()    closure()}func NewClosure() func() int {    i := 0    return func() int {        i++        return i    }}

執(zhí)行 go build -gcflags="-m" ./ 輸出如下

./main.go:19:6: can inline NewClosure./main.go:21:9: can inline NewClosure.func1./main.go:13:26: inlining call to NewClosure./main.go:21:9: can inline main.func1./main.go:14:9: inlining call to main.func1./main.go:16:9: inlining call to main.func1./main.go:13:26: func literal does not escape./main.go:20:2: moved to heap: i./main.go:21:9: func literal escapes to heap

表明閉包 closure 可以內(nèi)聯(lián)。如果把 3.1 或 3.2 或 3.3 的注釋打開(kāi),則將會(huì)輸出:

./main.go:19:6: can inline NewClosure./main.go:21:9: can inline NewClosure.func1./main.go:13:22: inlining call to NewClosure./main.go:13:22: func literal does not escape./main.go:20:2: moved to heap: i./main.go:21:9: func literal escapes to heap

表明閉包 closure 無(wú)法內(nèi)聯(lián)。

此外,如果想禁用閉包內(nèi)聯(lián),可以使用 -gcflags="-d=inlfuncswithclosures=0" 或-gcflags="-d inlfuncswithclosures=0" 參數(shù)編譯。

go build -gcflags="-d=inlfuncswithclosures=0" main.gogo build -gcflags="-d inlfuncswithclosures=0" main.go

如果想了解 go 1.18 的內(nèi)聯(lián)檢查邏輯,可以看這個(gè)源碼:inline.CanInline 和 (*inline.hairyVisitor).doNode。其調(diào)用順序是:inline.CanInline --> inline.hairyVisitor.tooHairy --> inline.hairyVisitor.doNode。

// CanInline determines whether fn is inlineable.// If so, CanInline saves copies of fn.Body and fn.Dcl in fn.Inl.// fn and fn.Body will already have been typechecked.func CanInline(fn *ir.Func) {...    // If marked "go:noinline", don"t inline    if fn.Pragma&ir.Noinline != 0 {        reason = "marked go:noinline"        return    }    // If marked "go:norace" and -race compilation, don"t inline.    if base.Flag.Race && fn.Pragma&ir.Norace != 0 {        reason = "marked go:norace with -race compilation"        return    }    // If marked "go:nocheckptr" and -d checkptr compilation, don"t inline.    if base.Debug.Checkptr != 0 && fn.Pragma&ir.NoCheckPtr != 0 {        reason = "marked go:nocheckptr"        return    }    // If marked "go:cgo_unsafe_args", don"t inline, since the    // function makes assumptions about its argument frame layout.    if fn.Pragma&ir.CgoUnsafeArgs != 0 {        reason = "marked go:cgo_unsafe_args"        return    }    // If marked as "go:uintptrescapes", don"t inline, since the    // escape information is lost during inlining.    if fn.Pragma&ir.UintptrEscapes != 0 {        reason = "marked as having an escaping uintptr argument"        return    }    // The nowritebarrierrec checker currently works at function    // granularity, so inlining yeswritebarrierrec functions can    // confuse it (#22342). As a workaround, disallow inlining    // them for now.    if fn.Pragma&ir.Yeswritebarrierrec != 0 {        reason = "marked go:yeswritebarrierrec"        return    }    // If fn has no body (is defined outside of Go), cannot inline it.    if len(fn.Body) == 0 {        reason = "no function body"        return    }...    visitor := hairyVisitor{        budget:        inlineMaxBudget, // inlineMaxBudget == 80         extraCallCost: cc,    }    if visitor.tooHairy(fn) {        reason = visitor.reason        return    }...}func (v *hairyVisitor) tooHairy(fn *ir.Func) bool {    v.do = v.doNode // cache closure    if ir.DoChildren(fn, v.do) {        return true    }...}func (v *hairyVisitor) doNode(n ir.Node) bool {...    case ir.OSELECT,        ir.OGO,        ir.ODEFER,        ir.ODCLTYPE, // can"t print yet        ir.OTAILCALL:        v.reason = "unhandled op " + n.Op().String()        return true...}
5、 有哪些有意思的使用場(chǎng)景5.1、 獲取 goid

goid 即 goroutine id,最常用三方庫(kù)應(yīng)該就是 petermattis/goid, 里通過(guò)匯編獲取 goid 的代碼關(guān)鍵邏輯如下:

runtime_go1.9.go 代碼:

//go:build gc && go1.9// +build gc,go1.9package goidtype stack struct {    lo uintptr    hi uintptr}type gobuf struct {    sp   uintptr    pc   uintptr    g    uintptr    ctxt uintptr    ret  uintptr    lr   uintptr    bp   uintptr}type g struct {    stack       stack    stackguard0 uintptr    stackguard1 uintptr    _panic       uintptr    _defer       uintptr    m            uintptr    sched        gobuf    syscallsp    uintptr    syscallpc    uintptr    stktopsp     uintptr    param        uintptr    atomicstatus uint32    stackLock    uint32    goid         int64 // Here it is!}

goid_go1.5_amd64.go 代碼:

//go:build (amd64 || amd64p32) && gc && go1.5// +build amd64 amd64p32// +build gc// +build go1.5package goidfunc Get() int64

goid_go1.5_amd64.s 代碼:

//go:build (amd64 || amd64p32) && gc && go1.5// +build amd64 amd64p32// +build gc// +build go1.5#include "go_asm.h"#include "textflag.h"http:// func Get() int64TEXT ·Get(SB),NOSPLIT,$0-8    MOVQ (TLS), R14    MOVQ g_goid(R14), R13    MOVQ R13, ret+0(FP)    RET

不過(guò)這樣獲取 goid 有一個(gè)局限性,就是如果當(dāng)前處于 g0 調(diào)用棧(系統(tǒng)調(diào)用或CGO函數(shù)中)時(shí),拿到的不是當(dāng)前 g 的 goid,而是 是 g0 的 goid。在這種情況下 g.m.curg.goid 才是當(dāng)前 g 的 goid。參考Go1.18 標(biāo)準(zhǔn)庫(kù)下go/src/runtime/HACKING.md 文件里的說(shuō)明:

getg() and getg().m.curgTo get the current user g, use getg().m.curg.getg() alone returns the current g, but when executing on the system or signal stacks, this will return the current M"s "g0" or "gsignal", respectively. This is usually not what you want.To determine if you"re running on the user stack or the system stack, use getg() == getg().m.curg.

除了 goid,pid也可以用匯編獲取:choleraehyq/pid 是一個(gè) fork petermattis/goid 的倉(cāng)庫(kù),里面增加了獲取 pid 的實(shí)現(xiàn),實(shí)現(xiàn)代碼如下:

p_m_go1.19.go 代碼:

//go:build gc && go1.19 && !go1.21// +build gc,go1.19,!go1.21package goidtype p struct {    id int32 // Here is pid}type m struct {    g0      uintptr // goroutine with scheduling stack    morebuf gobuf   // gobuf arg to morestack    divmod  uint32  // div/mod denominator for arm - known to liblink    _       uint32// Fields not known to debuggers.    procid     uint64       // for debuggers, but offset not hard-coded    gsignal    uintptr      // signal-handling g    goSigStack gsignalStack // Go-allocated signal handling stack    sigmask    sigset       // storage for saved signal mask    tls        [6]uintptr   // thread-local storage (for x86 extern register)    mstartfn   func()    curg       uintptr // current running goroutine    caughtsig  uintptr // goroutine running during fatal signal    p          *p      // attached p for executing go code (nil if not executing go code)}

pid_go1.5.go 代碼:

//go:build (amd64 || amd64p32 || arm64) && !windows && gc && go1.5// +build amd64 amd64p32 arm64// +build !windows// +build gc// +build go1.5package goid//go:nosplitfunc getPid() uintptr//go:nosplitfunc GetPid() int {    return int(getPid())}

pid_go1.5_amd64.s 代碼:

// +build amd64 amd64p32// +build gc,go1.5#include "go_asm.h"#include "textflag.h"http:// func getPid() int64TEXT ·getPid(SB),NOSPLIT,$0-8    MOVQ (TLS), R14    MOVQ g_m(R14), R13    MOVQ m_p(R13), R14    MOVL p_id(R14), R13    MOVQ R13, ret+0(FP)    RET

不過(guò),通過(guò)這種方式獲取的 pid 也有一個(gè)局限性:在持有 pid 之后的時(shí)間里,可能當(dāng)前 goroutine 已經(jīng)被調(diào)度到其他 P 上了,也就是在使用 pid 的時(shí)候當(dāng)前 pid 已經(jīng)改變了。如果想要持有在持有 pid 的過(guò)程中持續(xù)幫當(dāng)當(dāng)前 P,可以使用一下方式:

import "unsafe"var _ = unsafe.Sizeof(0)//go:linkname procPin runtime.procPin//go:nosplitfunc procPin() int//go:linkname procUnpin runtime.procUnpin//go:nosplitfunc procUnpin()

runtime.procPin 和 runtime.procUnpin的實(shí)現(xiàn)代碼在Go 標(biāo)準(zhǔn)庫(kù)下的 src/runtime/proc.go 文件中:

//go:nosplitfunc procPin() int {    _g_ := getg()    mp := _g_.m    mp.locks++     // 鎖定 P 的調(diào)度    return int(mp.p.ptr().id)}//go:nosplitfunc procUnpin() {    _g_ := getg()    _g_.m.locks--}

通過(guò) procPin 函數(shù)鎖定 P 的調(diào)度后再使用 pid,然后通過(guò) procUnpin 釋放 P。不過(guò)這里也需要謹(jǐn)慎使用,使用不當(dāng)會(huì)對(duì)性能產(chǎn)生嚴(yán)重影響。

以上獲取 goid 的方式還有一個(gè)比較大的缺點(diǎn),就是如果 Go 編譯器修改了 g 的結(jié)構(gòu)體,就需要重新適配。

《Go語(yǔ)言高級(jí)編程》第三章第8節(jié) 的實(shí)現(xiàn)可以避免這個(gè)問(wèn)題。其原理是,通過(guò)匯編構(gòu)建一個(gè) g 類型的 interface{},然后通過(guò)反射獲取 goid 成員的偏移量。根據(jù)原理,可以如下實(shí)現(xiàn):

func Getg() int64func getgi() interface{}var g_goid_offset uintptr = func() uintptr {    g := getgi()    if f, ok := reflect.TypeOf(g).FieldByName("goid"); ok {        return f.Offset    }    panic("can not find g.goid field")}()
TEXT ·Getg(SB), NOSPLIT, $0-8    MOVQ (TLS), AX    ADDQ ·g_goid_offset(SB),AX    MOVQ (AX), BX    MOVQ BX, ret+0(FP)    RET// func getgi() interface{}TEXT ·getgi(SB), NOSPLIT, $32-16    NO_LOCAL_POINTERS    MOVQ $0, ret_type+0(FP)    MOVQ $0, ret_data+8(FP)    GO_RESULTS_INITIALIZED    // get runtime.g    // MOVQ (TLS), AX    MOVQ $0, AX    // get runtime.g type    MOVQ $type·runtime·g(SB), BX    // MOVQ BX, ·runtime_g_type(SB)    // return interface{}    MOVQ BX, ret_type+0(FP)    MOVQ AX, ret_data+8(FP)    RET

實(shí)際上還可以繼續(xù)簡(jiǎn)化:

var runtime_g_type uint64  // go 源碼中聲明var gGoidOffset uintptr = func() uintptr { //nolint    var iface interface{}    type eface struct {        _type uint64        data  unsafe.Pointer    }    // 結(jié)構(gòu) iface 后,修改他的類型為 g    (*eface)(unsafe.Pointer(&iface))._type = runtime_g_type    if f, ok := reflect.TypeOf(iface).FieldByName("goid"); ok {        return f.Offset    }    panic("can not find g.goid field")}()
GLOBL ·runtime_g_type(SB),NOPTR,$8  DATA ·runtime_g_type+0(SB)/8,$type·runtime·g(SB)  // 匯編中初始化。匯編中可以訪問(wèn) package 的私有變量
5.2、Monkey Patch

Go 語(yǔ)言實(shí)現(xiàn)猴子打點(diǎn)的 package 不一定需要使用匯編,比如 bouk/monkey 和 go-kiss/monkey。不過(guò)字節(jié)開(kāi)源的 monkey 和 內(nèi)部的 mockito 都使用了匯編。他們有一個(gè)同源的依賴庫(kù),分別在 mockey/internal/monkey 目錄和 mockito/monkey 目錄下。

其 Patch() 的調(diào)用路徑如下:Build() -> Patch() -> PatchValue() -> WriteWithSTW() -> Write() -> do_replace_code() 其中 do_replace_code() 是匯編實(shí)現(xiàn)的,作用是使用 mprotect 系統(tǒng)調(diào)用來(lái)修改內(nèi)存權(quán)限(mprotect系統(tǒng)調(diào)用是修改內(nèi)存頁(yè)屬性的)。原因是:可執(zhí)行代碼區(qū)是只讀的,需要修改為可讀寫(xiě)后才能修改,修改為可執(zhí)行后才能執(zhí)行(有想用 Go 寫(xiě)病毒的,可以參考一下)。

func (builder *MockBuilder) Build() *Mocker {    mocker := Mocker{target: reflect.ValueOf(builder.target), builder: builder}    mocker.buildHook(builder)    mocker.Patch()    return &mocker}func (mocker *Mocker) Patch() *Mocker {    mocker.lock.Lock()    defer mocker.lock.Unlock()    if mocker.isPatched {        return mocker    }    mocker.patch = monkey.PatchValue(mocker.target, mocker.hook, reflect.ValueOf(mocker.proxy), mocker.builder.unsafe)    mocker.isPatched = true    addToGlobal(mocker)    mocker.outerCaller = tool.OuterCaller()    return mocker}// PatchValue replace the target function with a hook function, and stores the target function in the proxy function// for future restore. Target and hook are values of function. Proxy is a value of proxy function pointer.func PatchValue(target, hook, proxy reflect.Value, unsafe bool) *Patch {    tool.Assert(hook.Kind() == reflect.Func, ""%s" is not a function", hook.Kind())    tool.Assert(proxy.Kind() == reflect.Ptr, ""%v" is not a function pointer", proxy.Kind())    tool.Assert(hook.Type() == target.Type(), ""%v" and "%s" mismatch", hook.Type(), target.Type())    tool.Assert(proxy.Elem().Type() == target.Type(), ""*%v" and "%s" mismatch", proxy.Elem().Type(), target.Type())    targetAddr := target.Pointer()    // The first few bytes of the target function codeconst bufSize = 64    targetCodeBuf := common.BytesOf(targetAddr, bufSize)    // construct the branch instruction, i.e. jump to the hook function    hookCode := inst.BranchInto(common.PtrAt(hook))    // search the cutting point of the target code, i.e. the minimum length of full instructions that is longer than the hookCode    cuttingIdx := inst.Disassemble(targetCodeBuf, len(hookCode), !unsafe)    // construct the proxy code    proxyCode := common.AllocatePage()    // save the original code before the cutting pointcopy(proxyCode, targetCodeBuf[:cuttingIdx])    // construct the branch instruction, i.e. jump to the cutting pointcopy(proxyCode[cuttingIdx:], inst.BranchTo(targetAddr+uintptr(cuttingIdx)))    // inject the proxy code to the proxy function    fn.InjectInto(proxy, proxyCode)    tool.DebugPrintf("PatchValue: hook code len(%v), cuttingIdx(%v)\n", len(hookCode), cuttingIdx)    // replace target function codes before the cutting point    mem.WriteWithSTW(targetAddr, hookCode)    return &Patch{base: targetAddr, code: proxyCode, size: cuttingIdx}}// WriteWithSTW copies data bytes to the target address and replaces the original bytes, during which it will stop the// world (only the current goroutine"s P is running).func WriteWithSTW(target uintptr, data []byte) {    common.StopTheWorld()    defer common.StartTheWorld()    err := Write(target, data)    tool.Assert(err == nil, err)}

而 Write 函數(shù)的實(shí)現(xiàn)在 github.com/bytedance/mockey/internal/monkey/mem/write_linux.go,其代碼如下:

package memimport (    "syscall""github.com/bytedance/mockey/internal/monkey/common")func Write(target uintptr, data []byte) error {        do_replace_code(target, common.PtrOf(data), uint64(len(data)), syscall.SYS_MPROTECT,        syscall.PROT_READ|syscall.PROT_WRITE, syscall.PROT_READ|syscall.PROT_EXEC))    return nil}func do_replace_code(    _ uintptr, // void   *addr    _ uintptr, // void   *data    _ uint64, // size_t  size    _ uint64, // int     mprotect    _ uint64, // int     prot_rw    _ uint64, // int     prot_rx)

do_replace_code 函數(shù)的匯編實(shí)現(xiàn)在 github.com/bytedance/mockey/internal/monkey/mem/write_linux_amd64.s,代碼如下:

#include "textflag.h"#define NOP8 BYTE $0x90; BYTE $0x90; BYTE $0x90; BYTE $0x90; BYTE $0x90; BYTE $0x90; BYTE $0x90; BYTE $0x90;#define NOP64 NOP8; NOP8; NOP8; NOP8; NOP8; NOP8; NOP8; NOP8;#define NOP512 NOP64; NOP64; NOP64; NOP64; NOP64; NOP64; NOP64; NOP64;#define NOP4096 NOP512; NOP512; NOP512; NOP512; NOP512; NOP512; NOP512; NOP512;#define addr        arg + 0x00(FP)#define data        arg + 0x08(FP)#define size        arg + 0x10(FP)#define mprotect    arg + 0x18(FP)#define prot_rw     arg + 0x20(FP)#define prot_rx     arg + 0x28(FP)#define CMOVNEQ_AX_CX   \    BYTE $0x48          \    BYTE $0x0f          \    BYTE $0x45          \    BYTE $0xc8TEXT ·do_replace_code(SB), NOSPLIT, $0x30 - 0    JMP START    NOP4096START:    MOVQ    addr, DI    MOVQ    size, SI    MOVQ    DI, AX    ANDQ    $0x0fff, AX    ANDQ    $~0x0fff, DI    ADDQ    AX, SI    MOVQ    SI, CX    ANDQ    $0x0fff, CX    MOVQ    $0x1000, AX    SUBQ    CX, AX    TESTQ   CX, CX    CMOVNEQ_AX_CX    ADDQ    CX, SI    MOVQ    DI, R8    MOVQ    SI, R9    MOVQ    mprotect , AX    MOVQ    prot_rw  , DX    SYSCALL    MOVQ    addr, DI    MOVQ    data, SI    MOVQ    size, CX    REP    MOVSB    MOVQ    R8, DI    MOVQ    R9, SI    MOVQ    mprotect , AX    MOVQ    prot_rx  , DX    SYSCALL    JMP     RETURN    NOP4096RETURN:    RET
5.3、 優(yōu)化獲取行號(hào)性能

筆者另一篇掘金文章 《golang文件行號(hào)探索》 中有詳細(xì)說(shuō)明,代碼如下:

//stack_amd64.gotype Line uintptrfunc NewLine() Linevar rcuCache unsafe.Pointer = func() unsafe.Pointer {    m := make(map[Line]string)    return unsafe.Pointer(&m)}()func (l Line) LineNO() (line string) {    mPCs := *(*map[Line]string)(atomic.LoadPointer(&rcuCache))    line, ok := mPCs[l]    if !ok {        file, n := runtime.FuncForPC(uintptr(l)).FileLine(uintptr(l))        line = file + ":" + strconv.Itoa(n)        mPCs2 := make(map[Line]string, len(mPCs)+10)        mPCs2[l] = line        for {            p := atomic.LoadPointer(&rcuCache)            mPCs = *(*map[Line]string)(p)            for k, v := range mPCs {                mPCs2[k] = v            }            swapped := atomic.CompareAndSwapPointer(&rcuCache p, unsafe.Pointer(&mPCs2))            if swapped {                break            }        }    }    return}
# stack_amd64.sTEXT    ·NewLine(SB), NOSPLIT, $0-8    MOVQ     retpc-8(FP), AX    SUBQ     $1, AX             // 注意,這里要 -1    MOVQ     AX, ret+0(FP)    RET

該代碼除了使用匯編獲取行號(hào)外,還是用了無(wú)鎖的 RCU(Read-copy update) 算法提升并發(fā)查詢速度。還有一點(diǎn)要注意的,retpc-8(FP) 是函數(shù)返回地址,也就是調(diào)用指令 CALL 的下一行指令, 所以需要 -1 才能得到 CALL 指令的 pc,參考Go 源碼 src/runtime/traceback.g 的這段注釋:

// file/line information using pc-1, because that is the pc of the// call instruction (more precisely, the last byte of the call instruction).// Callers expect the pc buffer to contain return addresses and do the// same -1 themselves, so we keep pc unchanged.// When the pc is from a signal (e.g. profiler or segv) then we want// to look up file/line information using pc, and we store pc+1 in the// pc buffer so callers can unconditionally subtract 1 before looking up.// See issue 34123.// The pc can be at function entry when the frame is initialized without// actually running code, like runtime.mstart.
5.4、 優(yōu)化獲取調(diào)用棧性能

筆者另一篇掘金文章 《關(guān)于 golang 錯(cuò)誤處理的一些優(yōu)化想法》 中有詳細(xì)說(shuō)明。stack_amd64.go 代碼:

//go:build amd64// +build amd64package errorsimport (    _ "unsafe")func buildStack(s []uintptr) int

stack_amd64.s 代碼:

//go:build amd64 || amd64p32 || arm64// +build amd64 amd64p32 arm64#include "go_asm.h"#include "textflag.h"#include "funcdata.h"http:// func buildStack(s []uintptr) intTEXT ·buildStack(SB), NOSPLIT, $24-8    NO_LOCAL_POINTERS    MOVQ     cap+16(FP), DX     // s.cap    MOVQ     p+0(FP), AX        // s.ptr    MOVQ    $0, CX            // loop.iloop:    MOVQ    +8(BP), BX        // last pc -> BX    SUBQ     $1, BX     MOVQ    BX, 0(AX)(CX*8)        // s[i] = BX        ADDQ    $1, CX            // CX++ / i++    CMPQ    CX, DX            // if s.len >= s.cap { return }    JAE    return                // 無(wú)符號(hào)大于等于就跳轉(zhuǎn)    MOVQ    +0(BP), BP         // last BP; 展開(kāi)調(diào)用棧至上一層    CMPQ    BP, $0             // if (BP) <= 0 { return }    JA loop                    // 無(wú)符號(hào)大于就跳轉(zhuǎn)return:    MOVQ    CX,n+24(FP)     // ret n    RET
5.5、 字符串比較

Go 語(yǔ)言源碼里的字符串比較函數(shù),實(shí)際上使用了 SIMD 指令加速,由匯編實(shí)現(xiàn)。源碼在 Go 源碼文件中:src/cmd/compile/internal/typecheck/builtin/runtime.go :

func cmpstring(string, string) int

src/internal/bytealg/compare_amd64.s:

TEXT ·Compare(SB),NOSPLIT,$0-56        // AX = a_base (want in SI)        // BX = a_len  (want in BX)        // CX = a_cap  (unused)        // DI = b_base (want in DI)        // SI = b_len  (want in DX)        // R8 = b_cap  (unused)        MOVQ    SI, DX        MOVQ    AX, SI        JMP    cmpbody<>(SB)    TEXT runtime·cmpstring(SB),NOSPLIT,$0-40        // AX = a_base (want in SI)        // BX = a_len  (want in BX)        // CX = b_base (want in DI)        // DI = b_len  (want in DX)        MOVQ    AX, SI        MOVQ    DI, DX        MOVQ    CX, DI        JMP    cmpbody<>(SB)    // input:    //   SI = a    //   DI = b    //   BX = alen    //   DX = blen    // output:    //   AX = output (-1/0/1)    TEXT cmpbody<>(SB),NOSPLIT,$0-0        CMPQ    SI, DI    ...loop:    CMPQ    R8, $16    JBE    _0through16    MOVOU    (SI), X0    MOVOU    (DI), X1    PCMPEQB X0, X1    PMOVMSKB X1, AX    XORQ    $0xffff, AX    // convert EQ to NE    JNE    diff16    // branch if at least one byte is not equal    ADDQ    $16, SI    ADDQ    $16, DI    SUBQ    $16, R8    JMP    loop    ···

這里 MOVOU、PCMPEQB、PMOVMSKB 等就是 SIMD 指令。如果想詳細(xì)了解 SIMD 指令可以看一下 Intel 的官方文檔 《Intel? Intrinsics Guide》。另,據(jù)筆者的嘗試,SSE 和 SSE2 指令是可以直接在 Go 語(yǔ)言會(huì)便利使用的。有想法的同學(xué)可以自己驗(yàn)證一下其他 SIMD 指令。

5.6、 字符串搜索

我們常用的字符串搜索函數(shù) strings.Index,也使用了匯編實(shí)現(xiàn)的 SIMD 指令加速。代碼在 Go 源碼文件 src/strings/strings.go 下:

// Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.func Index(s, substr string) int {    n := len(substr)    switch {    case n == 0:        return 0case n == 1:        return IndexByte(s, substr[0])    case n == len(s):        if substr == s {            return 0        }        return -1case n > len(s):        return -1case n <= bytealg.MaxLen:        // Use brute force when s and substr both are smallif len(s) <= bytealg.MaxBruteForce {            return bytealg.IndexString(s, substr)    ...}// IndexByte returns the index of the first instance of c in s, or -1 if c is not present in s.func IndexByte(s string, c byte) int {    return bytealg.IndexByteString(s, c)}

IndexByteString 函數(shù)聲明在 src/internal/bytealg/indexbyte_native.go

//go:build 386 || amd64 || s390x || arm || arm64 || ppc64 || ppc64le || mips || mipsle || mips64 || mips64le || riscv64 || wasmpackage bytealg//go:noescapefunc IndexByte(b []byte, c byte) int//go:noescapefunc IndexByteString(s string, c byte) int

src/internal/bytealg/index_native.go

//go:build amd64 || arm64 || s390x || ppc64le || ppc64package bytealg//go:noescape// Index returns the index of the first instance of b in a, or -1 if b is not present in a.// Requires 2 <= len(b) <= MaxLen.func Index(a, b []byte) int//go:noescape// IndexString returns the index of the first instance of b in a, or -1 if b is not present in a.// Requires 2 <= len(b) <= MaxLen.func IndexString(a, b string) int

匯編實(shí)現(xiàn)在 src/internal/bytealg/indexbyte_amd64.s

#include "go_asm.h"    #include "textflag.h"    TEXT    ·IndexByte(SB), NOSPLIT, $0-40        MOVQ b_base+0(FP), SI        MOVQ b_len+8(FP), BX        MOVB c+24(FP), AL        LEAQ ret+32(FP), R8        JMP  indexbytebody<>(SB)    TEXT    ·IndexByteString(SB), NOSPLIT, $0-32        MOVQ s_base+0(FP), SI        MOVQ s_len+8(FP), BX        MOVB c+16(FP), AL        LEAQ ret+24(FP), R8        JMP  indexbytebody<>(SB)    // input:    //   SI: data    //   BX: data len    //   AL: byte sought    //   R8: address to put result    TEXT    indexbytebody<>(SB), NOSPLIT, $0        // Shuffle X0 around so that each byte contains        // the character we"re looking for.        MOVD AX, X0        PUNPCKLBW X0, X0        PUNPCKLBW X0, X0        PSHUFL $0, X0, X0...

src/internal/bytealg/index_amd64.s

#include "go_asm.h"    #include "textflag.h"    TEXT ·Index(SB),NOSPLIT,$0-56        MOVQ a_base+0(FP), DI        MOVQ a_len+8(FP), DX        MOVQ b_base+24(FP), R8        MOVQ b_len+32(FP), AX        MOVQ DI, R10        LEAQ ret+48(FP), R11        JMP  indexbody<>(SB)    TEXT ·IndexString(SB),NOSPLIT,$0-40        MOVQ a_base+0(FP), DI        MOVQ a_len+8(FP), DX        MOVQ b_base+16(FP), R8        MOVQ b_len+24(FP), AX        MOVQ DI, R10        LEAQ ret+32(FP), R11        JMP  indexbody<>(SB)    // AX: length of string, that we are searching for    // DX: length of string, in which we are searching    // DI: pointer to string, in which we are searching    // R8: pointer to string, that we are searching for    // R11: address, where to put return value    // Note: We want len in DX and AX, because PCMPESTRI implicitly consumes them    TEXT indexbody<>(SB),NOSPLIT,$0...

筆者驗(yàn)證了一下 IndexByte 和自定義通過(guò) for 循環(huán)實(shí)現(xiàn)建的差別:

func BenchmarkIndexByte(b *testing.B) {    b.Run("IndexByte", func(b *testing.B) {        b.ReportAllocs()        for i := 0; i < b.N; i++ {            str := testdata.TwitterJsonOut            n := 0            k := 0for {                j := strings.IndexByte(str[k:], "]")                if j < 0 {                    break                }                n++                k += j + 1            }            _ = n        }        b.SetBytes(int64(b.N))        b.StopTimer()    })    b.Run("for", func(b *testing.B) {        b.ReportAllocs()        str := testdata.TwitterJsonOut        for i := 0; i < b.N; i++ {            n := 0for i := 0; i < len(str); i++ {                if str[i] == "]" {                    n++                }            }            _ = n        }        b.SetBytes(int64(b.N))        b.StopTimer()    })}

結(jié)果如下:

BenchmarkIndexByte/IndexByteBenchmarkIndexByte/IndexByte-12    3072980    387.5 ns/op    7929621.19 MB/s    0 B/op           0 allocs/opBenchmarkIndexByte/forBenchmarkIndexByte/for-12          516663    2417 ns/op      213777.66 MB/s    0 B/op           0 allocs/op

由結(jié)果可知,SIMD 的加速性能還是挺好的。不過(guò),實(shí)際上如果 strings.IndexByte() 字符串很短 或 所查找的字符在字符串中大量存在的話,性能甚至?xí)?for 循環(huán)慢。這個(gè)可以自行驗(yàn)證一下。

5.7、 自定義SIMD優(yōu)化

如果感興趣,可以照著 Go 編譯器里的匯編抄,慢慢嘗試。github 上也有許多項(xiàng)目可以抄,比如:minio/sha256-simd

5.8、 隨意跳轉(zhuǎn)

這段代碼個(gè)人覺(jué)得很有意思,雖然有缺陷,但不失為一次大膽的嘗試。筆者另一篇掘金文章《關(guān)于 golang 錯(cuò)誤處理的一些優(yōu)化想法》 中有詳細(xì)說(shuō)明,實(shí)現(xiàn)原理類似 C 語(yǔ)言的棧溢出攻擊,就是替換函數(shù)的 RET 返回地址。

測(cè)試代碼如下:

func TestTagTry0(t *testing.T) {    defer func() {        fmt.Printf("1 -> ")    }()    tag, err1 := NewTag() // 當(dāng) tag.Try(err) 時(shí),跳轉(zhuǎn)此處并返回 err1    fmt.Printf("2 -> ")    if err1 != nil {        fmt.Printf("3 -> ")        return    }    defer func() {        fmt.Printf("4 -> ") // 由于的缺陷:這里 debug 下 defer 不內(nèi)聯(lián),會(huì)執(zhí)行;release 下 defer 內(nèi)聯(lián),不會(huì)執(zhí)行    }()    fmt.Printf("5 -> ")    err2 := errors.New("err2")    tag.Try(err2)  // 這里 err2!=nil,則會(huì)跳轉(zhuǎn)到 tag 創(chuàng)建處的下一行指令執(zhí)行,即 fmt.Printf("2 -> ")    fmt.Printf("6 -> ")    return}

測(cè)試結(jié)果:

# release 下 defer 內(nèi)聯(lián),不會(huì)輸出 42 -> 5 -> 2 -> 3 -> 1 -># debug 下 defer 不內(nèi)聯(lián),會(huì)輸出 42 -> 5 -> 2 -> 3 -> 4 -> 1
5.9 調(diào)用其他 package 的私有函數(shù)

通過(guò)過(guò)擺脫 golang 編譯器的一些約束,調(diào)用其他 package 的私有函數(shù)。如這篇文章《How to call private functions (bind to hidden symbols) in GoLang》。

上面 goid 的例子的最后,也講了通過(guò)匯編使用 package 私有的類型,即 DATA ·runtime_g_type+0(SB)/8,$type·runtime·g(SB) ,這里不在重復(fù)。

5.10 提高 CGO 調(diào)用的性能

我們知道,CGO 和系統(tǒng)調(diào)用時(shí),Go 語(yǔ)言需要把 goroutine 的調(diào)用棧切換回 g0 調(diào)用棧,并使用 g0 調(diào)用,整個(gè)過(guò)程性能損耗比較大。實(shí)際上,我們可以通過(guò)匯編適配 C 語(yǔ)言的 ABI 來(lái)直接調(diào)用 C 語(yǔ)言的函數(shù),參考 github 下的這個(gè)庫(kù): petermattis/fastcgo。不過(guò),這么做也有很大的局限性,比如導(dǎo)致棧溢出、因 goroutine 無(wú)法被搶占而影響 GC 性能等。

參考文檔:

《Go語(yǔ)言高級(jí)編程》第三章

??https://github.com/chai2010/advanced-go-programming-book/tree/master/ch3-asm??

Go 語(yǔ)言匯編的官方文檔

??https://go.dev/doc/asm??

GoFunctionsInAssembly

??https://lrita.github.io/images/posts/go/GoFunctionsInAssembly.pdf??

A Quick Guide to Go"s Assembler

??https://go.dev/doc/asm??

《Go語(yǔ)言原本》1.4 Plan 9 匯編語(yǔ)言

??https://golang.design/under-the-hood/zh-cn/part1basic/ch01basic/asm/??

關(guān)鍵詞:

相關(guān)新聞

Copyright 2015-2020   三好網(wǎng)  版權(quán)所有 聯(lián)系郵箱:435 22 640@qq.com  備案號(hào): 京ICP備2022022245號(hào)-21
92麻豆一区二区三区| 夜色17s精品人妻熟女av| 加勒比不卡在线视频| 国产精品无码无卡免费观| 天堂网免费在线电影| 青娱乐免费视频一二三| 精久久久久久久久久久久| 自拍偷拍色图亚洲天堂| 夫亡人妻被强干中文字幕| 午夜亚洲国产精品中字| 在线看日韩av不卡| 精品国产av虐杀两警花| 大尺度av毛片在线网址| 欧美男男在线观看视频网站| 中文字幕av特黄毛片| 91精品久久久久久久久99蜜臀 | 亚洲av三级电影在线观看| 97人妻av人人澡人人爽| 亚洲欧美综合另类最新| 日韩加勒比精品在线看| 熟女一区二区视频在线| 亚洲国产精品一区51动漫| 国产精品igao为爱寻找激情| 加勒比东京热绿帽人妻多人操| 人妻女侠被擒受辱记| 日韩欧美国产一区二区在线观看| 人妻激情偷乱一区二区三区av| 国产人妻777人伦精品hd超碰| 日韩国产欧美久久一区| 人妻少妇视频系列视频在线| 国产成人91色精品免费看片| 亚洲乱熟女一区二区三区影片| 性色蜜桃臀x88av天美传媒| 911精产国品一二三产区区| 免费绝清毛片a在线播放| 午夜福利在线不卡视频| 白白色在线免费视频发布视频| 国产91黑丝小视频在线观看| 亚洲 偷拍 自拍 欧美| 伊人免费观看视频一| 婷婷一区二区三区五月丁| 中文字幕欧美人妻在线.| 天天摸天天舔天天操天天日| 中文字幕久久久国产| 两个奶被揉得又硬又翘怎么回事| 免费看超污视频在线观看| 开心五月综合激情婷婷| 五十岁熟妇高潮喷水| 欧美日韩黄片免费在线观看| 五月的婷婷综合视频| 成年人免费福利在线| 亚洲中文字幕在线av| 加勒比东京热绿帽人妻多人操| 91大神在线免费观看视频| 55夜色66夜色亚洲精品| 不用付费特黄特色亚洲特级黄色片| 蜜桃tv一区二区三区| 日本香港韩国三级黄色| 亚洲欧洲无码一区2区无码| 91精品国产成人久久久久久| 国产人妻熟女ⅹxx丝袜| 琪琪日本福利伦理视频| 黄色大片一级老太太操逼| 最新国产午夜激情视频| 老司机在线视频福利观看| 亚洲欧美精品日韩偷拍| 日韩一区二区在线播放观看| 男女插鸡巴视频软件| 九九热精品视频在线播放| 国产成人在线观看hd| 色屁屁一区二区三区在线观看| 欧美一区日韩二区三区四区| 亚洲国产日韩a在线欧美| 最新国产精品综合网高清| 另类欧美激情校园春色| 免费成人av麻豆| 国内精品一区二区2021在线| 天天色 天天操 天天好逼| 一区二区三区国产精华液区别大吗 | 欧美成人短视频在线播放| av网页免费在线观看| 日本成人福利电影网| 在线 制服 中文字幕 日韩| 台湾18禁久久久久久久激情视频| 99在线视频精品观看高| 快色视频在线观看免费| www,日韩av,com| 综合久久伊人久久88| 老鸭窝在线毛片观看免费播放| 蜜乳av中文字幕一区二区| 久99久视频免费观看中文字幕| 精品欧美黑人一区二区三区| 久久av色噜噜ai换脸| 18福利视频在线观看| 60路70路日本熟妇| 欧美性感美女热舞视频| 亚洲成人激情在线综合| 国产黑色丝袜 在线日韩欧美| 人妻色综合aaaaaa网| 女人扒开逼让男人操| 亚洲少妇色小说综合| 91中文字幕视频网站| 日韩女同与成人用品电影免费看| 亚洲中文字幕在线视频观看二区| 亚洲一区在线视频观看地址| 91大神在线免费观看视频| 中文字幕在线免费观看成人| 日韩成人在线电影首页| 青青在线视频看看| 中文字幕 首页 人妻| 99精品久久99久久久久一| 国产三级自拍视频在线观看网站| 久久久国产精品免费视频网| 日本韩国欧美在线视频| 亚洲第一区av中文字幕| 欧美精品熟妇免费在线| 日韩成人在线电影首页| 免费24小时人妻视频| 天堂av在线最新地址| 乱子伦国产一区二区三区| 国产精美视频精品视频精品| 一区二区三区 国产日韩欧美| av无限看熟女人妻另类av| 一区二区三区观看在线| 青青草成人免费自拍视频| 国产福利一区二区三区在线观看 | 天天透天天舔天天操| 欧美日韩亚洲国产视频二区| 啪啪啪网站免费在线看| 超碰在线pro中文字幕| 色狠狠色综合久久久绯色| 2020国产激情视频在线观看| 久草久热这里只有精品| 五十岁熟妇高潮喷水| 精品国产污污污污免费观看| 二十四小时日本高清在线观看 | 91亚洲精品久久蜜桃| 亚洲成a人77777| 中文字幕 人妻 熟女| 91久久精品美女高潮喷水白浆| 国产av嗯嗯啊啊av| 中文字幕 中文字幕 亚洲| 狠狠操av一区二区三区| 92麻豆一区二区三区| 久久精品久久久久观看99水蜜桃| 成年人免费福利在线| 二十四小时日本高清在线观看 | 国长拍拍视频免费孕妇| 最新国产精品拍在线观看| 久草视频在线看免费| 快进来插我的逼嗯啊视频| 在线视频国产精品欧美| 最近日韩免费在线观看| 60路70路日本熟妇| 最新免费在线观看污视频| 中文字幕免费啪啪啪| 欧美肥妇久久久久久| 中文字幕熟女人妻丝袜丝在线| 黄色网络中文字幕日本| 91久久精品美女高潮喷水白浆| 国产精品黄色片大全| 亚洲成人 国产精品| 九十九步都是爱最后一步是尊严 | 天海翼亚洲一区在线观看| v天堂国产精品久久| 中文字幕欧美一区二区视频| 新亚洲天堂男子av| 亚洲码av一区二区三区| 黑鸡巴肏少妇逼视频| 蜜乳av中文字幕一区二区| 精品一区二区三区免费毛片W| 一区二区三区免费版在线| 加勒比不卡在线视频| 亚洲人成大片在线观看| 九九六视频,这里只有精品 | 亚洲日本欧美韩国另类综合| 四虎精品久久免费最新| 丰满人妻被猛烈进入中文字幕| av男人站在线观看| 欧美黑人1区2区3区| 大香焦一道本一区二区三区| 亚洲激情噜噜噜久久久| 青青操久久综合激情| 夜夜躁av麻豆男| 美女激情久久久久久久| 亚洲情色777中文字幕| 男女69视频在线观看免费| 亚欧洲乱码视频一二三区| 天天曰天天摸天天爽| 91中文字幕视频网站| 欧美区一区二区三视频| 全国熟妇精品一区二区免费视频| 国产精品久久久久久成人久| 大成色亚洲一二三区| 麻豆午夜激情在线观看| 亚洲av综合av一去二区三区| 亚洲一区二区三区四区入口| 久久精品国产亚洲av清纯| 国产高清在线观看av| 日本熟妇乱妇熟色视频| 在线国产精品欧美| 中字幕人妻熟女人妻a62v网| 黄版视频在线免费观看| 欧美亚洲精品色图网站| 中文字幕人妻一区色偷偷久久| 日本丰满熟妇浓密多毛| 韩国在线播放一区二区三区| ass亚洲熟女ass| 国产一区二区手机在线观看| 熟女俱乐部jukujoclub| 上床啪啪啪免费视频| 精产国品一二三产品区别97| 天天操天天干加勒比久久| jizzjizz国产精品传媒| 91青青青国产免费高清| 青青青在线观看国产| 黄版视频在线免费观看| 亚洲熟女一区二区三区250p | 国产视频成人自拍蝌蚪视频| 国产自拍偷拍在线精品| 可以免费观看日韩av| 亚洲AV无码久久精品国产一区老 | 亚洲国产美女主播在线观看| 精产国品一二三77777| 成人资源中文在线观看| 天天操天天舔天天射天天日天天干| 可以直接看av网站| 2019年中文字幕在线播放视频| 都市激情校园春色 亚洲| 日韩国产欧美久久一区| 欧美日本在线免费视频| 国产精品久久人人添| 在线观看视频免费一区二区三区| 日韩女同与成人用品电影免费看| 亚洲欧美综合另类最新| www国产亚洲精品久久久| 久久人妻人人草人人爽| 亚洲乱熟女一区二区三区影片 | 天天色 天天操 天天好逼| 制服丝袜 中文字幕 日韩 | 免费在线观看亚洲福利| 欧洲精品在线免费观看| 亚洲精品久久久人妻| 亚洲欧洲一区二区三区在线| 欧美一区二区播放视频| 瑟瑟干视频在线观看| 亚洲经典av中文字幕| 亚洲制服丝袜在线看| 55夜色66夜色亚洲精品| 亚洲春色av中文字幕| 荣立三等功退休有什么待遇| 国产 亚洲 欧美 自拍| 中文字幕 首页 人妻| 天天干夜夜操夜夜骑| 国产高清自拍偷拍在线| 91精品一区一区三区| 视频在线 一区二区| 欧美第一激情综合网欧美激情| 欧美成人屋影院在线视频观看| 亚洲在线免费观看18| 天天日天天玩天天摸| 50熟妇一区二区三区| 360偷拍蜜桃臀69式| 亚洲春色av中文字幕| 男人和女人的逼视频| 熟妇高潮久久久久久久| 伊人精品久久一区二区| 91 精品视频在线看| 日本福利视频网站导航| 国产成人在线观看hd| 天天插天天操天天射天天干| 男人电影天堂在线观看| 亚洲高清免费在线观看视频| 日本黄色一级电影网址| 精品日本少妇久久久| 日本丰满熟妇浓密多毛| 午夜宅男电影av网站| 嗯~嗯~啊啊啊~高潮了软件| 伊人精品久久一区二区| 18福利视频在线观看| 新香蕉视频香蕉视频2| 中文字幕人妻一区二区视频系列| 十八禁黄色免费污污污亚洲| 天堂网免费在线电影| 99精品久久精品一区二区| 一区二区三区四区 在线播放| 新亚洲天堂男子av| 豆豆专区操逼性视频在线| 国产免费久久精品99re丫丫| 亚洲国产精品 久久久| 色视频免费观看网址| 亚洲av网站一区二区三区| 天天做天天日天天搞| 色丁香久久激情综合网| 国产亚洲精品啪啪视频| 欧美成人性生活视频播放| 中文字幕麻绳捆绑的人妻| 日韩人妻一区二区三区在线观看| 东京热日韩av影片| 91久久久久久最新网站| 无码精品黑人一区二区老人| 亚洲欧美一级特黄大片| 日本成年视频在线免费观看| 黄色网络中文字幕日本| 最新日韩中文字幕免费在线观看| 中文字幕日韩首页欧美在线激情| 加勒比东京热绿帽人妻多人操| 狠狠操深爱婷婷综合一区| 91人妻人人做人人爽高清| 中文字幕人妻精品精品| 91亚洲最新蜜桃在线| 老鸭窝在线毛片观看免费播放 | 人妻超清中文字幕在线乱码| 夫亡人妻被强干中文字幕| 天天操天天舔天天射天天日天天干| 五月婷婷激情视频网| 日本老女人日比视频| 女生裸体视频免费网站 | 亚洲综合成人精品成人精品| 免费看一级高潮喷水片| 日韩激情文学在线视频| 高清av在线婷一区二区色日韩| 99精品久久99久久久久一| 黄色大片一级老太太操逼| 91精品久久久久久久99蜜月| 天天色 天天操 天天好逼| 快色视频在线观看免费| 大尺度久久久久久久| 精品国产污污污污免费观看| 少妇熟女天堂网av| 中文字幕人妻一区二区视频系列| 大片a免费观看在线视频观看| 国产激情免费在线视频| 久久精品四虎夜夜拍拍拍| 国产剧情av在线免费观看| 91色乱一区二区三区| 4438x亚洲最大的成人| 亚洲欧美小说中文字幕| 精品av天堂毛片久久久| 一区二区三区国产在线成人av| 欧美精品熟妇免费在线| 在线能看视频你懂的| www,日韩av,com| 国产视频成人一区二区| 超peng视频在线免费播放97| 亚洲最大先锋资源采集站| 97精品视频,全部免费| 97视频538在线观看| 超peng视频在线免费播放97| 开心五月综合激情婷婷| 亚洲少妇视频在线观看| 日本熟女0930视频| 亚洲第一页欧美第一页| 日韩一级视频一区二区三区| 久久久亚洲熟女一区二区| 天天操天天日天天碰| 亚洲一区二区偷拍女厕所| 午夜精品一区二区三区不卡顿| 五月激情婷婷四射基地| 高清国产美女a一级毛片| 亚洲欧洲无码一区2区无码| 韩国在线播放一区二区三区| 在线免费观看a视频免费| 91色老久久精品偷偷蜜臀| 欧美日本亚欧在线观看| 手机看片1024精品国产| 天天日天天亲天天操| 又爽又粗又猛又色又黄视频| 色噜噜噜噜色噜噜色合久一| 91久久精品美女高潮喷水白浆| 最新国产精品久久精品app| 九九热精品视频在线播放| 日韩av电影中文在线免费观看| 久久精品国产亚洲av热软件| 大陆中文字幕视频在线| 东京热日韩av在线| 综合激情网,激情五月| 欧美猛少妇色ⅹⅹⅹⅹⅹ猛叫| 妈妈的朋友中字在线免费观看| 国产精品视频网站污污污| 69视频在线精品国自产拍| 老司机伊人99久久精品| 18福利视频在线观看| 人妻免费视频黄片在线视频| 九九热精品视频在线播放| 丰满人妻熟女aⅴ一区| 老熟女xxxⅹhd老熟女性| 汤姆提醒30秒中转进站口| 九九热在线精品播放| 91精品久久久久久久99蜜月| 午夜8050免费小说| 亚洲欧美激情国产综合久久久| 中文字幕人妻一区二区视频系列 | 亚洲黄色免费在线观看网站| 92午夜免费福利视频www| 亚洲美女午夜激情视频在线观看| 亚洲欧美国产一本综合首页| 69精品互换人妻4p| 亚洲情色777中文字幕| 69精品人妻久久久久久久久久久| 亚洲激情噜噜噜久久久| 男人的天堂在线2025| 99精品久久99久久久久一| 亚洲精品久久久人妻| 天天透天天舔天天操| 日本人妻熟妇丰满成熟HD系列| 国产成人情侣av在线| 最新日韩av电影在线播放| 欧美一区二区播放视频| 特级aaaaa黄色片| 国产精品蝌蚪自拍视频| 日日夜夜免费视频精品| 日韩少妇免费在线播放| 成年男女免费视频网站无毒| 亚洲一区二区精品在线播放| 国产大桥未久一区二区| 9久re热视频在线精品| 男生和女生羞羞91在线看| 亚洲精品国产99999| 日韩男女视频网站在线观看| av一区二区三区四区五区在线| 久久一级片三上悠亚| 人妻人妻在线视频网站| 日本香港韩国三级黄色| 国产美女视频带a∨黄色片| 在线国产精品欧美| 一区二区三区国产精华液区别大吗| 亚洲美女午夜激情视频在线观看| 欧美男男在线观看视频网站| 天天色 天天操 天天好逼| 97人妻人人揉人人躁人人夜夜爽| 女人高潮潮呻吟喷水网站| 日韩三级精品电影久久久久| 亚洲色视频在线播放网站| 黑人黄色免费一级av| 亚洲激情噜噜噜久久久| 国产伦理二区三区在干嘛呢| 免费在线观看视频啪啪| 亚洲一区在线视频观看地址| 久久无码高清免费视频| 日本少妇三级交换做爰做| 五月在线视频免费播放91| 国产黄色主播网址大全在线播放| 91超精品碰国产在线观看| 国产精品剧情在线亚洲| 我爱搞在线观看视频| 夫妻黄色一级性生活片| 欧美大鸡吧男操女啊啊啊视频| 亚洲自拍偷拍一区二区中文字幕| 九九热在线精品播放| 亚洲国产精品青青草| 成人精品动漫一区二区| 一二区二区不卡视频| 亚洲熟女人妻自拍在线视频 | 亚洲欧美日韩电影一区| 手机看片福利一区二区三区四区 | 最新日韩中文字幕啪啪啪| 亚洲国产日韩欧美一区二区三区,| 中文字幕丰满子伦无码专区 | 伊人久久综合国产精品| 97cao在线视频| 亚洲字幕一区二区夜色av| 亚洲制服丝袜美腿在线| 美女把腿张开给男的捅| 中国特黄色性生活片| 免费观看在线中文字幕视频| 99亚偷拍自图区亚洲| 美女av色播在线播放| 亚洲美女色www色| 不卡在线一区二区三区| 亚洲av毛片在在线播放| 在线能看视频你懂的| 日本少妇人妻中文在线| 农村大炕有肉大屁股熟妇| 熟妇人妻丰满久久久久久久| 国产精品中文字幕丝袜| 极品内射老女人操逼视频| 精品国产污污污免费入口| 日韩av电影中文在线免费观看| 成人做爰av在线观看网站| 亚洲国产日韩欧美一区二区三区,| 久久久视频在线播放| 国产高清视频www夜色资源| 中国精品人妻一区二区| 国产精品性感美女视频| 最新日韩中文字幕免费在线观看| 福利小视频免费在线| 天天躁狠狠躁狠狠躁性色| av在线播放观看h| 中日韩又粗又硬又大精品| 国产午夜在线播放视频| 欧美色视频网址大全| 久久中文字幕av一区二区| 夜夜操夜夜爱夜夜摸| 我爱搞在线观看视频| 欧美情色av在线观看| 天天干天天操天天要| 92麻豆一区二区三区| jiee日本美女视频网站| 日韩免费黄色片在线观看| av在线播放观看h| 天天干夜夜撸天天操| 放荡人妻极品少妇全集| 亚欧洲乱码视频一二三区| 91国产精品乱码久久久久久| 久久人人爽人人爽人人av东京热 | 精品国模一区二区三区欧美| 男女爱爱好爽视频免费看| 国产大桥未久一区二区| 午夜精品视频免费观看| 亚洲成人三级黄色片| 中文字幕熟女乱一区二区| 亚洲欧洲一区二区三区在线| 亚洲va999天堂va| 美国男的操女孩的小嫩逼| 顶级欧美色妇xxxx| 欧美日本在线免费视频| 看女人大BB群伦交| 亚洲精品乱码久久久久app | 伊人网在线免费观看| 青青免费观看视频| 中国精品人妻一区二区| 午夜福利午夜福利影院| 黑人3p日本女优中出| 老熟妇一区二区三区v∧88| 久久久西西gogo日本美女人体| ass亚洲熟女ass| 狠狠操深爱婷婷综合一区| 亚洲综合另类欧美久久| 午夜呻吟亚洲精品中文字幕在上面| 日本亚洲午夜福利一区二区三区| 国产精品久久久99| 美女网站视频久久精品| 亚洲国产精品自拍偷拍视频在线 | 68福利精品在线视频| 国产美女视频带a∨黄色片| 天天操天天搞天天操| 亚洲欧美日韩中文视频| 美女网站视频久久精品| 啊~插得好快别揉我胸了视频| 久久99国产中文丝袜| 熟女人妻精品视频一区| 国产做A爱免费视频在线观看| 亚洲成a人77777| 高清国产美女a一级毛片| 亚洲 自拍 激情 另类| 精品人妻在线激情视频| 国产视频成人一区二区| 亚洲男人天堂最新网址大全 | 国产经典精品欧美日韩| 午夜精品老牛av一区二区三区| 国产资源在线观看二区| 色老头一区二区三区四区五区 | 青青草原在线播放日韩| 日本久久久久久黄色| 夜色17s精品人妻熟女av| 亚洲欧美激情国产综合久久久| 中字幕人妻熟女人妻a62v网| 人妻中文字幕亚洲在线| 天天日 天天舔 天天射| caopeng97在线观看视频| 亚洲欧美精品海量播放| 久久久久夜色国产精品电影| 二十四小时日本高清在线观看| 亚洲人成大片在线观看| 亚洲av手机免费在线| 一看就是假奶的av| 亚洲av中文无码网站| av在线观看视频免费| avjpm亚洲伊人久久| 久久免费视频ww一区| 蜜桃tv一区二区三区| 国模伊人久久精品一区二区三区| 欧美三区四区在线视频| 亚欧洲乱码视频一二三区| 日韩av电影中文在线免费观看| av激情四射五月婷婷| www一区二区91| 日本成年视频在线免费观看| 亚洲少妇色小说综合| 69国产精品成人aaaaa片| 熟女一区二区三区综合| 国产精品性感美女视频| 日本不卡 中文字幕| 欧美不卡一二三区精品| 人妻超清中文字幕在线乱码| 中国精品人妻一区二区| 99热99这里免费的精品| 人妻女侠被擒受辱记| 大成色亚洲一二三区| 天天干夜夜撸天天操| 丰满少妇_区二区三区| 另类欧美激情校园春色| 国产熟女五十路一区二区三区 | 大成色亚洲一二三区| 国产一区二区三区四区精| 天天色天天射天天日天天干| 国产精品久久久99| 人妻激情偷乱一区二区三区av| 色老头一区二区三区四区五区| 熟妇人妻av无码中文字幕| 亚洲欧美日韩中文视频| 99久久国语露脸国产精品| 欧美黑人性猛交小矮人| 1级黄色片在线观看| 夜夜人人干人人爱人人操| 美利坚合众国av天堂| 国产精品成人免费电影| 最新激情中文字幕视频| 韩日一级人添人人澡人人妻精品| 午夜精品老牛av一区二区三区| 精产国品一二三产品区别97| 抽插小穴啊啊啊视频| 久久人人爽人人爽人人av东京热| 伊人精品久久一区二区| 亚洲综合一区二区三区四区| 在线有码人妻自拍视频| 一区二区三区午夜福利在线| 中文字幕 中文字幕 亚洲| 黄色av 在线观看| 国产自拍偷拍在线精品| 亚洲av综合av一去二区三区| 亚洲国产精品 久久久| 亚洲在线免费观看18| 国产大桥未久一区二区| 国产精品网站亚洲发布| 美女av色播在线播放| 亚洲欧美日韩中文在线观看| 亚洲自拍偷拍av在线| 亚洲字幕一区二区夜色av| 色999日韩偷自拍拍免费| 天堂一区二区三区在线等| 伊人网在线欧美日韩在线| 中文字幕av人妻一区二区三区| 亚洲欧美一级特黄大片| 日本一区二区三区调教性奴视频 | 美女激情久久久久久久| 日本成年视频在线免费观看| 夏目彩春av在线看| 久久sm人妻中出精品一区二区| 人人妻人人狠人人爽| 蜜乳av一区二区三区免费观看| 日本成年视频在线免费观看| 天天干夜夜操91视频网站| av在线中文字幕在线| 在线免费观看a视频免费| 丰满少妇高潮喷水视频| jiee日本美女视频网站| 夜夜操天天干夜夜操| 黑人爆操女人免费视频| tushy一区二区三区视频| 99久9在线视频播放| 亚洲欧美韩国日本一区二区| 人妻激情偷乱一区二区三区av| 亚洲天堂色综合久久| 日本亚洲精品视频在线观看| 新香蕉视频香蕉视频2| 国产极品气质外围av| 亚洲精品乱码久久久久app| 国产精品 亚洲欧美 自拍偷拍| 亚洲乱码av一区二区蜜桃av| 亚洲午夜国产末满十八岁勿进网站| 青青青青午夜手机国产视频| 国产中文亚洲熟女日韩| 99精品视频在线在线观看| 日韩国产欧美一区二区三区粉嫩| 婷婷一区二区三区五月丁| 鸡巴插进美女的嫩小穴视频| 国产黄色主播网址大全在线播放| 香港日本台湾经典三级| 亚洲av毛片一区二区三区网| 日韩黄色在线观看网站上| 亚洲 偷拍 自拍 欧美| 99999久久久精品| 97精品国产91久久久| 亚洲va999天堂va| 精品国模一区二区三区欧美| 自拍偷拍色图亚洲天堂| 自拍偷拍 亚洲性图 欧美另类| 老熟女xxxⅹhd老熟女性| 丝袜美女诱惑佐佐三上| 亚洲欧美另类校园春色| 日本欧美国产在线一区| 亚洲欧洲一区二区三区在线| 天天干夜夜操91视频网站| 国产福利小视频在线观看网站| 女同大尺度视频网站在线观看| 99久久精品视频16| av人摸人人人澡人人超碰小说| 熟女一区二区三区综合| 亚洲自拍偷拍av在线| 自拍偷拍色图亚洲天堂| 在线观看网站伊人网| 亚洲色视频在线播放网站| 欧美在线观看视频欧美| 久久99精品热在线观看| 蜜乳视频一区二区三区| 国产,亚洲,欧美综合| 亚洲男人的天堂最新网址| 亚洲综合熟女乱中文| 伊人网在线欧美日韩在线| 久久精品久久久久观看99水蜜桃| 操人妻人妻天天爽天天偷| 69国产在线视频网站| 快色视频在线观看免费| 另类欧美激情校园春色| 9420高清视频在线观看国语版| 我爱搞在线观看视频| 青青青在线观看国产| 亚洲av网站一区二区三区| 夜夜操夜夜爱夜夜摸| 宅男噜噜噜66国产在线观看| 黄色av日韩在线观看| 亚洲成人,国产精品| 中文字幕日本一二三区| 人妻中文字幕亚洲在线| 午夜五十路久久福利| 国产毛片特级Av片| 国产精品国产三级在线高清观看| 国产欧美福利在线观看| 蜜乳视频一区二区三区| 亚洲欧美日韩电影一区| 国产激情在线观看一区二区三区| 国产白丝一区二区三区av| 男人资源站中文字幕| 中出小骚货在线观看| 国产福利小视频在线观看网站| 黄片操操操操操操c| 污网址在线观看视频| 日本老女人日比视频| 五月激情婷婷四射基地| 美利坚合众国av天堂| 欧美极品少妇高潮喷水| 亚洲制服丝袜美腿在线| 亚洲欧美另类校园春色| 亚洲乱熟女一区二区三区影片| 亚洲人成小说网站色| 九九热精品视频在线播放| 亚洲av三级电影在线观看| 国产精品igao为爱寻找激情| 国产白丝一区二区三区av| 国产精品蝌蚪自拍视频| 在线观看免费啪啪啪| 亚洲美女露隐私av一区二区精品| 日本人妻少妇xxxxxxx| 顶级欧美色妇xxxx| 熟妇人妻丰满久久久久久久| 大香蕉伊人97在线| 99久久免费播放在线观看视频| 999久久久人妻精品一区| av里面的动作是真进去吗| 都市激情校园春色 亚洲| 久久人妻人人草人人爽| 得得爱在线视频观看| 免费成人av麻豆| 97超碰人人爽人人做| 38av一区二区三区| 午夜福利在线不卡视频| 九九热视频1这里只有精品| 男女啪啪啪啪91av日韩| 得得爱在线视频观看| 9662av在线视频| 大香蕉在线欧美在线视频| 狠狠操av一区二区三区| 黄色网络中文字幕日本| 日本少妇熟女乱码一区二区| 强乱人妻中文字幕日本| 日韩黄色在线观看网站上| 日韩女同与成人用品电影免费看| 荣立三等功退休有什么待遇| 乱子伦国产一区二区三区| 国产成人在线观看视频播放| 人妻少妇视频系列视频在线| 亚洲一区亚洲二区成人福利| 天天日夜夜操人人爽| 亚洲人妻系列在线视频| 妈妈的朋友中字在线免费观看| 日本不卡 中文字幕| 亚洲av日韩久久网站| 日本黄色一级电影网址| 免费24小时人妻视频| 男人用大鸡巴狂操女人肉穴| 天天日夜夜操人人爽| 国产av剧变态维修工虐杀美女| 亚洲欧美激情国产综合久久久| 中文字字幕在线精品乱码| 猫咪亚洲中文在线中文字幕| 美女福利视频一区二区三区四区| 成人av中文字幕在线看| 亚洲av中文无码网站| 丰满少妇高潮喷水视频| 国产探花自拍亚洲av| 丰满少妇人妻一区二区三区蜜桃| 豆豆专区操逼性视频在线| 欧美黄色性视频网站| 中文字幕观看中文字幕免费| 成人精品动漫一区二区| 91精品综合久久久久久五月天| av在线免费在线观看| 亚洲天堂男人的天堂| av日韩视频在线观看| 凹凸视频一区二区在线观看| 男人av一区二区三区| 91亚洲最新蜜桃在线| 成人午夜麻豆大胆视频| 青青青青青爽视频在线| 4438全国成人免费视频| 国产视频成人一区二区| 一二区二区不卡视频| 1级黄色片在线观看| 亚洲妹妹我爱你在线观看| 亚洲 偷拍 自拍 欧美| 特级aaaaa黄色片| 伊人久久综合国产精品 | 天天想要天天操天天干| 精品国产人伦一区二区三区| 人妻色综合aaaaaa网| 午夜国产一区二区三区| 亚洲AV无码一二三四区在线播放| 人妻系列中文字幕大乳丰满人妻| 人妻色综合aaaaaa网| 欧美日韩不卡视频合集| 九十九步都是爱最后一步是尊严 | 中文人妻av一区二区三区| 九九九九九久久久国产| 亚洲av中文无码网站| 日本一区二区三区调教性奴视频| 自拍偷拍 国产激情| 精品人妻 色中文熟女 oo| 亚洲午夜国产末满十八岁勿进网站 | 欧美精品一区二区三区观看| 国产毛片特级Av片| 国产一级一国产一级毛片| 制服丝袜 中文字幕 日韩| 久久99精品热在线观看| 欧美成人屋影院在线视频观看| 日韩一级欧美一级片| 杜达雄啪啪毛片视频| 日韩女同与成人用品电影免费看| 不卡高清一区二区三区| 日本黄色一级电影网址| 四虎精品久久免费最新| 97成人老师在线视频| 亚洲美女色www色| 在线免费观看a视频免费| 亚洲成人自拍av在线| 亚洲中文字幕最新地址| 日韩人妻精品久久久久| 天天综合久久无人区| 日本一区二区三区的资源| 午夜在线观看一级毛| 亚洲综合在线视频在线播放| 97精品视频,全部免费| 欧美人与动欧交视频| 91精品视频在线观看视频| 日本一道中文字幕99| 九九六视频,这里只有精品| 黄片操操操操操操c| 嗯~嗯~啊啊啊~高潮了软件| 91精品综合久久久久久五月天| 97超碰人人爽人人做| 精品美女洗澡一区二区| 亚洲a级视频在线播放| 欧美熟女xx00视频| aa福利影视在线观看| 欧美亚洲精品色图网站| 18在线观看免费观看| 91佛爷视频在线观看| 少妇精品视频一区二区免费看| 天天干天天弄天天日| 国内精品一区二区2021在线| 亚洲欧美另类校园春色| 国产高清在线观看av| 国产av精品一区二区三区久久| 亚洲一区在线视频观看地址| 日韩人妻中文字幕二区| 色狠狠色综合久久久绯色| 精品国模一区二区三区欧美| 欧美精品激情在线不卡| 久久久久久久久久久久久国产| 最近日韩免费在线观看| 超碰在线免费观看视频97| 成年男女免费视频网站无毒| 99 re国产精品| 青青草一个释放的网站| 超碰在线免费观看视频97| 国产午夜在线播放视频| 精品高潮呻吟久久av| 亚洲国产中文字幕在线看| 快进来插我的逼嗯啊视频| 天天早上头和脸出汗是怎么办| 日本老女人日比视频| 鸡巴在里面福利视频在线观看| 97人妻在线视频自拍| 在线观看中文字幕少妇av| 国产精品黄色片大全| 在线观看免费啪啪啪| 日本欧美高清在线观看视频| 天天干夜夜爽狠狠操| 又爽又粗又猛又色又黄视频| 又粗又长又硬又黄又爽| 91中文字幕视频网站| 日韩欧美一区二区三区免费看 | 熟女一区二区三区综合| 91精品国产成人久久久久久| 日韩av熟妇在线观看| 丰满放荡熟妇在线播放| 蜜桃臀av在线一区二区| 2018中文字字幕人妻| 天天日天天亲天天操| 放荡人妻极品少妇全集| 裸日本资源在线午夜| 国产天堂av不卡网| 91大神在线免费观看视频| 国产精美视频精品视频精品| 亚洲综合天堂av网站在线观看| 亚洲成人激情在线综合| 荣立三等功退休有什么待遇| 99久久免费播放在线观看视频| 国产漂亮白嫩美女在线图片| 欧美视频亚洲视频在线| 亚洲最大的自拍偷拍网| 九九九九九久久久国产| 亚洲一区二区三区四区入口| 日本小视频一区二区| 99国产精品久久99久久久| 黄色片免费网站在线| 日本成年视频在线免费观看| 亚洲国产美女主播在线观看| 国产成人情侣激情视频| 国产精品久久久99| 在线观看中文字幕少妇av| 伊人网在线观看 视频一区| 久久sm人妻中出精品一区二区| 日本免费人爱做视频在线观看不卡| 亚洲 综合 欧美 一区| 亚洲中文字幕在线av| 欧美日韩高清片在线观看| 精品视频一区二区三区◇| 丰满少妇高潮喷水视频| 夜夜躁婷婷av蜜桃妖| 亚洲最大先锋资源采集站| 黑吊操欧美极品美女| 天堂网成人av电影| 亚洲熟女乱一区二区精品成人| 在线观看中文字幕少妇av| 亚洲精品色图1234| 不卡在线一区二区三区| 亚洲国产精品青青草| 成人午夜麻豆大胆视频| 国产av啊啊啊啊啊啊啊| 九九九九九久久久国产| 国产男女无套?免费网站下载| 九一精品人妻一区二区三区| 日韩久久不卡免费视频| 久草久热这里只有精品| 国产精品剧情在线亚洲| 大片a免费观看在线视频观看| 久久久久久免费观看av| 91九色91在线视频| 最新日韩av电影在线播放| 外国美女舔男人坤坤| 亚洲人成小说网站色| 亚洲无人区乱码中文字幕一区| 天天操天天干天天舔天天| 国产青青青青草免费在线视频| 精产国品一二三产品区别91| 国产大桥未久一区二区| 天天摸天天干夜夜操| 999国产精品视频免费看| 日本国产亚洲欧美色综合| 午夜在线观看一级毛| 日韩一级视频一区二区三区| 中文字幕av特黄毛片| 女人扒开逼让男人操| 人妻少妇精品二三区| 国产天堂av不卡网| 91精品久久久久久久久99蜜臀| 日本成人福利电影网| 亚洲精品一区二区gif| 青青操久久综合激情| 亚洲av在线免费播放| 日本人妻少妇xxxxxxx| 亚洲黄色免费在线观看网站| 最近最新最好看的中文字幕| 亚洲成人五月婷婷久久综合| 日本少妇三级交换做爰做| 可以免费观看日韩av| 欧美精品乱码99久久蜜桃免费| 在线有码人妻自拍视频| 久久久西西gogo日本美女人体| 丰满少妇_区二区三区| 综合激情网,激情五月| 高清av在线婷一区二区色日韩| 中文字幕国产一区在线视频| 久久久久夜色国产精品电影| 久操资源在线免费播放| 国产美女主播av在线| 亚洲一区视频中文字幕在线播放| 视频在线+欧美十亚洲曰本| 国产青青青青草免费在线视频| 欧美不卡一二三区精品| 99在线视频精品观看高| 亚洲欧美另类丝袜另类自拍| 熟女俱乐部jukujoclub| 老司机伊人99久久精品| 亚洲午夜精品视频节目| 亚洲宅男噜噜噜66在线观看| 最新国产午夜激情视频| 色狠狠色综合久久久绯色| 激情九月天在线视频| 91性高湖久久久久久久久久| 精品视频一区二区三区◇| 老熟女 露脸 嗷嗷叫| 黄色av网址在线播放| 婷婷一区二区三区五月丁| 久久久久高潮白浆久久| 欧美黄色性视频网站| 亚洲午夜精品视频节目| 亚洲人成大片在线观看| 国产精品剧情在线亚洲| 999国产精品视频免费看| 2019年中文字幕在线播放视频| 一区二区九日韩美女| 欧美vs亚洲vs日韩| 精品人妻人人做人人爽| 快使劲弄我视频在线播放 | 超碰在线pro中文字幕| 99re这里是国产精品首页| 成人人妻h在线观看| 男生和女生羞羞91在线看| 亚洲成人偷拍自拍在线| 裸日本资源在线午夜| 女女抠逼白虎白丝袜| 午夜精品视频免费观看| 午夜精品老牛av一区二区三区 | 欧美性受黑人猛交裸体视频| 51精品视频在线免费观看| 亚洲av综合av一去二区三区| 成人人妻h在线观看| 亚洲熟女一区二区三区250p| 天堂av国产av伦理av| 亚洲午夜国产末满十八岁勿进网站| 天天碰天天摸天天搞| 手机看电影一区二区三区| 欧美日韩福利视频网| 大香蕉在线欧美在线视频| 欧美大鸡吧男操女啊啊啊视频| 欧美vr专区日韩vr专区| 天天操,天天射,天天爽| 欧美久久蜜臀蜜桃资源吧| 亚洲成年人精品国产| avtt中文字幕手机版| 国产91精品福利系列| 4438x亚洲最大的成人| 日本老女人日比视频| 91精品视频在线观看视频| 天天早上头和脸出汗是怎么办 | 午夜宅男电影av网站| www,日韩av,com| 亚洲精品色图1234| 欧美亚洲国产一区二区| 午夜精品久久久久久久久久蜜桃| 性感人妻 中文字幕| 日韩欧美一区二区三区免费看| 中文字幕日韩人妻在线三区| 欧美人与动欧交视频| 91激情四射婷婷综合| 91系列视频在线播放| 超peng视频在线免费播放97| 国产亚洲精品啪啪视频| 日韩加勒比精品在线看| 成人免费电影二区三区| 国产极品气质外围av| 成人免费电影二区三区| 亚洲国产综合久久精品| av男人站在线观看| av人摸人人人澡人人超碰小说| 亚洲精品一区二区gif| 松本菜奈实最新av在线| 精品日本少妇久久久| 精品精品精品精品精品污污污污| 久久内射天天玩天天懂色| 亚洲美女色www色| 久久久久国产精品二区| 亚洲女人自熨在线视频| 亚洲激情噜噜噜久久久| 92麻豆一区二区三区| 天天天天天天天天日日日| 99在线视频精品观看高| 天天日天天干天天日天天干天天| 亚洲理论在线a中文字幕97| 99热99这里免费的精品| 豆豆专区操逼性视频在线| 色欲AV亚洲AV无码精品| 欧美成人性生活视频播放| 国产成人91色精品免费看片| 中文字幕欧美人妻在线.| 欧美精品乱码99久久蜜桃免费 | 岳的大肥屁熟妇五十路| 51精品视频在线免费观看| 亚洲成人三级黄色片| 日韩欧美中文字幕老司机三分钟 | 性色蜜桃臀x88av天美传媒| 4438全国成人免费视频| 69xx精品久久久久| 亚洲男人的天堂最新网址| 中文字幕久久久国产| 美女露阴道让男人捅| 51精品视频在线免费观看| iga肾三级算严重吗| 亚洲一区二区精品在线播放| 日本少妇人妻凌辱在线| 亚洲人成大片在线观看| 日韩欧美中文字幕老司机三分钟| 中文字幕亚洲乱码精品无限| 99热在线只有的精品| 亚洲综合在线视频在线播放| 国产在线小视频一区二区| 人妻激情综合久久久久蜜桃| 男生和女生羞羞91在线看| 最新日韩av电影在线播放| 偷拍熟女大胆免费视频| 97香蕉久久国产超碰| 91麻豆精品国产在线| 人妻激情综合久久久久蜜桃| 亚洲熟女少妇中文字幕系列| 欧美一级aaaaaaa片| 日韩无码国产一区二区| 亚洲精品久久久人妻| 在线能看视频你懂的| 久久人人爽人人爽人人av东京热| 欧美一区二区播放视频| 国产熟妇色xxⅹ交白浆视频| 久操资源在线免费播放| 极品内射老女人操逼视频| 天天摸天天舔天天操天天日| 自拍偷拍视频亚洲一区| 日本香港韩国三级黄色| 92在线播放观看视频| 亚洲欧美国产一本综合首页| 新亚洲天堂男子av| 在线播放 日韩 av| 男女插鸡巴视频软件| 久久久久国产精品二区| 黑人侵犯人妻森泽佳奈| 日本欧美国产在线一区| 国产精品久久人人添| 久久久久久久岛国免费观看| 国产探花自拍亚洲av| 在线免费观看视频18| 久久久久久a女人处女| 亚洲经典av中文字幕| 韩国毛片w妈妈的朋友7| 日韩成人精品久久久免费看| 欧美成人性生活视频播放| 自拍丝袜国产欧美日韩| 欧美日韩亚洲国产视频二区| 日本福利网站一区二区| 欧美成人短视频在线播放| 91九色人妻在线播放| 欧美久久一区二区伊人| 青青在线视频看看| 欧美vr专区日韩vr专区| 男生用大肌巴操美女骚穴| 日韩成人免费观看电影| 91在线九色porny| 特级aaaaa黄色片| 亚洲最大的自拍偷拍网| av 一区二区三区 熟女| 网友自拍第一页99热| 亚洲av网站一区二区三区| 欧美日韩黄片免费在线观看| 亚洲精品一区二区gif| 国产精品视频网站污污污 | 亚洲激情噜噜噜久久久| 日本特级黄片免费观看| 视频在线+欧美十亚洲曰本| 免费在线观看视频啪啪| 日本人妻熟妇丰满成熟HD系列| 公侵犯人妻中文字幕巨| 日本韩国欧美在线视频| 国产精品黄色片大全| 天天插天天干天天狠| 男人电影天堂在线观看| 青娱乐不卡视频在线| 国产自拍偷拍在线精品| 偷拍熟女大胆免费视频| 岛国av成人午夜高清| 欧美一级特黄大片做受99| 免费看超污视频在线观看| 欧美熟女xx00视频| 伊人情人成综合视频| 亚洲精品色图1234| 视频在线+欧美十亚洲曰本| 日韩黄色在线观看网站上| 天天综合久久无人区| 亚洲综合首页综合在线观看| 久久久久久a女人处女| 日本特级黄片免费观看| 一区二区三区四区视频精品免费| 农村大炕有肉大屁股熟妇| 裸日本资源在线午夜| 中文字幕中文字幕在线中…一区| 亚洲乱码av一区二区蜜桃av| 中文字幕亚洲无线乱码| 嗯~嗯~啊啊啊~高潮了软件| 手机看片1024精品国产| 最近日韩免费在线观看| 天天色天天射天天日天天干| 国产精品内射婷婷一级| 成年人免费黄色av| 视频自拍偷拍视频自拍 | 60路70路日本熟妇| 91在线九色porny| 91精品91久久久久| 91精品国产91久久久久久密臀| 久久人人爽人人爽人人av东京热| 最近在线中文字幕免费| 久久综合狠狠综合久久综| 天堂av在线最新地址| 国产高清在线观看av| 在线免费观看视频18| 色屁屁一区二区三区在线观看| www一区二区91| 欧美日韩黄片免费在线观看| 一区二区三区婷婷中文字幕| 国产女人18毛片水真多精选| 国产人妻熟女ⅹxx丝袜| 91精品久久久久久久99蜜月| 97人妻av人人澡人人爽| 日本少妇丰满大bbb的小乳沟| 成人精品影视一区二区| 久久久久九九九九九12| 日本不卡 中文字幕| av无限看熟女人妻另类av| 人人人妻人人人妻精品少妇| 欧美极品少妇高潮喷水| 黑人大巨屌操美女逼| 日日夜夜免费视频精品| 老熟女 露脸 嗷嗷叫| www一区二区91| 亚洲乱码av一区二区蜜桃av| 天天在线播放日韩av| 汤姆提醒30秒中转进站口| 情趣视频在线观看91| 亚洲午夜熟女在线观看| 午夜3p福利视频合集| 在线免费观看欧美小视频| 中出小骚货在线观看| av天堂a亚洲va天堂va里番| 在线观看视频免费一区二区三区| 亚洲妹妹我爱你在线观看| 国产人妻777人伦精品hd超碰| 男人资源站中文字幕| 欧美丝袜亚洲国产日韩| 亚洲天堂av最新在线| 日韩在线 中文字幕| 亚洲精品色图1234| 天天弄天天草天天日天天| 天天干天天日天天弄| 国产成人在线观看视频播放| 欧美日韩黄片免费在线观看| 波多野结衣在线一区别| 第一福利视频在线观看| 久久无码高清免费视频| 国产精美视频精品视频精品| 天天做天天日天天搞| 天天干夜夜撸天天操| 日本高清久久人人爽| 人人妻人人爽人人摸| 日本国产亚洲欧美色综合| 青青草原在线播放日韩| 国产成人深夜福利短视频99| 最新日韩中文字幕免费在线观看| 国产熟女五十路一区二区三区| 在线 制服 中文字幕 日韩| 久久精品国产亚洲av清纯| jizzjizz国产精品传媒| www一区二区91| 欧美日韩成人高清中文网| 亚洲自拍偷拍av在线| 国产白丝一区二区三区av| 日韩av熟妇在线观看| 97视频人人爱麻豆| 真人一进一出抽搐大尺度视频| 麻豆白洁少妇在线播放| 人妻系列级片在线观看视频| 男女69视频在线观看免费| 黑人3p日本女优中出| 久久精品久久久久观看99水蜜桃| 国产91黑丝小视频在线观看| 国际精品熟女一区二区| 伊人综合在线视频免费观看| 精品一区二区三区免费毛片W| 夜夜骚av一二三区| 久久一级片三上悠亚| 日本黄页在线观看视频| 久久av色噜噜ai换脸| 亚洲午夜精品视频节目| 欧美日韩福利视频网| 国产人妻熟女ⅹxx丝袜| 欧美vr专区日韩vr专区| 中文字幕观看中文字幕免费 | 麻豆国产精品777777在| 区一区二区三免费观看视频| 2020国产成人精品视频| 久久99嫩草99久久精品| 亚洲精品9999蜜桃| 亚洲一区在线视频观看地址| 国产av精品一区二区三区久久| 国产激情一区二区视频| 国产,亚洲,欧美综合| 四虎国产精品国产精品国产精品| 老色鬼精品视频在线观看播放| 91中文字幕视频网站| 5d蜜桃臀女无痕裸感| 无人区一码二码三码区别在哪| 欧美激情视频第一页| 182tv精品免费在线观看| 欧美成人一二三在线网| 青青草原在线播放日韩| 午夜精品视频免费观看| 中文字幕 一区二区在线观看| 国产欧美福利在线观看| 福利小视频免费在线| 亚洲综合在线视频在线播放| 亚洲宅男噜噜噜66在线观看| 一二三四区国产在线观看| 国产大桥未久一区二区| 亚洲欧美国产人成在线| 一级做性色a爱片久久片| 午夜久久久久久av五月| 亚洲高清一区二区三区久久| 360偷拍蜜桃臀69式| 欧美成人一二三在线网| 亚洲色视频在线播放网站| 亚洲成a人77777| 女生裸体视频免费网站| a级黄片免费观看| 欧美精品乱码99久久蜜桃免费| 精品国产久久久久午夜精品av| 免费在线观看视频啪啪| 午夜久久人妻一级内射av网址| 天天操天天舔天天做| 亚洲 自拍 激情 另类| ass亚洲熟女ass| 91精品资源在线观看| av一区二区三区蜜桃| 大乳人妻一区二区三区| 都市激情校园春色 亚洲| 美女张开腿给男人桶爽的软件| 免费成人av麻豆| 日本男女免费福利视频| 亚洲国产综合久久精品| 360偷拍蜜桃臀69式| 亚洲天堂色综合久久| 99精品久久一区二区| 国产欧美福利在线观看| 91精品资源在线观看| 国际精品熟女一区二区| 91在线九色porny| 中文字幕在线免费观看人妻| 一级做性色a爱片久久片| 亚洲自拍偷拍av在线| 美国十次了亚洲天堂网国产| 午夜情色一区二区三区| 漂亮人妻口爆久久精品| 大尺度久久久久久久| 久久人人爽人人爽人人av东京热 | 人妻在线中文视频视频| 亚洲第一页欧美第一页| 中文字幕 首页 人妻| jiee日本美女视频网站| 久草视频在线看免费| 色欲天天媓色媓香视频综合网| 69视频在线精品国自产拍 | 人妻被强av系列一区二区| 欧美亚洲精品色图网站| 午夜免费福利老司机| 可以免费观看日韩av| 最新久久这里只有精品| 夏目彩春av在线看| 亚洲在线观看中文字幕av| 99女福利女女视频在线播放| 91污污在线观看视频| 一区二区三区婷婷中文字幕| 国产激情视频在线观看的 | 久99久视频免费观看中文字幕| 亚洲中文字幕在线视频观看二区 | 美女av色播在线播放| 国内精品一区二区2021在线 | 最近最新欧美日韩精品| 色丁香久久激情综合网| 大鸡扒操大逼大片免费关看| 日本少妇精品免费视频| 麻豆午夜激情在线观看| 2021国产在线视频| 午夜福利午夜福利影院| 丰满少妇_区二区三区| 亚洲av中文免费在线| 超碰在线免费观看视频97| 在线视频国产精品欧美| 日本老熟妇av老熟妇| 国产视频成人一区二区| 亚洲情色777中文字幕| 92麻豆一区二区三区| 久久精品国产亚洲av清纯| 男女插鸡巴视频软件| 顶级欧美色妇xxxx| 中文字幕在线字幕乱码怎么设置| 亚洲黄色成人一级片| 18福利视频在线观看| 丰满少妇_区二区三区| 欧美vr专区日韩vr专区| 亚洲高清免费在线观看视频| 豆豆专区操逼性视频在线| 国产一级一国产一级毛片| 日本韩国欧美在线视频| 亚成区一区二区人妻熟女| 日韩成人在线电影首页| 亚洲综合成人精品成人精品| 亚洲国内精品久久久久久久| 91色哟哟视频在线观看| 91精品久久久久久久99蜜月 | 中文字幕在线免费观看人妻| 亚洲一区在线视频观看地址| 韩国在线播放一区二区三区| 人人妻人人狠人人爽| 天天操天天射天天操天天日| 强乱人妻中文字幕日本| 日韩精品视频一区二区三区在线| 裸露视频免费在线观看| 亚洲国产日韩欧美一区二区三区,| 天天操天天搞天天操| 两个人在一起靠逼啊啊啊| 亚洲无人区乱码中文字幕一区| 杜达雄啪啪毛片视频| 亚洲AV无码久久精品国产一区老| 黄片视频免费观看视频| 亚洲男人天堂最新网址大全 | 操人妻人妻天天爽天天偷| 国模伊人久久精品一区二区三区| 91中文字幕视频网站| 老熟女 露脸 嗷嗷叫| 国产成人91色精品免费看片| 夜夜人人干人人爱人人操| 精品国模一区二区三区欧美| 国产91精品福利系列| 日本欧美高清在线观看视频| 欧美性受黑人猛交裸体视频| 黄色av 在线观看| 国产精品剧情在线亚洲| 久久中文字幕av一区二区| 国际日韩日韩日韩日韩日韩| 99免费观看在线视频| 中文字幕免费啪啪啪| 97cao在线视频| 第一福利视频在线观看| 欧美男男在线观看视频网站| 中文字幕 人妻 熟女| 中文字字幕在线精品乱码| 9662av在线视频| 夜夜爽夜夜操夜夜爱| 中文字幕人妻一区二区视频系列 | 99久9在线视频播放| 国产高清自拍偷拍在线| 亚成区一区二区人妻熟女| 日韩欧美一区二区三区免费看| 97超碰人人爽人人做| 自拍偷拍 国产激情| 国产91九色视频在线观看| 欧美强奸视频在线观看| 国产高清视频www夜色资源| 国语精品视频自产自拍| 网站在线观看蜜臀91| 4438x亚洲最大的成人| 熟女人妻少妇一区二区| 男生和女生羞羞91在线看| 亚洲色大WWW永久网站| 亚洲熟女人妻自拍在线视频 |