Students who have just learned Go must have thought about the startup process of the Go program. Regarding this issue, you can read Rao Da’s articleHow the Go program runs. Today we narrow down the problem and learn how the Go program loads startup parameters and how to parse the parameters.
Children who have studied C language must be familiar with argc and argv.
C programs always start execution from the main function main, and in the main function with parameters, according to convention, the naming of argc and argv will be used as the main function parameters.
Among them, argc (argument count) represents the number of command line parameters, and argv (argument value) is an array of pointers used to store parameters.
#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; }
Compile and execute the above C code, and you will get the following output
$ gcc c_main.c -o main $ ./main foo bar sss ddd argc = 5 argv[0] = ./main, argv[1] = foo, argv[2] = bar
So how do you get the command line parameters in Go language?
Like C, the Go program is also executed from the main function (user layer), but in the main function argc and argv are not defined.
We can obtain command line parameters through the os.Args function.
package main import ( "fmt" "os" ) func main() { for i, v := range os.Args { fmt.Printf("arg[%d]: %v\n", i, v) } }
Compile and execute Go function
$ go build main.go $ ./main foo bar sss ddd arg[0]: ./main arg[1]: foo arg[2]: bar arg[3]: sss arg[4]: ddd
Like C, the first parameter also represents the executable file.
下文我们需要展示一些 Go 汇编代码,为了方便读者理解,先通过两图了解 Go 汇编语言对 CPU 的重新抽象。
Go汇编为了简化汇编代码的编写,引入了 PC、FP、SP、SB 四个伪寄存器。
四个伪寄存器加上其它的通用寄存器就是 Go 汇编语言对 CPU 的重新抽象。当然,该抽象的结构也适用于其它非 X86 类型的体系结构。
回到正题,命令行参数的解析过程是程序启动中的一部分内容。
以 linux amd64 系统为例,Go 程序的执行入口位于<span style="font-size: 15px;">runtime/rt0_linux_amd64.s</span>
。
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8 JMP _rt0_amd64(SB)
<span style="font-size: 15px;">_rt0_amd64</span>
函数实现于 <span style="font-size: 15px;">runtime/asm_amd64.s</span>
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 寄存器。
<span style="font-size: 15px;">rt0_go</span>
函数完成了 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 又被折腾回了栈内存 <span style="font-size: 15px;">0(SP)</span>
和 <span style="font-size: 15px;">8(SP)</span>
中。
<span style="font-size: 15px;">args</span>
函数位于<span style="font-size: 15px;">runtime/runtime1.go</span>
中
var ( argc int32 argv **byte ) func args(c int32, v **byte) { argc = c argv = v sysargs(c, v) }
在这里,argc 和 argv 分别被保存至变量<span style="font-size: 15px;">runtime.argc</span>
和<span style="font-size: 15px;">runtime.argv</span>
。
在<span style="font-size: 15px;">rt0_go</span>
函数中调用执行完<span style="font-size: 15px;">args</span>
函数后,还会执行<span style="font-size: 15px;">schedinit</span>
。
func schedinit() { ... goargs() ...
<span style="font-size: 15px;">goargs</span>
实现于<span style="font-size: 15px;">runtime/runtime1.go</span>
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 的 <span style="font-size: 15px;">string</span>
类型,最终保存于<span style="font-size: 15px;">runtime.argslice</span>
。
这里有个知识点,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 的信息保存至<span style="font-size: 15px;">runtime.argslice</span>
中,那聪明的你一定能猜到os.Args方法就是读取的该slice。
在<span style="font-size: 15px;">os/proc.go</span>
中,是它的实现
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
而<span style="font-size: 15px;">runtime_args</span>
方法的实现是位于 <span style="font-size: 15px;">runtime/runtime.go</span>
中的<span style="font-size: 15px;">os_runtime_args</span>
函数
//go:linkname os_runtime_args os.runtime_args func os_runtime_args() []string { return append([]string{}, argslice...) }
在这里实现了<span style="font-size: 15px;">runtime.argslice</span>
的拷贝。至此,<span style="font-size: 15px;">os.Args</span>
方法最终成功加载了命令行参数 argv 信息。
In this article we introduced that Go can be started using the <span style="font-size: 15px;">os.Args</span>
parser command line parameters, and learned its implementation process.
During the study of the source code of the loading implementation, we found that if we start from one point and trace its implementation principle, the process is not complicated. We hope that children will not be afraid of studying the source code. The
<span style="font-size: 15px;">os.Args</span>
method stores command line arguments in string slices, which can be extracted by traversing. But in actual development, we generally do not use the <span style="font-size: 15px;">os.Args</span>
method directly, because Go provides us with a more useful flag package. However, due to space reasons, this part will not be written later.
The above is the detailed content of How Go implements loading of startup parameters. For more information, please follow other related articles on the PHP Chinese website!