Go實現啟動引數載入

大雄45發表於2022-01-21
導讀 剛學Go的同學一定思考過 Go 程式的啟動過程,關於這個問題可以看饒大的文章Go程式是怎樣跑起來的。今天我們將問題縮小,來學習Go程式是怎麼載入啟動引數,以及如何進行引數解析。

Go實現啟動引數載入Go實現啟動引數載入

C 引數解析

學習過 C 語言的童鞋,一定對 argc 和 argv 不會陌生。

C 程式總是從主函式 main 開始執行的,而在帶引數的主函式中,依照慣例,會使用 argc 和 argv 的命名作為主函式引數。

其中,argc (argument count)代表的是 行引數個數,argv(argument value) 是用來存放指向引數的指標陣列。

#includeint main(int argc, char *argv[]) 
{ 
 printf("argc = %d\n",argc); 
 printf("argv[0] = %s, argv[1] = %s, argv[2] = %s \n", argv[0], argv[1], argv[2]); 
 return 0; 
}

編譯執行以上 C 程式碼,得到輸出如下

$ gcc c_main.c -o main 
$ ./main foo bar sss ddd 
argc = 5 
argv[0] = ./main, argv[1] = foo, argv[2] = bar

那在 Go 語言中,又該如何獲取 行引數呢?

os.Args 載入

同 C 一樣,Go 程式也是從 main 主函式開始(使用者層)執行,但主函式中並沒有定義 argc 和 argv。

我們可以透過 os.Args 函式,獲取命令列引數。

package main 
 
import ( 
 "fmt" 
 "os" 
) 
 
func main() { 
 for i, v := range os.Args { 
  fmt.Printf("arg[%d]: %v\n", i, v) 
 } 
}

編譯執行 Go 函式

 $ go build main.go 
 $ ./main foo bar sss ddd 
arg[0]: ./main 
arg[1]: foo 
arg[2]: bar 
arg[3]: sss 
arg[4]: ddd

同 C 一樣,第一個引數也是代表可執行檔案。

載入實現

下文我們需要展示一些 Go 彙編程式碼,為了方便讀者理解,先透過兩圖瞭解 Go 組合語言對 CPU 的重新抽象。

X86/AMD64 架構

Go實現啟動引數載入Go實現啟動引數載入

Go 偽暫存器

Go實現啟動引數載入Go實現啟動引數載入

Go彙編為了簡化彙編程式碼的編寫,引入了 PC、FP、SP、SB 四個偽暫存器。

四個偽暫存器加上其它的通用暫存器就是 Go 組合語言對 CPU 的重新抽象。當然,該抽象的結構也適用於其它非 X86 型別的體系結構。

回到正題,命令列引數的解析過程是程式啟動中的一部分內容。

以   amd64 系統為例,Go 程式的執行入口位於runtime/rt0_linux_amd64.s。

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8 
    JMP _rt0_amd64(SB)

_rt0_amd64函式實現於 runtime/asm_amd64.s

TEXT _rt0_amd64(SB),NOSPLIT,$-8 
    MOVQ    0(SP), DI   // argc 
    LEAQ    8(SP), SI   // argv 
    JMP runtime·rt0_go(SB)

看到 argc 和 argv 的身影了嗎?在這裡,它們從棧記憶體分別被載入到了 DI、SI 暫存器。

rt0_go函式完成了 runtime 的所有初始化工作,但我們這裡僅關注 argc 和 argv 的處理過程。

TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0 
    // copy arguments forward on an even stack 
    MOVQ    DI, AX      // argc 
    MOVQ    SI, BX      // argv 
    SUBQ    $(4*8+7), SP        // 2args 2auto 
    ANDQ    $~15, SP 
    MOVQ    AX, 16(SP) 
    MOVQ    BX, 24(SP) 
    ... 
    MOVL    16(SP), AX      // copy argc 
    MOVL    AX, 0(SP) 
    MOVQ    24(SP), AX      // copy argv 
    MOVQ    AX, 8(SP) 
    CALL    runtime·args(SB) 
    CALL    runtime·osinit(SB) 
    CALL    runtime·schedinit(SB) 
    ...

經過一系列操作之後,argc 和 argv 又被折騰回了棧記憶體 0(SP)和 8(SP) 中。

args 函式位於runtime/runtime1.go中

var ( 
 argc int32 
 argv **byte 
) 
 
func args(c int32, v **byte) { 
 argc = c 
 argv = v 
 sysargs(c, v) 
}

在這裡,argc 和 argv 分別被儲存至變數runtime.argc和runtime.argv。

在rt0_go函式中呼叫執行完args函式後,還會執行schedinit。

func schedinit() { 
  ... 
 goargs() 
 ...

goargs實現於runtime/runtime1.go

var argslice []string 
 
func goargs() { 
 if GOOS == "windows" { 
  return 
 } 
 argslice = make([]string, argc) 
 for i := int32(0); i < argc; i++ { 
  argslice[i] = gostringnocopy(argv_index(argv, i)) 
 } 
}

該函式的目的是,將指向棧記憶體的命令列引數字串指標,封裝成 Go 的 string型別,最終儲存於runtime.argslice。

這裡有個知識點,Go 是如何將 C 字串封裝成 Go string 型別的呢?答案就在以下程式碼。

func gostringnocopy(str *byte) string { 
 ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} 
 s := *(*string)(unsafe.Pointer(&ss)) 
 return s 
} 
 
func argv_index(argv **byte, i int32) *byte { 
 return *(**byte)(add(unsafe.Pointer(argv), uintptr(i)*sys.PtrSize)) 
} 
 
func add(p unsafe.Pointer, x uintptr) unsafe.Pointer { 
 return unsafe.Pointer(uintptr(p) + x) 
}

此時,Go 已經將 argc 和 argv 的資訊儲存至runtime.argslice中,那聰明的你一定能猜到os.Args方法就是讀取的該slice。

在os/proc.go中,是它的實現

var Args []string 
 
func init() { 
 if runtime.GOOS == "windows" { 
  // Initialized in exec_windows.go. 
  return 
 } 
 Args = runtime_args() 
} 
 
func runtime_args() []string // in package runtime

而runtime_args方法的實現是位於 runtime/runtime.go中的os_runtime_args函式

//go:linkname os_runtime_args os.runtime_args 
func os_runtime_args() []string { return append([]string{}, argslice...) }

在這裡實現了runtime.argslice的複製。至此,os.Args方法最終成功載入了命令列引數 argv 資訊。

總結

本文我們介紹了 Go 可以利用os.Args解析程式啟動時的命令列引數,並學習了它的實現過程。

在載入實現的原始碼學習中,我們發現如果從一個點出發,去追溯它的實現原理,這個過程並不複雜,希望童鞋們不要懼怕研究原始碼。

os.Args方法將命令列引數儲存在字串切片中,透過遍歷即可提取它們。但在實際開發中我們一般不會直接使用os.Args方法,因為 Go 為我們提供了一個更好用的 flag 包。但鑑於篇幅原因,該部分的內容以後再寫了。

原文來自:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2853128/,如需轉載,請註明出處,否則將追究法律責任。

相關文章