Go 如何實現啓動參數的加載
剛學 Go 的同學一定思考過 Go 程序的啓動過程,關於這個問題可以看饒大的文章 Go 程序是怎樣跑起來的。今天我們將問題縮小,來學習 Go 程序是怎麼加載啓動參數,以及如何進行參數解析。
C 參數解析
學習過 C 語言的童鞋,一定對 argc 和 argv 不會陌生。
C 程序總是從主函數 main 開始執行的,而在帶參數的主函數中,依照慣例,會使用 argc 和 argv 的命名作爲主函數參數。
其中,argc (argument count)代表的是命令行參數個數,argv(argument value) 是用來存放指向參數的指針數組。
#include <stdio.h>
int 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 彙編爲了簡化彙編代碼的編寫,引入了 PC、FP、SP、SB 四個僞寄存器。
四個僞寄存器加上其它的通用寄存器就是 Go 彙編語言對 CPU 的重新抽象。當然,該抽象的結構也適用於其它非 X86 類型的體系結構。
回到正題,命令行參數的解析過程是程序啓動中的一部分內容。
以 linux 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 包。但鑑於篇幅原因,該部分的內容以後再寫了。
參考
go 語言高級編程 -------- 彙編語言部分學習筆記:https://blog.csdn.net/cyq6239075/article/details/106480140
Go 程序是怎樣跑起來的:https://mp.weixin.qq.com/s/Rewl0DKnq6CY53m5D3G2qw
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/0Hgbb-WZwX9HUeY3PiL2mw