予焦啦!使用 GDB 推进

本节是以 Golang 上游 ee91bb83198f61aa8f26c3100ca7558d302c0a98 为基准做的实验。

予焦啦!回顾昨日,我们从 Linux 当中学习一些灵感,但我们的结论是现在什麽先暂时都不必做;不管是 CPU ID、装置树位址或是堆叠指标的指定。不如就先这样下去,看看会发生什麽事吧。

本节重点概念

  • 基础
    • GDB 常用指令
  • Golang
    • 执行期初始化问题排除范例
    • 组合语言内的结构偏移量运算子 _

关於 Golang 执行期初始化,可参阅拙作,虽然是以 x86_64 为主就是了。

试着走下去

我们已经很清楚,堆叠指标这时候还没有完全搞定。是可以像 Linux 那样,在虚拟记忆体启用之前先设一个,启用後再设一个,但何必这麽麻烦呢?我们先把它拿掉:

diff --git a/src/runtime/rt0_opensbi_riscv64.s b/src/runtime/rt0_opensbi_riscv64.s
index c65afa5c79..878b92b7bb 100644
--- a/src/runtime/rt0_opensbi_riscv64.s
+++ b/src/runtime/rt0_opensbi_riscv64.s
@@ -67,8 +67,6 @@ TEXT _rt0_riscv64_opensbi(SB),NOSPLIT|NOFRAME,$0
        ECALL
        MOV     $early_halt(SB), A0
        CSRRW   CSR_STVEC, A0, X0
-       MOV     0(X2), A0       // argc
-       ADD     $8, X2, A1      // argv
        JMP     main(SB)
        
        

然後执行看看。结果我们可以看到再次卡入早期错误处理,

...
Boot HART MIDELEG         : 0x0000000000000222
Boot HART MEDELEG         : 0x000000000000a109
HI0000000000000007
000000008001ded8
0000000080252428

这次是 scause 为 7,代表记忆体写入存取错误。原因也和之前一样是误触 OpenSBI 的物理记忆体保护区段。而错误位置在:

ffffff8000052428 <runtime.rt0_go>:
ffffff8000052428:       fe113c23                sd      ra,-8(sp)

其实也还没有多远,就在我们从 _rt0_riscv64_opensbi 进入点跳往 main,再由 main 函式里面跳往位在 src/runtime/asm_riscv64.srt0_go 的第一行。我们即将与 Linux 共用这个阶段的初始化。但也一样又存取到了堆叠指标。

但我们今天是打定主意来试试看多少往前走的,那麽就随便设置一个无伤大雅的值好了。如先前的函式序幕(prologue)范例与这里的范例,我们知道堆叠指标是隋着程序的进行由高位往低位发展的,所以不如就先设在 256MB 的位置吧,但别忘了基底是从 0x80000000 起算,也就是说改成:

...
        JMP     main(SB)
 
 TEXT main(SB),NOSPLIT|NOFRAME,$0
        MOV     $runtime·rt0_go(SB), T0
+       MOV     $0x90000000, X2
        JALR    ZERO, T0

然後试跑:

Boot HART MIDELEG         : 0x0000000000000222
Boot HART MEDELEG         : 0x000000000000a109
HI0000000000000005
74e0e55be0d63618
0000000080254510

又是记忆体读取错误。而且错误的位置在一个很夸张的地方,完全无法对应到实体记忆体。发生错误的所在是:

ffffff80000544f0 <runtime.gcWriteBarrier>:
ffffff80000544f0:       f2113023                sd      ra,-224(sp)
ffffff80000544f4:       f2010113                addi    sp,sp,-224
ffffff80000544f8:       0ca13423                sd      a0,200(sp)
ffffff80000544fc:       0cb13823                sd      a1,208(sp)
ffffff8000054500:       030db503                ld      a0,48(s11)
ffffff8000054504:       0a053503                ld      a0,160(a0)
ffffff8000054508:       00001fb7                lui     t6,0x1
ffffff800005450c:       01f50fb3                add     t6,a0,t6
ffffff8000054510:       6c0fb583                ld      a1,1728(t6) # 16c0 <internal/cpu.process
Options-0xffffff8000000940>
...

这时候就不得不强调,让十六进位的後五位数在两种定址模式底下维持一样可以让问题轻松许多。此时的连结位址是 0xffffff8000054510 而载入位址是 0x80254510,算是很方便可以定位的了。

笔者当然也是可以就自己浅薄的了解,就地开始解释 gcWriteBarrier 函式的任务是什麽,但这样有点见树不见林。我们现在有一个应该还堪用的堆叠指标,所以 Golang 执行期也就这麽执行下去,可是依循了怎麽样的轨迹,导致程序最後死在这里呢?又,有没有可能其实我们从早期错误处理观察到的错误状态,实际上已经是第二、第三现场,距离案发地点已经有一段距离了呢

有时候,系统的问题就是需要抽丝剥茧、从各个角度去观察才有办法慢慢掌握问题,这时候,只有印出讯息功能来辅佐除错的话,有时候还是过於虚弱。所以我们就趁这个机会,来使用最权威的除错工具:GDB。

GDB 登场!

GDB 是 GNU Project Debugger,它有着强大的功能。笔者这里只打算介绍一些简单的常用指令,并展示它们已经足够强大,来辅助我们的除错。

QEMU 模拟器有提供机制让 GDB 能够连接除错。我们首先打起 QEMU,但多附带 -S -s。其中,-S 代表在开始的时候冻结模拟器的执行,-s 代表预设将除错埠(port)设置在本机的 1234。

qemu-system-riscv64 \
        -smp 4 \
        -M virt \
        -m 256M \
        -nographic \
        -bios misc/opensbi/build/platform/generic/firmware/fw_jump.bin \
        -device loader,file=ethanol/goto/goto.bin,addr=0x80200000 \
        -device loader,file=ethanol/ethanol,addr=0x80201000,force-raw=on -S -s
        

这麽执行之後,有别於先前都会快速进入 OpenSBI,现在就是冻结的状态了。在另外一个终端机,我们可以使用针对 RISC-V 的 GDB 来连线:

笔者在 Arch Linux 直接安装了 community/riscv64-linux-gnu-gdb 来使用。当然,可以自己建置或是使用预先编好的版本。

$ riscv64-linux-gnu-gdb -ex "target remote :1234"                                        [6/346]
GNU gdb (GDB) 10.2                                                                              
Copyright (C) 2021 Free Software Foundation, Inc.               
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.   
There is NO WARRANTY, to the extent permitted by law.               
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=riscv64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
Remote debugging using :1234
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x0000000000001000 in ?? ()
(gdb) 

最一开始,QEMU 有一小段启动程序码,位在 0x1000 的区域,可以使用 x/10i $pc 指令来观察当前程序指标($pc)起算十行组合语言的内容:

(gdb) x/10i $pc
=> 0x1000:      auipc   t0,0x0
   0x1004:      addi    a2,t0,40
   0x1008:      csrr    a0,mhartid
   0x100c:      ld      a1,32(t0)
   0x1010:      ld      t0,24(t0)
   0x1014:      jr      t0
   0x1018:      unimp
   0x101a:      0x8000
   0x101c:      unimp
   ...

请留意,由於这是 GNU 工具,所以组合语言的目的、来源方向也是相反於 Golang 组语。靠近助忆符的是目标(被改写的对象),而较远的通常是来源运算元。

这里有四个暂存器被写入。a0~2 等三个暂存器是作为系统软件之间的参数,所以我们目睹到了 a0 被设为 Hart ID 的现场。a1 也即将被设为 DTB 的所在。至於 a2,不在这次系列文的范畴,但有兴趣的读者可以去搜寻 OpenSBI 动态模式(dynamic mode)。

最後在 0x1014 之处,跳向 t0。在这之前,t0 暂存器历经一个 auipc 指令,以及一个 ld 指令。

前者是指加入常数到程序指标的高位(除去低 12 bit)部分,因此执行完 0x1000 指令时,t0 应该已经成为 0x1000,毕竟该指令附带的常数为 0x0。这个可以使用以下 GDB 指令序列验证:

(gdb) p/x $t0
$1 = 0x0
(gdb) si
0x0000000000001004 in ?? ()
(gdb) p/x $t0
$2 = 0x1000
(gdb)

笔者这里先印出 t0 暂存器的值,之後使用 si(step instruction,执行单一一行指令),程序指标转移到下一行,且 0x1000 的指令生效,t0 被写入 0x1000

後者,也就是一个读取指令,是以 t0 为基底取 24 位元组的位置(整组 24(t0) 即为此意),将这个位置的内容存放回 t0 暂存器。当然我们可以使用多个 si 指令推移程序行进,但我们也可以使用强大的设置断点功能,直接先守在 0x1014 处,跳跃之前:

(gdb) p/x $t0
$2 = 0x1000
(gdb) b *0x1014
Breakpoint 1 at 0x1014
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, 0x0000000000001014 in ?? ()
(gdb) p/x $t0
$3 = 0x80000000
(gdb)

所以我们知道了,稍後程序执行的跳转目的地是 0x80000000,也就是 OpenSBI,或者说 QEMU 参数 -bios 指向的位址。

在先前上传的 Hoddara repo 里面,其实没有 -bios 参数带入给 QEMU,而是使用内建的 OpenSBI 韧体。QEMU 在启用 RISC-V 平台时,要是没有 -bios 参数,就会自动使用之。这里我们为了 GDB 的使用顺畅,暂且跳过 OpenSBI 的准备部分,留到本日最後的一小节。

OpenSBI

往前进一行,我们就能来到 OpenSBI 的领域。

(gdb) si
0x0000000080000000 in ?? ()
(gdb) x/10i $pc
=> 0x80000000:  add     s0,a0,zero
   0x80000004:  add     s1,a1,zero
   0x80000008:  add     s2,a2,zero
   0x8000000c:  jal     ra,0x80000698
   0x80000010:  add     a6,a0,zero
   0x80000014:  add     a0,s0,zero
   0x80000018:  add     a1,s1,zero
   0x8000001c:  add     a2,s2,zero
   0x80000020:  li      a7,-1
   0x80000022:  beq     a6,a7,0x8000002a
(gdb)

看起来确实像是有东西,而不是像先前 0x1000 的小型启动码数行之後就会开始出现 unimp 这种未定义内容。但这里显示的都是十六进位数字,比起纯然的二进位档也只多了一点点可读性而已。当然,GDB 不会让开发者失望的。

编译的产物如果夹带有除错讯息,GDB 就可以提取出来参照使用。所以这里我们可以使用 add-symbol-file 指令:

(gdb) add-symbol-file misc/opensbi/build/platform/generic/firmware/fw_jump.elf           [0/399]
add symbol table from file "misc/opensbi/build/platform/generic/firmware/fw_jump.elf"
(y or n) y
Reading symbols from misc/opensbi/build/platform/generic/firmware/fw_jump.elf...
(gdb) x/10i $pc
=> 0x80000000 <_start>: add     s0,a0,zero
   0x80000004 <_start+4>:       add     s1,a1,zero
   0x80000008 <_start+8>:       add     s2,a2,zero
   0x8000000c <_start+12>:      jal     ra,0x80000698 <fw_boot_hart>
   0x80000010 <_start+16>:      add     a6,a0,zero
   0x80000014 <_start+20>:      add     a0,s0,zero
   0x80000018 <_start+24>:      add     a1,s1,zero
   0x8000001c <_start+28>:      add     a2,s2,zero
   0x80000020 <_start+32>:      li      a7,-1
   0x80000022 <_start+34>:      beq     a6,a7,0x8000002a <_try_lottery>

一样的位址,稍微加了一点料的输出,但我们现在可以看到位址与符号(symbol)的对应关系了。口说无凭,我们可以在 OpenSBI 的程序码中搜寻这些符号,且看合不合理:

$ grep 'fw_boot_hart' -R ./
./firmware/fw_payload.S:        .global fw_boot_hart
./firmware/fw_payload.S:fw_boot_hart:                                                           
./firmware/fw_dynamic.S:        .global fw_boot_hart
./firmware/fw_dynamic.S:fw_boot_hart:
./firmware/fw_base.S:   call    fw_boot_hart
./firmware/fw_jump.S:   .global fw_boot_hart
...

从除错器看到的是跳跃到 fw_boot_hart,而搜出来的四个档案只有一个是呼叫,所以先观察 firmware/fw_base.S

...
_start:
        /* Find preferred boot HART id */
        MOV_3R  s0, a0, s1, a1, s2, a2
        call    fw_boot_hart
        add     a6, a0, zero
        MOV_3R  a0, s0, a1, s1, a2, s2
        li      a7, -1
        beq     a6, a7, _try_lottery
...

正是如出一辙。

推进到 ethanol

但我们无需在这里久留。再度设置断点於 0x80200000 的 goto 小程序所在位址,然後前进:

(gdb) x/10i $pc                                  
=> 0x80200000:  auipc   t6,0x55
   0x80200004:  addi    t6,t6,1200
   0x80200008:  jr      t6
...
(gdb) si
0x0000000080200004 in ?? ()
(gdb) 
0x0000000080200008 in ?? ()
(gdb) 
0x00000000802554b0 in ?? ()

如果不输入指令,单纯按输入键的话,就会有重复上一个指令的效果。

来到实体位置 0x802554b0,正是这个版本的 ethanol 映像档的进入点位址对应的实体位址:

Entry point address:               0xffffff80000554b0

但由於连结位址(link address)与载入位址(load address)不相同,所以就算如同刚才 OpenSBI 那样,引用 add-symbol-file,再透过 GDB 观察程序码也是没有效果的:

(gdb) add-symbol-file ethanol/ethanol
...
(gdb) x/10i $pc
=> 0x802554b0:  addiw   a0,zero,72
   0x802554b4:  addiw   a7,zero,1
   0x802554b8:  sext.w  a6,zero
   0x802554bc:  ecall
   0x802554c0:  auipc   a0,0x0
   0x802554c4:  addi    a0,a0,-80
   0x802554c8:  csrw    stvec,a0
   0x802554cc:  auipc   t6,0x0
   0x802554d0:  jr      12(t6)

这时候我们仍然可以强制指定,我们希望程序码区段的符号能够做特定的位址对应,指令如下:

(gdb) add-symbol-file ethanol/ethanol -s .text 0x80202000             
add symbol table from file "ethanol/ethanol" at                                                      
        .text_addr = 0x80202000                                                                 
(y or n) y                                                                                      
Reading symbols from ethanol/ethanol...
...
(gdb) x/10i $pc                                  
=> 0x802554b0 <_rt0_riscv64_opensbi>:   addiw   a0,zero,72
   0x802554b4 <_rt0_riscv64_opensbi+4>: addiw   a7,zero,1
   0x802554b8 <_rt0_riscv64_opensbi+8>: sext.w  a6,zero
   0x802554bc <_rt0_riscv64_opensbi+12>:        ecall
   0x802554c0 <_rt0_riscv64_opensbi+16>:        auipc   a0,0x0
   0x802554c4 <_rt0_riscv64_opensbi+20>:        addi    a0,a0,-80
   0x802554c8 <_rt0_riscv64_opensbi+24>:        csrw    stvec,a0
   0x802554cc <_rt0_riscv64_opensbi+28>:        auipc   t6,0x0
   0x802554d0 <_rt0_riscv64_opensbi+32>:        jr      12(t6)
...

笔者也不清楚为什麽 early_haltmain 这里没有被 GDB 侦测出来。要详细理解的话,可能必须深入挖掘 GDB 与映像档的除错资讯本身,这就超过本系列范围了。

追踪错误

我们接下来在最後回报的错误位址设下断点,

(gdb) b *0x80254510
Breakpoint 1 at 0x80254510
(gdb) c
Continuing.
...
(gdb) x/10i $pc-0x10
   0x80254500 <runtime.gcWriteBarrier+16>:      ld      a0,48(s11)
   0x80254504 <runtime.gcWriteBarrier+20>:      ld      a0,160(a0)
   0x80254508 <runtime.gcWriteBarrier+24>:      lui     t6,0x1
   0x8025450c <runtime.gcWriteBarrier+28>:      add     t6,a0,t6
=> 0x80254510 <runtime.gcWriteBarrier+32>:      ld      a1,1728(t6)

果然没错,就是这里,但是这时候就已经达成错误条件了吗?事实上,各位读者或许都曾耳闻 Golang 的垃圾回收(Garbage Collection)机制做得很好,其中这个 gcWriteBarrier 也是关键程序之一,它在许多地方都会被执行到。

为此,我们检验一下这时的 t6 暂存器,毕竟是准备要拿来存取的记忆体基底:

(gdb) p/x $t6
$1 = 0x74e0e55be0d62f59
(gdb) si
...
early_halt () at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/rt0_opensbi_riscv64.s:61
61              JMP     early_halt(SB)
(gdb)

这时候的 t6 就已经是一个夸张的内容了,所以存取之後当然会陷入我们的早期错误处理区,这也是符合预期的。

追溯原因

如果只是在这里观察 t6 且确认其内容错误,本身实在没什麽了不起。所以我们再开一次 GDB,停在触发错误的前一刻:

(gdb) bt
#0  0x0000000080254510 in runtime.gcWriteBarrier ()
    at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/asm_riscv64.s:653
#1  0x000000008023ebd8 in runtime.args (c=-2145037200, v=0x82200000)
    at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/runtime1.go:63
#2  0x00000000802524b4 in runtime.rt0_go ()
    at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/asm_riscv64.s:54

bt 指令是向後追踪(backtrace)的简写,是在堆叠当中搜寻呼叫历史的强大功能。我们这里就追溯到了,从 rt0_go 进入之後的这个 args,再呼叫了 gcWriteBarrier,显然造成了问题。

分析

再看一次案发现场:

   0x80254500 <runtime.gcWriteBarrier+16>:      ld      a0,48(s11)
   0x80254504 <runtime.gcWriteBarrier+20>:      ld      a0,160(a0)
   0x80254508 <runtime.gcWriteBarrier+24>:      lui     t6,0x1
   0x8025450c <runtime.gcWriteBarrier+28>:      add     t6,a0,t6
=> 0x80254510 <runtime.gcWriteBarrier+32>:      ld      a1,1728(t6)

以及它对应的组合语言原始码,在 src/runtime/asm_riscv64.s

647 TEXT runtime·gcWriteBarrier(SB),NOSPLIT,$2216   ...
651         MOV     g_m(g), A0 
652         MOV     m_p(A0), A0
653         MOV     (p_wbBuf+wbBuf_next)(A0), A1
...

这里必须解释一下 Golang 的组合语言档案中,g_m(g) 或是 m_p(A0) 作为一种来源运算元的语义。

Golang 执行期有些重要的结构体,如 g,定义在 src/runtime/runtime2.go

 403 type g struct {
 ...
 417         m         *m 
 418         sched     gobuf
 419         syscallsp uintptr
 420         syscallpc uintptr 
 ...

其中有个 m 成员,型别为 *m,也就是 m struct 的指标。这些都是没什麽大不了的程序语言语法。但在组合语言里面存取的时候,毕竟就不像高阶语言一样可以方便地使用 g_instance_x.m 这样的方式去存取一个结构里面的成员变数了。

所以,g_m(g) 代表的是,对於括号内的暂存器(这里是 g,通用暂存器 s11 在 Golang 里面的别名),将它当作一个 g struct (底线之前为结构名称)的位址,然後取得 m 这个成员的位移量。以这个例子来说,反应在 Golang 与 GNU 两种风格的组合语言就是

   MOV     g_m(g), A0
   
   就是
   
   0x80254500:      ld      a0,48(s11)

我们可以如何印证这件事情?g 结构内的 m 成员的偏移量是 48 个位元组,这个可以透过静态分析观察 g 的内部结构,再加以计算。事实上,在 m 成员之前有 6 个指标,因此 m 的偏移量是 6*8 = 48 位元组,也就不足为奇。

也就是说,Golang 组合语言里面的底线,等同於在 .go 档当中的 . 存取成员运算子的弱化版:它是一个偏移量,需要搭配存放在暂存器中的记忆体位址当作基底。

回顾产生问题的部分,从 objdump 工具或是 gdb 反组译,看到的是

   0x80254500 <runtime.gcWriteBarrier+16>:      ld      a0,48(s11)
   0x80254504 <runtime.gcWriteBarrier+20>:      ld      a0,160(a0)
   0x80254508 <runtime.gcWriteBarrier+24>:      lui     t6,0x1
   0x8025450c <runtime.gcWriteBarrier+28>:      add     t6,a0,t6
=> 0x80254510 <runtime.gcWriteBarrier+32>:      ld      a1,1728(t6)

这部分的语义细节非常少,只看到一些加加减减的运算。所以还是看原始档:

647 TEXT runtime·gcWriteBarrier(SB),NOSPLIT,$2216   ...        
651         MOV     g_m(g), A0 
652         MOV     m_p(A0), A0
653         MOV     (p_wbBuf+wbBuf_next)(A0), A1
...

我们就更能够理解这一段真正的意义所在。首先是从 g 暂存器存取 m,然後是从这个 m 取得 p,然後再取得两个偏移量一口气加总起来从这个 p 的基底开始算,最後在真正能够把该记忆体的内容存放在 A1 之前,就已经发生了错误。

使用 GDB 弥补静态分析的弱点

以上都是静态分析,光看程序码就能够理解的,但这没有办法帮助我们理解问题的成因。我们需要一些现场的照片和线索,才有办法进行推理。这就是 GDB 的强项了。

首先,先窥探一下 s11,也就是 Golang 这边称呼的 g。其实 g 是 Golang 执行期模型里面很重要的一个角色:Goroutine。它是比执行绪(thread)更轻量的共常式(coroutine),完全由使用者空间的执行期控制。但我们这些先无需理解细节,只要知道在绝大部分的时间点,一支 RISC-V 上的 Golang 程序的 s11 或是 g 暂存器里面存放的即是当前所属的 Goroutine 即可。

对於 Golang 的 G-M-P 模型感兴趣的读者可以参考拙作,虽然只是一篇学习笔记,但有附带一些更详尽的说明文章。

(gdb) p/x $s11
$3 = 0x802abea0
(gdb) x/10gx 0x802abea0
0x802abea0 <runtime.g0>:        0x000000008ffeffe0      0x000000008fffffe0
0x802abeb0 <runtime.g0+16>:     0x000000008fff0380      0x000000008fff0380
0x802abec0 <runtime.g0+32>:     0x0000000000000000      0x0000000000000000
0x802abed0 <runtime.g0+48>:     0x00000000802ac040      0x0000000000000000
0x802abee0 <runtime.g0+64>:     0x0000000000000000      0x0000000000000000

经过 GDB 的符号解析,我们知道这时候的 s11 ,尽管仍在非常早期的阶段,已经设置为 g0 这个 Golang 预设的 Goroutine 了,而不是某个在执行的後期才动态配置出来的 Goroutine。

s11 为基底,48 为偏移量,取得的值是 0x802ac040,所以这就是那个 m

x/10gx 0x00000000802ac040
0x802ac040 <runtime.m0>:        0x00000000802abea0      0x34e852b3f52eb1f9
0x802ac050 <runtime.m0+16>:     0x97d2168c4acd91e9      0x18afc19f7bb6f487
0x802ac060 <runtime.m0+32>:     0xf35dac95931c6ccc      0x39cfba9e1ee9dd06
0x802ac070 <runtime.m0+48>:     0xa306e80df3b88efc      0x68c2b9fd68b8349d
0x802ac080 <runtime.m0+64>:     0xc1e6df31a41e9370      0x001d90f047b05275
...
0x802ac0d0 <runtime.m0+144>:    0x04a8fd952fb5acef      0x69821f9b0ef12226
0x802ac0e0 <runtime.m0+160>:    0x74e0e55be0d61f59      0xa344a5670bbf4628
0x802ac0f0 <runtime.m0+176>:    0xad5cad7fddacc029      0x17088f3812d6f3cc

这个 m0 也是一个预先定义好的变数,两者同在 src/runtime/proc.go 之中被定义。偏移量 160 的内容是 0x74e0e55be0d61f59 当然看起来就不是一个很正常的位址,最後的错误也发生在这里:这个位置被当作 P 结构的基底去存取了。

话说这个 src/runtime/proc.go 档案的一开头也有非常详尽的 G-M-P 模型描述,事实上,是整个 Golang 的排程器(scheduler)的描述。

但如果 m0 的内容本来就包含这些乱七八糟的值,那我们又有什麽方法可以避免这个错误呢?

附录:建置 OpenSBI

若想略过以下步骤,也可以直接使用今天更新的 Hoddarla repomake opensbi 指令。

取得 OpenSBI 原始码

有两种方法,一种是取得 tar 原始档案集(tarball),位在这里;或是直接使用 git 指令取得最新的开发版本,

git clone https://github.com/riscv-software-src/opensbi

编译

笔者假设各位有兴趣的读者都已经在先前的章节中,曾经以 . .hdlarc 指令将 RISC-V 的交叉工具链放置在可执行的路径环境变数之中。

  1. 进入原始码资料夹中。
  2. CROSS_COMPILE=riscv64-buildroot-linux-musl- make PLATFORM=generic

生成的 build/platform/generic/firmware/fw_jump.bin 档案,即是本日实验当中用来当作 -bios 参数传入的档案罗!

小结

予焦啦!今日在这里戛然而止。分明才刚开始的旅程,为什麽看起来已经撞上了一堵高墙呢?理所当然的存取,按着记忆体基底与偏移量取值,只是原先那些结构体里面的值偏偏就是会造成存取错误的值。

先给各位读者一个关键字:.bss。我们明日再会!


<<:  Day_05 opkg套件管理

>>:  Unity与Photon的新手相遇旅途 | Day2-环境介绍

【设计+切版30天实作】|Day12 - 怎麽让使用者选中您想要他选的Plans设计?

设计大纲 今天要来设计「方案」,外面的网站通常会有3个方案供使用者选择。另外如果要吸引使用者注册的话...

资安学习路上-渗透测试实务2

弱点扫描(机器扫描) 蒐集对方系统资讯,可透过工具列举出易受攻击的弱点 上图取自台科大资安社课教材 ...

卡夫卡的藏书阁【Book22】- Kafka - KafkaJS 消费者 4

“Love is, that you are the knife which I plunge i...

#2 - Button文字换起来! (CSS: 移动位置)

Day 1 介绍了用CSS 伪元素的方式放大缩小变宽去做连结特效。传送门 今天也选了几个button...

Day34. 范例:歌曲排行(迭代器模式)

本文同步更新於blog 需求一:KTV系统要按照新增到系统的时间,由旧到新,实作歌曲排行 定义系统...