本节是以 Golang 上游 8854368cb076ea9a2b71c8b3c8f675a8e19b751c 为基准做的实验
予焦啦!经过了第零章确保开发工具、第一章起步除错、第二章的基本记忆体机制,到目前为止也来到了这次铁人赛的中点;考量到目前的状态,笔者不打算进入第三章,而是以断章来称呼这中间的过渡期,就算是没有中场休息的休息吧!
无论如何,截至昨天为止,记忆体看起来满足了 Golang 的竞技场、元资料(metadata)、heap 的配置,也安然度过包含了许多记忆体初始化部分的 mcommoninit
函数了。但最後,我们是卡在 goargs
函数。笔者作为一个母语是 C 且时常使用命令列程序的人,顾名思义的直觉是这一定和命令列参数有关。如果是一般的 Golang 命令列程序的话,它们在启用之时,作业系统会妥善地服务它们,将来自使用者的命令列参数摆放在它们执行时可以存取到的位址,以及整理好环境变数让程序得以取用。但如果是自己将要成为作业系统映像的 ethanol,显然就没有如此妥善的机制了。
以上面的直觉为方向,今天就再度来试着渡过这个部分吧。
先以 linux/riscv64 系统组合为例,首先是入口之後不久就设定的两个值:
TEXT _rt0_riscv64_linux(SB),NOSPLIT|NOFRAME,$0
MOV 0(X2), A0 // argc
ADD $8, X2, A1 // argv
至於为什麽一开始进来就在堆叠指标有这些资料,就是前情提要所指,所谓作业系统的妥贴服务。之後进到 src/runtime/asm_riscv.s
的 rt0_go
函数,
// func rt0_go()
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
// X2 = stack; A0 = argc; A1 = argv
ADD $-24, X2
MOV A0, 8(X2) // argc
MOV A1, 16(X2) // argv
这里,两组资料从暂存器进入到堆叠上,就是准备要由其他函数来呼叫了。之後的流程可以参考拙作。
goargs
附近func argv_index(argv **byte, i int32) *byte {
return *(**byte)(add(unsafe.Pointer(argv), uintptr(i)*goarch.PtrSize))
}
func args(c int32, v **byte) {
argc = c
argv = v
sysargs(c, v)
}
func goargs() {
if GOOS == "windows" {
return
}
argslice = make([]string, argc)
for i := int32(0); i < argc; i++ {
argslice[i] = gostringnocopy(argv_index(argv, i))
}
}
args
函数即是在前小节承接堆叠内变数的函数。它初始化全域变数 argc
与 argv
,但 Golang 不像 C 语言那样将这两个值暴露给 imt main(int argc, char *argv[]
,而是在之後经过 goargs
函数来将另外一个全域变数 argslice
做成字串切片(slice,很抱歉我知道这翻译很烂),并让类似 C 的位元阵列指标,能够使用 Golang 的方式表示。
这个之後正常运行的话,会作为 os
套件包的 Args
变数,供命令列程序提取参数使用。范例可参考这个。
中间省略的细节在
src/runtime/runtime.go
里面,透过go:linkname
这个编译器指令,将argslice
转手做成另一个字串切片的函数,连结到os
套件包里面,并在os
的init
函数使用来设置Args
。所以许多 Golang 的命令列程序指南里面都会提到,要引用os
套件并使用os.Args
来取得命令列参数,也就是相当於 C 语言 main 函数的char *argv[]
参数的效果。
作业系统映像,当然就没有所谓的命令列参数,但以使用惯例来讲,还是有核心参数(kernel argument, 或 boot argument)之类的,传递资料给核心的方式存在。往往是透过启动载入器传递,或是我们今天的状况,若是在 QEMU 模拟器给了启动参数(-append
):
$ qemu-system-riscv64 \
-smp 4 \
-M virt,dumpdtb=dtb \
-m 512M \
-nographic \
-bios ../misc/opensbi/build/platform/generic/firmware/fw_jump.bin \
-kernel ethanol/goto/goto.bin \
-device loader,file=../ethanol/hw,addr=0x80201000,force-raw=on \
-append "arg1 arg2 arg3 arg4"
执行之後,传递到系统的装置树里面,就有这样的资讯:
chosen {
bootargs = "arg1 arg2 arg3 arg4";
stdout-path = "/soc/uart@10000000";
};
所以,我们可以再使用装置树,将这些资讯捞出来,然後设法走回原本的流程,让它能够自然建立。
以之前使用装置树的经验,我们知道它早已作为字串型别建立起来了。但是,障碍在於,Golang 期待 argv
这个全域变数的型别为 **byte
(以下称为双重 byte 指标)。
在一整个装置树字串里面,我们当然可以找到 chosen
节点里面的 bootargs
性质的值的位置,也就是我们真正感兴趣的部分。但这只会是一个起始位址。所有我们想要取得的参数可以在那个位址开始的区域内找到,并且以空格间隔开来。在装置树中的,只是一群字串而已。
但一般来说,C 和 Golang 执行期期待的是已经格式化过的字串阵列(char *argv[]
) 或是双重的位元组指标(argv **byte
),并且每个字串遵守 C 语言传统,以 NULL(\0
)结尾。在这个情况下,argv
这个阵列之内必须收集所有的字串的起始位址,使得 argv[0]
到 argv[argc-1]
之间的每一个位址都可以被解读成一个 C 传统字串。
也就是说,我们还得想办法将前者转换成後者。
argv
格式化在 args
函数当中,全域变数 argc
与 argv
即被赋值。然而,我们先前出问题的 goargs
才是真正将 C 传统的字串阵列转换为 Golang 较容易使用的字串切片之所在。关键在於,我们整个第二章打通了的执行期记忆体初始化要是尚未安顿好,许多原先的 Golang 写法根本无法使用。
在两个函数之间,我们还有一个作业系统相依(OS-dependent)的函数 osinit
,先前我们已经在这里安插过装置树的第一个使用者 GetMemoryInfo
,取得了实体记忆体的位置与大小资讯。现在,我们也可以在这个阶段处理参数的格式化。
func osinit() {
ncpu = 1
@@ -121,6 +122,8 @@ func osinit() {
// For the memory map of ethanol, check
// src/runtime/ethanol/README.
baseInit()
+ argc = ethanol.GetArgc()
+ print("argc: ", argc, "\n")
}
这里名为 GetArgc
,因为我们会走访装置树里面的 bootargs
那一段,数一数到底有几个。这个实作在 src/runtime/ethanol/fdt.go
里面:
+func GetArgc() int32 {
+ nodeOffset := getWord(fdt, 8)
+ strOffset := getWord(fdt, 12)
+ // find "bootargs" directly
+ regStrOffset, found := getStrOffset(fdt, "bootargs", strOffset, getWord(fdt, 32))
+ if !found {
+ return argsNotFound()
+ }
+
+ var l uint
+ var off uint
+ getReg := false
+ for i := nodeOffset; i < nodeOffset+getWord(fdt, 36); i += 4 {
+ if getWord(fdt, i) == FDT_BEGIN_NODE {
+ i += 4
+ if fdtsubstr(fdt, "chosen", i) {
前面的逻辑与 GetMemoryInfo
前面完全一样,都是先在字串区捞出所欲搜寻的属性的位址,然後定位出要搜寻的节点。之後,
+ if getWord(fdt, i+8) == regStrOffset {
+ l = getWord(fdt, i+4)
+ off = i + 12
+ break
...
+ if l > 1 {
+ return setArgv(fdt, off) + 1
+ }
+ return argsNotFound()
+}
l
从 4 个偏移量的位址取得整个 bootargs
的大小之後,我们在最後有一个基本判断,那就是 l
必须大於 1。这是因为,通常如果是不给 append
参数而启动 QEMU,那 bootargs
仍然会占有一个空字元。若是运行在没有 bootargs
的组态之下的话,l
就会是 0。
所以,非普通(non-trivial)的状况,呼叫到 setArgv
,这又是为何?这是因为,笔者打算,就是因为需要从头到尾扫过一次参数区,所以我们更能够趁机整理出一个字串阵列。演算法也很简单,就是每次找到空格,就将它取代成空字元,并且记录下它之後的字元的位址。作为特例,第一个参数一开始就纪录位址。这样一来,最後还会缺一个参数计数,因为最後一个参数是以空字元结尾而非空格;这也就是最後一个加一的结算。
setArgv
的实作在 src/runtime/ethanol/misc.s
:
+// func setArgv(s string, off uint) uint32
+TEXT runtime∕ethanol·setArgv(SB), NOSPLIT|NOFRAME, $0-28
+ MOV s+0(FP), A0
+ MOV off+16(FP), A1
+ ADD A0, A1, A1
+
+ // get argv
+ MOV $runtime·argv(SB), A0
+ MOV 0(A0), A0
...
第一个参数是字串,以 Golang 的 ABI 而言,字串占去两个指标的长度,所以这里只取第一个(0(FP)
)指标代表该字串的位址。取得偏移量之後加上原本字串内容的位址之後,就是 bootargs
的内容了。然後再取得 argv
全域变数,就算是准备好了。
+ MOV $0, A2
+ MOV $0x20, A3
+setarg:
+ MOV A1, 0(A0)
+ ADD $8, A0, A0
+findnull:
+ MOVBU 0(A1), A5
+ BEQ ZERO, A5, endarg
+ BNE A3, A5, skip
+ MOVB ZERO, 0(A1)
+ ADD $1, A2, A2
+ ADD $1, A1, A1
+ JMP setarg
+skip:
+ ADD $1, A1, A1
+ JMP findnull
+endarg:
A2
是作为 argc
的计数使用的,一开始清空;A3
则是一个常数,储存的 0x20
是空格的 ASCII 码编号,用来方便比对用的。
setarg
就是在 argv
所在的位置写下当前的参数的位址,并且将 argv
的阵列索引向前推进一个指标的大小,在这里是 8 个位元组。
findnull
会一个一个检验当前的参数的内容的字元。要是已经是空字元了的话,就走到最後去,因为整个参数区已经被走访完毕。如果不是,则比对是否为空格。若是空格,则将之写成空字元,且纪录 argc
计数。若否,则单纯继续推进。
+
+ MOVW A2, ret+24(FP)
+ RET
回传值的写回,这也就不必多说了。
这里是从 osinit
呼叫进来,heap 或是竞技场之类的 Golang 记忆体管理都还没初始化,所以也没办法用很动态的方式直接生一块出来用。又,我也不想要在资料区内再准备一块专门给 argv
用的指标阵列,因为那麽一来我还得限定参数上限。
所以这里我打算挪用虚拟位址 0xffffffc000000000-0xffffffc000002000
。确实这一块正在被当作早期堆叠使用,但是堆叠是由高位往低位倒退的。如果是从头开始放 argv
的话,只要两个都不要长得太多,也就不至於相撞了。
再次地,这个的确也是不永续的技术债。
只要稍微调整一下就能够支援环境变数了。我们的目标是像:
diff --git a/Makefile b/Makefile
index 571dbba..6fb48b1 100644
--- a/Makefile
+++ b/Makefile
@@ -7,8 +7,10 @@ run:
-m 512M \
-nographic \
-bios misc/opensbi/build/platform/generic/firmware/fw_jump.bin \
- -device loader,file=ethanol/goto/goto.bin,addr=0x80200000 \
+ -kernel ethanol/goto/goto.bin \
+ -append "ethanol arg1 arg2 env1=1 env2=abc" \
-device loader,file=ethanol/hw,addr=0x80201000,force-raw=on $(EXTRA_FLAGS)
这样给定的命令列,有两个参数,类似两个环境变数的东西。至於环境变数通常是怎麽取得,可以观察 goenvs_unix
函数
func goenvs_unix() {
// TODO(austin): ppc64 in dynamic linking mode doesn't
// guarantee env[] will immediately follow argv. Might cause
// problems.
n := int32(0)
for argv_index(argv, argc+1+n) != nil {
n++
}
envs = make([]string, n)
for i := int32(0); i < n; i++ {
envs[i] = gostring(argv_index(argv, argc+1+i))
print("envp[", i, "] = ", envs[i], "\n") // 这是笔者安插的,仅是为了观察用
}
}
实际上,以一般使用者环境的期待来讲,在大部分的类 Unix 作业系统之下,环境变数的位置在命令列参数之後相隔一个空字元。所以我们可以稍加修改先前的程序流程,改成让它在回圈里面扫过字元的时候,观察是否含有等号(ASCII 0x3d
):
index 75d01cc75e..fc76d8a217 100644
--- a/src/runtime/ethanol/misc.s
+++ b/src/runtime/ethanol/misc.s
@@ -24,21 +24,49 @@ TEXT runtime∕ethanol·setArgv(SB), NOSPLIT|NOFRAME, $0-28
MOV $0, A2
MOV $0x20, A3
+ MOV $0x3d, A4
setarg:
MOV A1, 0(A0)
ADD $8, A0, A0
findnull:
MOVBU 0(A1), A5
- BEQ ZERO, A5, endarg
+ BEQ ZERO, A5, end
+ BEQ A4, A5, env
以 env
符号标志另外一个基本上一样的阶段,但是当前的参数段应该要挪到 argv
阵列内的後一格,且以一个空指标区隔参数与环境变数:
-endarg:
- MOVW A2, ret+24(FP)
+ // if fdt[i] == '='
+env:
+ MOV A2, A6
+ MOV -8(A0), A2
+ MOV ZERO, -8(A0)
+ MOV A2, 0(A0)
+ ADD $8, A0, A0
+ JMP findnull2
+setenv:
+ MOV A1, 0(A0)
+ ADD $8, A0, A0
+findnull2:
+ MOVBU 0(A1), A5
+ BEQ ZERO, A5, end
+ BNE A3, A5, skip2
+ // if fdt[i] == ' '
+ MOVB ZERO, 0(A1)
+ ADD $1, A1, A1
+ JMP setenv
+skip2:
+ // fdt[i] character is normal
+ ADD $1, A1, A1
+ JMP findnull2
+end:
+
+ MOVW A6, ret+24(FP)
RET
稍加扩充之後 setArgv
就可以也在扫过一遍装置树区块之时,一并把环境变数也处理好了。
可以在今日上传的 Hoddarla repo 执行以下实验。
在 src/runtime/runtime1.go
里面的 goargs
与 goenvs_unix
函数当中插入对应的印出讯息并试跑,得:
...
Reserve: 0x10000 bytes, at 0x0 but at 0xffffffc500010000
Map: 0x10000 bytes, at 0xffffffc500010000
argv[0] = ethanol
argv[1] = arg1
argv[2] = arg2
envp[0] = env1=1
envp[1] = env2=abc
panic: newosproc: not implemented
fatal error: panic on system stack
...
goroutine 1 [running]:
runtime.systemstack_switch()
I000000000000000f
0000000000000000
ffffffc00002b6c0
出现了新的错误、错在新的位置,这些挑战就留待下回分晓吧。
予焦啦!今天其实没做什麽特别的事情,就是单纯把原本在记忆体当中平铺直叙的、代表命令列参数与环境变数的字元们,整理成符合 Golang 执行期所期待的结构,并让 Golang 原有的函数能够将它们转换成字串切片,之後能够更方便使用。各位读者,我们明日再会!
>>: 【Day10】Git 版本控制 - 将档案 push 到 GitHub 的懒人包
iOS APP 开发 OC 第十三天,测试网路状态 iOS Reachability tags: O...
程序写了老半天,说到底就是为了处理资料。 不管处理逻辑使用了多少技术,到头来终究会得出一个结果,并且...
yo, what's up 本章要来介绍 FP 的重要观念,Algebraic structure!...
财源滚滚 今日会详细讲解Service和Deployment的功能 Service 在[Day22]...
前言 今天会介绍laravel一些简单的指令 以及建立路由还有CRUD方法 跟资料库连线喔!! 目标...