本节是以 Golang 上游 4b654c0eeca65ffc6588ffd9c99387a7e48002c1 为基准做的实验
予焦啦!昨日启用了虚拟位址的使用,试跑之後也走到比较远的地方了,但看起来似乎触发了严重的错误。严重的用词不是很精确,实际上这里指称的是,进入到了 Golang 执行期的函数 fatalthrow
里面。
throw
机制目前我们已知的是出现错误的虚拟位址在 0xffffffc00002f650
,且试试能否在 gdb 中这样使用:
(gdb) b *0xffffffc00002f650
Cannot access memory at address 0xffffffc00002f650
事实上是不行的。笔者猜测,GDB 与 QEMU 之间的除错机制并没有聪明到能够理解尚未建立的虚拟位址,而且这时候,分页表都还没建立。尽管如此,我们还是可以使用 gdb 的,但使用上稍微迂回一点。我们可以总是预先停在 satp
控制暂存器写入时的该行指令,然後
si
指令推进一行,理论上启用了虚拟位址pc
)到 stvec
中纪录的位址。既然我们有除错器的帮助,何必让指令页面错误发生再跳转呢?由於实在太迂回,笔者直接准备一个辅助脚本在 hoddarla 专案本身里面 misc/mkgdbrc.sh
:
$ cat misc/mkgdbrc.sh
#!/bin/bash
PREFIX=riscv64-buildroot-linux-musl-
KERNEL=ethanol/ethanol
echo target remote :1234
echo b *0x802$("$PREFIX"objdump -d "$KERNEL" | grep satp | sed -e "s/^.*\([0-9a-f]\{5\}\):.*$/\1/")
echo c
echo d 1
echo si
echo set \$pc=\$stvec
echo file $KERNEL
这样,使用者或开发者可以在第一个终端机开启 QEMU:
$ make EXTRA_FLAGS='-S -s'
make -C ethanol/
make[1]: 进入目录「/home/noner/FOSS/hoddarla/ithome/ethanol」
make[1]: 对「all」无需做任何事。
make[1]: 离开目录「/home/noner/FOSS/hoddarla/ithome/ethanol」
qemu-system-riscv64 \
-smp 4 \
-M virt \
-m 512M \
-nographic \
-bios misc/opensbi/build/platform/generic/firmware/fw_jump.bin \
-device loader,file=ethanol/goto/goto.bin,addr=0x80200000 \
-device loader,file=ethanol/hw,addr=0x80201000,force-raw=on -S -s
在第二个终端机则可以使用上述辅助脚本开启 GDB:
$ make debug
riscv64-linux-gnu-gdb -x /tmp/gdbrc
...
Breakpoint 1 at 0x80255594
[Switching to Thread 1.2]
Thread 2 hit Breakpoint 1, 0x0000000080255594 in ?? ()
0x0000000080255598 in ?? ()
...
(gdb) x/10i $pc -0x10
0xffffffc000055590 <relocate+128>: sfence.vma
0xffffffc000055594 <relocate+132>: csrw satp,t0
0xffffffc000055598 <relocate+136>: ebreak
0xffffffc00005559c: unimp
0xffffffc00005559e: unimp
=> 0xffffffc0000555a0 <_rt1_riscv64_opensbi>: auipc a0,0x0
0xffffffc0000555a4 <_rt1_riscv64_opensbi+4>: addi a0,a0,-264
0xffffffc0000555a8 <_rt1_riscv64_opensbi+8>: csrw stvec,a0
0xffffffc0000555ac <_rt1_riscv64_opensbi+12>: auipc t0,0xffffd
0xffffffc0000555b0 <_rt1_riscv64_opensbi+16>: addi t0,t0,-348
前述脚本根据映像档内容生成 /tmp/gdbrc
之後,自动定位到虚拟位址启用的指令,然後停留在能够使用虚拟位址除错的阶段。
继续追查最终来到 fatalthrow
的原因,我们如以往一般使用除错器指令设置中断点:
(gdb) b *0xffffffc00002f650
Breakpoint 2 at 0xffffffc00002f650: file /home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go, line 1263.
(gdb) c
Continuing.
Thread 2 hit Breakpoint 2, 0xffffffc00002f650 in runtime.fatalthrow ()
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:1263
1263 *(*int)(nil) = 0 // not reached
读者或许会疑惑,为什麽笔者不使用符号设置中断点,而是要先查询确切位址再设置?这是因为笔者开发环境习惯会另外开一个终端机,列出
ethanol/ethanol
的反组译组合语言码;通常要开始除错之前,都已经先在这个终端机端详许久,所以切换到 GDB 所属的终端机前,只需要复制个想要除错的位址即可。为求行文风格的叙述一致性,在撰写除错相关段落时,一律使用位址来做除错。但本日最後的小节,笔者会附上更无缝的除错体验,供各位读者参考。
中断点这个位址显然是流程当中故意要出问题的,因为这里将 nil
空指标转型成整数指标之後,对之赋值,所以触发写入错误也是当然的了。
再来看看,目前为止 ethanol 的执行经历过什麽样的历史轨迹:
(gdb) bt
#0 0xffffffc00002f650 in runtime.fatalthrow ()
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:1263
#1 0xffffffc00002f410 in runtime.throw (s=...)
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:1198
#2 0xffffffc00000a0f0 in runtime.persistentalloc1 (size=256, align=8,
sysStat=0xffffffc0000d8778 <runtime.memstats+152>, ~r3=<optimized out>)
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/malloc.go:1413
#3 0xffffffc000009e14 in runtime.persistentalloc.func1 ()
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/malloc.go:1367
#4 0xffffffc000009db8 in runtime.persistentalloc (size=256, align=8,
sysStat=0xffffffc0000d8778 <runtime.memstats+152>, ~r3=<optimized out>)
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/malloc.go:1366
#5 0xffffffc00002a1dc in runtime.(*addrRanges).init (
a=0xffffffc0000d0cf8 <runtime.mheap_+65688>,
sysStat=0xffffffc0000d8778 <runtime.memstats+152>)
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/mranges.go:170
#6 0xffffffc000023b84 in runtime.(*pageAlloc).init (p=0xffffffc0000c0c68 <runtime.mheap_+8>,
mheapLock=0xffffffc0000c0c60 <runtime.mheap_>,
sysStat=0xffffffc0000d8778 <runtime.memstats+152>)
#7 0xffffffc000020cf4 in runtime.(*mheap).init (h=0xffffffc0000c0c60 <runtime.mheap_>)
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/mheap.go:726
#8 0xffffffc000008050 in runtime.mallocinit ()
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/malloc.go:481
#9 0xffffffc0000324cc in runtime.schedinit ()
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/proc.go:689
#10 0xffffffc0000524ec in runtime.rt0_go ()
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/asm_riscv64.s:56
以下再细分小节来简述目前,Golang 的执行期环境原本想要初始化什麽项目。
schedinit
(gdb) f 9
#9 0xffffffc0000324cc in runtime.schedinit ()
at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/proc.go:689
689 mallocinit()
(gdb) l
684 // The world starts stopped.
685 worldStopped()
686
687 moduledataverify()
688 stackinit()
689 mallocinit()
690 fastrandinit() // must run before mcommoninit
691 mcommoninit(_g_.m, -1)
692 cpuinit() // must run before alginit
693 alginit() // maps must not be used before this call
从 rt0_go
进入的主要函式 schedinit
,在拙作当中(第八天到第十二天)有过一些简略的描述。这是 Golang 排程器(scheduler)需要的机制的初始化函数。以下相关的说明,参考自 src/runtime/proc.go
开头的注解。
总的来讲,Golang 排程器的主要机制可以拆分成几个主题:基本单元、工作者线程的启动与暂停机制(unpark/park)、工作者线程的空转状态转换(spinning/non-spinning)。
G
代表共常式(之後本系列文在指称 Goroutine 时,以共常式称,这是国家教育研究院针对 coroutine 的翻译。coroutine 在其他程序语言通常指称使用者空间的协程机制),是执行 Golang 程序的基本单元。M
代表工作者线程(worker thread),通常直接对应到底下的作业系统提供的 pthread 或是轻量执行绪(LWP, Light-Weight Thread)。P
代表处理器(processor),是指工作者线程执行共常式所必须要具备的资源。
要能够有效率地使用系统资源,有两个目标是 Golang 排程器想要平衡取舍的:一个是,工作者线程的数量要够大才能够利用作业系统与硬体给予的平行度;另一个则是,工作者线程的数量不能过多,否则运算资源与能量会过度消耗。
启动的条件有二:当有 P
闲置时,或者没有空转的工作者线程之时。
若是一个工作者线程找不到本地(P
处理器)的工作,或是在全域(global)的执行伫列找不到工作可执行,则为空转(spinning)的。
空转状态记录在两组变数当中:m.spinning
与 sched.nmspinning
。如果有一个以上的空转工作者线程,整个 Golang 排程机制就不会再启动新的工作者线程。若最後一个空转的工作者线程取得了工作,则是要启动一个新的工作者线程。
mallocinit
接下来从 schedinit
进入 mallocinit
函式。这个函式的开头是一百多行的注解,在描述作业系统的记忆体管理抽象层(OS Memory Management Abstraction Layer)。这个抽象层是在作业系统自己的记忆体管理机制之上的 Golang 机制,由四个状态与七种转移函式组成:
sysAlloc
:从不可使用到就绪状态。自作业系统取得已经清零的区块。sysReserve
:从不可使用到被预留状态。如果传入非空指标(non-nil pointer),仅作为提示使用,实际上还是可能配置并回传另外的位址。sysFree
:从任何状态变回不可使用。如果 sysReserve
总是能够确保回传对齐(align)於堆叠配置器(heap allocator)的记忆体区域,就可以不需要实作这个状态转移。sysMap
:从被预留到已准备状态。必须确保回传的区域能够很快转换成就绪。sysUsed
:从已准备到就绪状态。这个步骤是在有严格需求的作业系统上,如 Windows,用以通知核心,如今这个区域有确实的需求了。sysUnused
:从就绪变回已准备状态。通知作业系统可以取回该区域。sysFault
:从就绪或是已准备变回预留状态。这仅是执行期除错需求,而标记某些记忆体区域产生错误。persistentalloc1
跳过了中间的数层呼叫,是因为这个阶段都在建立 Golang 的记忆体管理机制。它们抽象化到最後,就是上述介绍的状态机与转移函数。别忘了,我们的如意算盘是直接挪用这些 Golang 机制做为作业系统的记忆体管理机制,至於成败如何,还得看接下来怎麽走。
(gdb) l
1408 persistent.base = (*notInHeap)(sysAlloc(persistentChunkSize, &memstats.other_sys))
1409 if persistent.base == nil {
1410 if persistent == &globalAlloc.persistentAlloc {
1411 unlock(&globalAlloc.mutex)
1412 }
1413 throw("runtime: cannot allocate memory")
1414 }
我们可以看到,这里执行期呼叫 throw
的部分,是由 persistent.base
是否为空指标来判断的。而这个判断之所以成立,就是因爲前一个 sysAlloc
我们完全没有实作的缘故。
事实上,opensbi/riscv64
这个系统组合现有的 sysAlloc
在 src/runtime/mem_opensbi.go
中,实作为:
func sysAlloc(n uintptr, sysStat *sysMemStat) unsafe.Pointer {
p := sysReserve(nil, n)
sysMap(p, n, sysStat)
return p
}
...
func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer {
return v
}
func sysMap(v unsafe.Pointer, n uintptr, sysStat *sysMemStat) {
sysStat.add(int64(n))
}
所以也难怪在 persistentalloc1
这里会取回空指标了。这里先暂时打住,我们来观察一下执行期组件里面的 throw
函式。
throw
到 fatalthrow
throw
函式在执行期可以回报错误字串且回溯历史。但是显然我们还缺乏一些机制,使得最後整个系统抵达了 fatalthrow
的最後一行,并且在控制台(console)完全没有看到任何相关的讯息。
换个角度思考。如果是一般的 Golang 程序,发生了这种等级的问题,应该还是要能够显示讯息到控制台上才对。既然是需要显示的情境,那就一定和系统呼叫 write
有关才对。
在 runtime
组件里面,我们可以加入以下内容,使得我们可以嫁接原本要给 POSIX 系作业系统的 write
系统呼叫,变成转给 OpenSBI 的控制台字元印出的呼叫:
diff --git a/src/runtime/os_opensbi.go b/src/runtime/os_opensbi.go [1/1806]
index c6b44995da..af9979a16b 100644
--- a/src/runtime/os_opensbi.go
+++ b/src/runtime/os_opensbi.go
@@ -15,8 +15,14 @@ func exit(code int32) {
return
}
+func write2(p uintptr)
func write1(fd uintptr, p unsafe.Pointer, n int32) int32 {
- return 0
+ if fd == 2 || fd == 1 {
+ for i := uintptr(0); i < uintptr(n); i++ {
+ write2(uintptr(p) + i)
+ }
+ }
+ return n
}
这里针对档案描述子(fd)为标准输出或是标准错误的状态,将每个字元指标传给新增的 write2
函式。write2
函式则以组合语言实作,因为需要进行暂存器的操作:
diff --git a/src/runtime/sys_opensbi_riscv64.s b/src/runtime/sys_opensbi_riscv64.s
new file mode 100644
index 0000000000..ddf3897ffe
--- /dev/null
+++ b/src/runtime/sys_opensbi_riscv64.s
@@ -0,0 +1,11 @@
+#include "textflag.h"
+#include "go_asm.h"
+
+// func write2(p uintptr)
+TEXT runtime·write2(SB),NOSPLIT|NOFRAME,$0-8
+ MOV p+0(FP), A0
+ LB 0(A0), A0
+ MOV $1, A7
+ MOV $0, A6
+ ECALL
+ RET
这里新创造一个档案来放这个函式。将 p
中的指标先传出来到 A0
,再从这个位置读出一个字元到 A0
。A7
与 A6
则与先前的使用方式相同。
相关的实验可以使用已经更新的 Hoddarla 得到。
重新编译并执行,获得的结果像是:
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000b109
Hfatal error: runtime: cannot allocate memory
runtime stack:
runtime.throw({0xffffffc000064338, 0x1f})
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:1198 +0x60 fp=0xffffffc000001e4
8 sp=0xffffffc000001e20 pc=0xffffffc00002f490
runtime.persistentalloc1(0x100, 0x8, 0xffffffc0000d8798)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/malloc.go:1413 +0x2c0 fp=0xffffffc000001
e90 sp=0xffffffc000001e48 pc=0xffffffc00000a0f0
runtime.persistentalloc.func1()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/malloc.go:1367 +0x44 fp=0xffffffc000001e
c0 sp=0xffffffc000001e90 pc=0xffffffc000009e14
runtime.persistentalloc(0x100, 0x8, 0xffffffc0000d8798)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/malloc.go:1366 +0x68 fp=0xffffffc000001f
00 sp=0xffffffc000001ec0 pc=0xffffffc000009db8
runtime.(*addrRanges).init(0xffffffc0000d0d18, 0xffffffc0000d8798)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/mranges.go:170 +0x4c fp=0xffffffc000001f
28 sp=0xffffffc000001f00 pc=0xffffffc00002a1dc
runtime.(*pageAlloc).init(0xffffffc0000c0c88, 0xffffffc0000c0c80, 0xffffffc0000d8798)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/mpagealloc.go:314 +0x8c fp=0xffffffc0000
01f48 sp=0xffffffc000001f28 pc=0xffffffc000023b84
runtime.(*mheap).init(0xffffffc0000c0c80)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/mheap.go:726 +0x5dc fp=0xffffffc000001f6
8 sp=0xffffffc000001f48 pc=0xffffffc000020cf4
runtime.mallocinit()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/malloc.go:481 +0x100 fp=0xffffffc000001f
88 sp=0xffffffc000001f68 pc=0xffffffc000008050
runtime.schedinit()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/proc.go:689 +0x6c fp=0xffffffc000001fe0
sp=0xffffffc000001f88 pc=0xffffffc0000325d4
runtime.rt0_go()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/asm_riscv64.s:56 +0x9c fp=0xffffffc00000
2000 sp=0xffffffc000001fe0 pc=0xffffffc000052624
I000000000000000f
0000000000000000
ffffffc00002f6d0
大获成功!之後就可以因此多一种除错工具了。
Golang 实际上有提供脚本,让除错器更容易以符号的方式除错。该脚本位在 src/runtime/runtime-gdb.py
,使用时只需:
(gdb) source /home/noner/FOSS/hoddarla/ithome/go/src/runtime/runtime-gdb.py
Loading Go Runtime support.
就可以进行以下操作,比方说函数反组译:
(gdb) x/10i 'runtime.throw'
0xffffffc000034a20 <runtime.throw>: sd ra,-40(sp)
0xffffffc000034a24 <runtime.throw+4>: addi sp,sp,-40
0xffffffc000034a28 <runtime.throw+8>: sd zero,16(sp)
0xffffffc000034a2c <runtime.throw+12>: sd zero,24(sp)
0xffffffc000034a30 <runtime.throw+16>: sd zero,32(sp)
0xffffffc000034a34 <runtime.throw+20>: auipc gp,0x0
0xffffffc000034a38 <runtime.throw+24>: addi gp,gp,100
0xffffffc000034a3c <runtime.throw+28>: sd gp,16(sp)
0xffffffc000034a40 <runtime.throw+32>: ld gp,48(sp)
0xffffffc000034a44 <runtime.throw+36>: sd gp,24(sp)
或是窥探资料结构内容:
(gdb) p/x 'runtime.g0'
$2 = {stack = {lo = 0xffffffbfffff1fe0, hi = 0xffffffc000001fe0},
stackguard0 = 0xffffffbfffff2380, stackguard1 = 0xffffffbfffff2380, _panic = 0x0,
_defer = 0x0, m = 0xffffffc0001195e0, sched = {sp = 0xffffffc000001fb8,
pc = 0xffffffc000039ea8, g = 0xffffffc000119440, ctxt = 0x0, ret = 0x0, lr = 0x0,
bp = 0x0}, syscallsp = 0x0, syscallpc = 0x0, stktopsp = 0x0, param = 0x0,
atomicstatus = 0x0, stackLock = 0x0, goid = 0x0, schedlink = 0x0, waitsince = 0x0,
...
需注意单引号('
),一定要加才能够正常除错。它可以保护 Golang 符号里面常常出现的、表达从属关系的运算元(.
)。这在表达 Golang 符号的时候是很重要的元素,因为它连接了符号与所属的组件。所以尽管 GDB 本身也使用 .
符号作为结构与成员之间的从属运算元,冲突的语意就可以被消弭。
予焦啦!除了在除错方面导入两个小技巧(串接 throw
的显示与启用 Golang 除错支援)之外,今天简单预习了 Golang 的记忆体管理抽象层设计,我们应当可以考虑将记忆体管理实作在这里。但毕竟 Golang 原本都是建立在完整的作业系统之上,所以这里该如何设计?今天的这个例子是,呼叫端 persistentalloc1
传递给 sysAlloc
的建议用指标是 nil
,也就是说,它只需要取得一个作业系统已经处理好的区块,其余一概没有意见。但我们这里还没有什麽机制可以决定,到底该怎麽分配记忆体才好?在实体记忆体的哪个位置?该分配哪里的虚拟记忆体位址给它?
看来,我们接下来要正式解决的话,必须先实作 sysAlloc
已降的 Golang 记忆体抽象层功能才行。我们已经开始面临一些不单纯的冲突,但这是早先就已经预见的。无论如何,各位读者,我们明天再会!
<<: 【Day 9】Introduction - Practice 1
>>: Spring Framework X Kotlin Day 16 Why Kotlin
Day 26 Socket的实际应用(二) 昨天我们讲了Socket应用中的python客户端,我们...
1968年 在1968年,犹他大学的Ivan Sutherland和他的学生Bob Sproull创...
Case01 跟 Day06、Day08 范例差不多,重点差异如下: Controller 於 Ge...
回到地图 我们在前几天讨论到地图,但是我们不用 Google API,因为怕被收钱钱,我们用的是 O...
安装passport套件 安装套件cmd执行以下 composer require laravel/...