溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Golang匯編控制流的方法介紹

發布時間:2023-11-03 10:53:08 來源:億速云 閱讀:102 作者:栢白 欄目:開發技術

今天小編給大家分享的是Golang匯編控制流的方法介紹,相信很多人都不太了解,為了讓大家更加了解,所以給大家總結了以下內容,一起往下看吧。一定會有所收獲的哦。

順序執行

順序執行是我們比較熟悉的工作模式,類似俗稱流水賬編程。所有不含分支、循環和goto語言,并且每一遞歸調用的Go函數一般都是順序執行的。

比如有如下順序執行的代碼:

func main() {
	var a = 10
	println(a)
	var b = (a+a)*a
	println(b)
}

我們嘗試用Go匯編的思維改寫上述函數。因為X86指令中一般只有2個操作數,因此在用匯編改寫時要求出現的變量表達式中最多只能有一個運算符。同時對于一些函數調用,也需要改用匯編中可以調用的函數來改寫。

第一步改寫依然是使用Go語言,只不過是用匯編的思維改寫:

func main() {
	var a, b int
	a = 10
	runtime.printint(a)
	runtime.printnl()
	b = a
	b += b
	b *= a
	runtime.printint(b)
	runtime.printnl()
}

首先模仿C語言的處理方式在函數入口處聲明全部的局部變量。然后將根據MOV、ADD、MUL等指令的風格,將之前的變量表達式展開為用=、+=*=幾種運算表達的多個指令。最后用runtime包內部的printint和printnl函數代替之前的println函數輸出結果。

經過用匯編的思維改寫過后,上述的Go函數雖然看著繁瑣了一點,但是還是比較容易理解的。下面我們進一步嘗試將改寫后的函數繼續轉譯為匯編函數:

TEXT ·main(SB), $24-0
    MOVQ $0, a-8*2(SP) // a = 0
    MOVQ $0, b-8*1(SP) // b = 0

    // 將新的值寫入a對應內存
    MOVQ $10, AX       // AX = 10
    MOVQ AX, a-8*2(SP) // a = AX

    // 以a為參數調用函數
    MOVQ AX, 0(SP)
    CALL runtime·printint
    CALL runtime·printnl

    // 函數調用后, AX/BX 可能被污染, 需要重新加載
    MOVQ a-8*2(SP), AX // AX = a
    MOVQ b-8*1(SP), BX // BX = b

    // 計算b值, 并寫入內存
    MOVQ AX, BX        // BX = AX  // b = a
    ADDQ BX, BX        // BX += BX // b += a
    MULQ AX, BX        // BX *= AX // b *= a
    MOVQ BX, b-8*1(SP) // b = BX

    // 以b為參數調用函數
    MOVQ BX, 0(SP)
    CALL runtime·printint
    CALL runtime·printnl

    RET

匯編實現main函數的第一步是要計算函數棧幀的大小。因為函數內有a、b兩個int類型變量,同時調用的runtime·printint函數參數是一個int類型并且沒有返回值,因此main函數的棧幀是3個int類型組成的24個字節的棧內存空間。

在函數的開始處先將變量初始化為0值,其中a-8*2(SP)對應a變量、a-8*1(SP)對應b變量(因為a變量先定義,因此a變量的地址更?。?。

然后給a變量分配一個AX寄存器,并且通過AX寄存器將a變量對應的內存設置為10,AX也是10。為了輸出a變量,需要將AX寄存器的值放到0(SP)位置,這個位置的變量將在調用runtime·printint函數時作為它的參數被打印。因為我們之前已經將AX的值保存到a變量內存中了,因此在調用函數前并不需要在進行寄存器的備份工作。

在調用函數返回之后,全部的寄存器將被視為被調用的函數修改,因此我們需要從a、b對應的內存中重新恢復寄存器AX和BX。然后參考上面Go語言中b變量的計算方式更新BX對應的值,計算完成后同樣將BX的值寫入到b對應的內存。

最后以b變量作為參數再次調用runtime·printint函數進行輸出工作。所有的寄存器同樣可能被污染,不過main馬上就返回不在需要使用AX、BX等寄存器,因此就不需要再次恢復寄存器的值了。

重新分析匯編改寫后的整個函數會發現里面很多的冗余代碼。我們并不需要a、b兩個臨時變量分配兩個內存空間,而且也不需要在每個寄存器變化之后都要寫入內存。下面是經過優化的匯編函數:

TEXT ·main(SB), $16-0
    // var temp int

    // 將新的值寫入a對應內存
    MOVQ $10, AX        // AX = 10
    MOVQ AX, temp-8(SP) // temp = AX

    // 以a為參數調用函數
    CALL runtime·printint
    CALL runtime·printnl

    // 函數調用后, AX 可能被污染, 需要重新加載
    MOVQ temp-8*1(SP), AX // AX = temp

    // 計算b值, 不需要寫入內存
    MOVQ AX, BX        // BX = AX  // b = a
    ADDQ BX, BX        // BX += BX // b += a
    MULQ AX, BX        // BX *= AX // b *= a

    // ...

首先是將main函數的棧幀大小從24字節減少到16字節。唯一需要保存的是a變量的值,因此在調用runtime·printint函數輸出時全部的寄存器都可能被污染,我們無法通過寄存器備份a變量的值,只有在棧內存中的值才是安全的。然后在BX寄存器并不需要保存到內存。其它部分的代碼基本保持不變。

if/goto跳轉

早期的Go雖然提供了goto語句,但是并不推薦在編程中使用。有一個和cgo類似的原則:如果可以不使用goto語句,那么就不要使用goto語句。Go語言中的goto語句是有嚴格限制的:它無法跨越代碼塊,并且在被跨越的代碼中不能含有變量定義的語句。雖然Go語言不喜歡goto,但是goto確實每個匯編語言碼農的最愛。goto近似等價于匯編語言中的無條件跳轉指令JMP,配合if條件goto就組成了有條件跳轉指令,而有條件跳轉指令正是構建整個匯編代碼控制流的基石。

為了便于理解,我們用Go語言構造一個模擬三元表達式的If函數:

func If(ok bool, a, b int) int {
	if ok { return a } else { return b }
}

比如求兩個數最大值的三元表達式(a>b)?a:b用If函數可以這樣表達:If(a>b, a, b)。因為語言的限制,用來模擬三元表達式的If函數不支持范型(可以將a、b和返回類型改為空接口,使用會繁瑣一些)。

這個函數雖然看似只有簡單的一行,但是包含了if分支語句。在改用匯編實現前,我們還是先用匯編的思維來重寫If函數。在改寫時同樣要遵循每個表達式只能有一個運算符的限制,同時if語句的條件部分必須只有一個比較符號組成,if語句的body部分只能是一個goto語句。

用匯編思維改寫后的If函數實現如下:

func If(ok int, a, b int) int {
	if ok == 0 { goto L }
	return a
L:
	return b
}

因為匯編語言中沒有bool類型,我們改用int類型代替bool類型(真實的匯編是用byte表示bool類型,可以通過MOVBQZX指令加載byte類型的值)。當ok參數非0時返回變量a,否則返回變量b。我們將ok的邏輯反轉下:當ok參數為0時,表示返回b,否則返回變量a。在if語句中,當ok參數為0時goto到L標號指定的語句,也就是返回變量b。如果if條件不滿足,也就是ok非0,執行后面的語句返回變量a。

上述函數的實現已經非常接近匯編語言,下面是改為匯編實現的代碼:

TEXT ·If(SB), NOSPLIT, $0-32
    MOVQ ok+8*0(FP), CX // ok
    MOVQ a+8*1(FP), AX  // a
    MOVQ b+8*2(FP), BX  // b

    CMPQ CX, $0         // test ok
    JZ   L              // if ok == 0, skip 2 line
    MOVQ AX, ret+24(FP) // return a
    RET

L:
    MOVQ BX, ret+24(FP) // return b
    RET

首先是將三個參數加載到寄存器中,ok參數對應CX寄存器,a、b分別對應AX、BX寄存器。然后使用CMPQ比較指令將CX寄存器和常數0進行比較。如果比較的結果為0,那么下一條JZ為0時跳轉指令將跳轉到L標號對應的指令,也就是返回變量b的值。如果比較的結果不為0,那么JZ指令講沒有效果,繼續執行后的指令,也就是返回變量a的值。

在跳轉指令中,跳轉的目標一般是通過一個標號表示。不過在有些通過宏實現的函數中,更希望通過相對位置跳轉,這時候可以通過PC寄存器來計算跳轉的位置。

for循環

Go語言的for循環有多種用法,我們這里只選擇最經典的for結構來討論。經典的for循環由初始化、結束條件、迭代步長三個部分組成,再配合循環體內部的if條件語言,這種for結構可以模擬其它各種循環類型。

基于經典的for循環結構,我們定一個LoopAdd函數,可以用于計算任意等差數列的和:

func LoopAdd(cnt, v0, step int) int {
	result := v0
	for i := 0; i < cnt; i++ {
		result += step
	}
	return result
}

比如1+2+...+100可以這樣計算LoopAdd(100, 1, 1),10+8+...+0可以這樣計算LoopAdd(5, 10, -2)?,F在采用前面if/goto類似的技術來改造for循環。

新的LoopAdd函數只有if/goto語句構成:

func LoopAdd(cnt, v0, step int) int {
	var i = 0
	var result = 0
LOOP_BEGIN:
	result = v0
LOOP_IF:
	if i < cnt { goto LOOP_BODY }
	goto LOOP_END
LOOP_BODY
	i = i+1
	result = result + step
	goto LOOP_IF
LOOP_END:
	return result
}

函數的開頭先定義兩個局部變量便于后續代碼使用。然后將for語句的初始化、結束條件、迭代步長三個部分拆分為三個代碼段,分別用LOOP_BEGIN、LOOP_IF、LOOP_BODY三個標號表示。其中LOOP_BEGIN循環初始化部分只會執行一次,因此該標號并不會被引用,可以省略。最后LOOP_END語句表示for循環的結束。四個標號分隔出的三個代碼段分別對應for循環的初始化語句、循環條件和循環體,其中迭代語句被合并到循環體中了。

下面用匯編語言重新實現LoopAdd函數

// func LoopAdd(cnt, v0, step int) int
TEXT ·LoopAdd(SB), NOSPLIT, $0-32
	MOVQ cnt+0(FP), AX   // cnt
	MOVQ v0+8(FP), BX    // v0/result
	MOVQ step+16(FP), CX // step
LOOP_BEGIN:
	MOVQ $0, DX          // i
LOOP_IF:
	CMPQ DX, AX          // compare i, cnt
	JL   LOOP_BODY       // if i < cnt: goto LOOP_BODY
	goto LOOP_END
LOOP_BODY:
	ADDQ $1, DX          // i++
	ADDQ CX, BX          // result += step
	goto LOOP_IF
LOOP_END:
	MOVQ BX, ret+24(FP)  // return result
	RET

其中v0和result變量復用了一個BX寄存器。在LOOP_BEGIN標號對應的指令部分,用MOVQ將DX寄存器初始化為0,DX對應變量i,循環的迭代變量。在LOOP_IF標號對應的指令部分,使用CMPQ指令比較AX和AX,如果循環沒有結束則跳轉到LOOP_BODY部分,否則跳轉到LOOP_END部分結束循環。在LOOP_BODY部分,更新迭代變量并且執行循環體中的累加語句,然后直接跳轉到LOOP_IF部分進入下一輪循環條件判斷。LOOP_END標號之后就是返回返回累加結果到語句。

循環是最復雜的控制流,循環中隱含了分支和跳轉語句。掌握了循環基本也就掌握了匯編語言到寫法。掌握規律之后,其實匯編語言編程會變得異常簡單。

關于Golang匯編控制流的方法介紹就分享到這里了,希望以上內容可以對大家有一定的參考價值,可以學以致用。如果喜歡本篇文章,不妨把它分享出去讓更多的人看到。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

go
AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女