Markdown to HTML

AuroBreeze Blog

A tiny, fast Markdown blog for GitHub Pages.

一次FrostVista的排错

一次FrostVista的排错

前言

那几天在尝试使用gdb来追踪OS的运行流程的时候发现,自己的OS无法在-O0优化下运行(无优化),但是可以在-O2优化下运行,这就让我感到很奇怪,这是为什么?同一套代码竟然会在不同的优化下产生这么大的差异?

优化

1. -O0:零优化(默认级别)

  • 核心目标:最快的编译速度,最好的调试体验。

  • 特点

    • 如果你在编译时不加任何 -O 参数,编译器默认使用的就是 -O0

    • 编译器几乎不做任何优化。它会将源代码机械、线性地翻译成机器指令。

    • 每一个变量都会被完整地保存在内存中,每一次运算都会老老实实地从内存读、计算、再写回内存。

  • 适用场景日常开发和调试(Debug)。因为代码没有被重新排序或精简,当你使用 GDB 等调试器单步执行时,当前执行的汇编指令与你的源代码能做到完美的逐行对应,你可以随时查看任何变量的准确值。

2. -O1:基础优化

  • 核心目标:在不大幅增加编译时间的前提下,减少代码体积并提升运行速度。

  • 特点

    • 编译器会尝试做一些简单的优化,例如:去除永远不会执行的死代码(Dead code elimination)、简单的常量折叠(Constant folding)等。

    • 它的原则是:只做那些“性价比最高”的优化,既不拖慢编译速度,也不消耗过多内存。

  • 适用场景:想要稍微提升一下运行速度,但又不想像 -O2 那样等太久编译的场景。

3. -O2:标准/推荐优化

  • 核心目标:在不牺牲过多代码体积的前提下,最大化代码的执行效率

  • 特点

    • 包含了 -O1 的所有优化,并开启了绝大多数不需要进行“空间换时间”折中的优化。

    • 编译器会进行更深入的指令调度、公共子表达式消除、寄存器分配优化等。

    • 代码执行顺序可能会被打乱。编译器可能会把循环里的计算提到循环外,或者把几个操作合并。

  • 适用场景生产环境(Release)的标准配置。大多数开源软件和商业软件在发布时都会默认使用 -O2,因为它在性能和文件大小之间取得了最佳平衡。在这个级别下调试会非常痛苦,因为变量可能被优化进了寄存器甚至直接消失了,单步调试时代码会来回乱跳。

更多的详细信息请查阅:https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

问题发现

在整个代码运行的过程中,看到整体的OS代码会卡在kalloc_init这个过程中,这个过程是将内存收集并存放的过程,这个处理的时间会比较长。

当使用gdb调试卡住的时候,就可以使用Ctrl+C停下,然后gdb会给你当前运行的地址。

通过使用info reg查看所有的寄存器

ra             0x80008316       0x80008316
sp             0xffffffc08003fe30       0xffffffc08003fe30
gp             0x0      0x0
tp             0x0      0x0
t0             0xffffffc080dce000       -272715948032
t1             0x8003c000       2147729408
t2             0x0      0
fp             0x80035e38       0x80035e38
s1             0x0      0
a0             0xffffffc080dce000       -272715948032
a1             0xffffffc080004a7c       -272730404228
a2             0x80035e38       2147704376
a3             0x0      0
a4             0xf63    3939
a5             0x1000   4096
a6             0x0      0
a7             0x54494d45       1414090053
s2             0x0      0
s3             0x0      0
s4             0x0      0
s5             0x0      0
s6             0x0      0
s7             0x0      0
s8             0x0      0
s9             0x0      0
s10            0x0      0
s11            0x0      0
t3             0x0      0
t4             0x0      0
t5             0x0      0
t6             0x0      0
pc             0x80008290       0x80008290

还有info reg mscratch mtval mepc mcause

(gdb) info reg mscratch mtval mepc mcause
mscratch       0x8000000000000007       -9223372036854775801
mtval          0xffffffc080dce000       -272715948032
mepc           0x80008294       2147517076
mcause         0x7      7

通过观察看到a7 = 0x54494d45这正是我们使用sbi_set_timer来设置下一次tick时间的调用,不过很奇怪的是,在这里面a0, a1, a2.....a5这里面的数值有很多奇怪的地方,我们的sbi_set_timer只是会把这些数值置为0,而不是这些看不懂的魔法数字,我们也可以看到sp目前还指向虚拟地址空间的高地址空间, 但是发现ra也存放了返回的地址。

如果我们去查看ra保存的返回地址会发现,是在这个地方

  call m_trap

  mv a0, s0

ra竟然指向mv a0, s0,那么意思也就是说,这个时候已经进入开始执行了m_trap,但是在m_trap还没有处理完的时候就出发了其他的异常。

如果我们查看mepc会发现,mepc指向的位置是

   # Protect kernel timer interrupt context by swapping mscratch and a0
  csrrw a0, mscratch, a0

  # save the register to the memory pointed to by mscratch
  sd ra, 0(a0)

指向的是sd ra, 0(a0)这个位置,然后整个OS就卡住了

为什么会在这里卡住?

我们可以看到,ra已经指向了下面的调用m_trap的位置,证明,这个m_trap确实是调用了,不过,没有处理完就出现了错误,导致整个OS无法运行。

为什么会出现这种情况?为什么-O2可以正常运行,但是-O0就会出问题,我们将两种编译后的内核反汇编后查看m_trap函数

-O0版本:

void m_trap(uint64 mcause, uint64 mepc, uint64 *regs)
{
ffffffc0800071c6:   7159                    addi    sp,sp,-112
ffffffc0800071c8:   f486                    sd  ra,104(sp)
ffffffc0800071ca:   f0a2                    sd  s0,96(sp)
ffffffc0800071cc:   1880                    addi    s0,sp,112
ffffffc0800071ce:   faa43423            sd  a0,-88(s0)
ffffffc0800071d2:   fab43023            sd  a1,-96(s0)
ffffffc0800071d6:   f8c43c23            sd  a2,-104(s0)
    // Check whether the most significant bit is ahn exception or an
    // interrupt
    int is_interrupt = (mcause >> 63) & 1;
ffffffc0800071da:   fa843783            ld  a5,-88(s0)
ffffffc0800071de:   93fd                    srli    a5,a5,0x3f
ffffffc0800071e0:   fef42623            sw  a5,-20(s0)

...

-O2版本:

ffffffc080003fa6 <m_trap>:
{
    // Check whether the most significant bit is ahn exception or an
    // interrupt
    int is_interrupt = (mcause >> 63) & 1;

    uint64 code = mcause & ((1ULL << 63) - 1);
ffffffc080003fa6:   577d                    li  a4,-1
ffffffc080003fa8:   00175793            srli    a5,a4,0x1
ffffffc080003fac:   8fe9                    and a5,a5,a0
    int is_interrupt = (mcause >> 63) & 1;
ffffffc080003fae:   03f55693            srli    a3,a0,0x3f

    // WARNING: Ban kprintf and panic
    // Because the current SP is not in a valid state, the SP has already
    // been saved, and the current SP is in an undefined state.
    if (is_interrupt) {
ffffffc080003fb2:   00054663            bltz    a0,ffffffc080003fbe <m_trap+0x18>
            w_mip(r_mip() | MIP_STIP); // Key: Set STIP to allow S
                           // to receive scause = 5
        }
...

在这里可以看到,-O0版本的代码,将会调整sp将参数存放到栈上,但是,在M态下,MMU是默认禁用的,我们在上面可以看到我们的sp指针指向虚拟高地址,这个地址根本不存在在物理地址上,所以当调整sp,准备访问栈的时候就会报错。

而在-O2优化的版本下,所有的数据都存在在寄存器上,不依赖栈,所以就没有使用sp 指针从而避免了这个错误。

这也就是导致两种不同的优化方案而导致的不同。

其他问题

为什么将下次时间设置的够远就可以正常运行?

如果设置的够远,OS也仅仅是在这段时间完成任务后,再次遇到这个问题,然后卡住,-O0的情况下,只要使用了sbi的调用,那么这个问题是必然会出现的,只要tick发生了,那就一定会因为sp的问题而卡住

为什么第一次设置sbi调用就没有问题?

这个整个问题的核心就在于sp指向位置,在第一次调用sbi_set_timer的时候,这个时候是在执行timerinit函数,还没有通过switch_to_high_addresssp拉到虚拟高地址。

所以,这个时候sp处在虚拟低地址,也就是VA=PA的时候,这个时候传入的sp就是可以直接在物理地址使用,所以这个问题就没有触发。

如何解决这个问题?

要解决这个也很简单,因为-O0优化的版本进入m_trap的第一件事情就是调整sp,这个时候sp是虚拟高地址,那我们就可以在进入m_trap之前调整sp指针,让sp指向一个专门为M态服务的栈空间。

最好的方法就是在start.S中划分一段空间作为栈空间

    .section .bss.m_trap_stack
    .align 12
    .global m_trap_stack
m_trap_stack:
    .space 0x4000  # 16KB

    .global m_trap_stack_top
m_trap_stack_top:

在进入m_trap的时候调整sp

  csrr t0, mhartid         # Get current CPU ID
  slli t0, t0, 12          # Multiply hartid by 4096 (left shift by 12)
  la sp, m_trap_stack_top
  sub sp, sp, t0           # Adjust stack pointer for the specific CPU

这样就可以了。

疑问

为什么不在内核代码中,建立一个大的空数组作为栈空间,或者为什么不在ld文件中建立一个空间。

首先在ld文件中建立这样的一个空间,和在start.S这里是一样的。

在内核代码中建立这样的空间,首先是麻烦,建立数组后,还有再把指针指向数组的末尾,其次,是内存对齐的问题,栈SP指针要求16字节对齐,这样的话,还需要再为了数组写一个对齐,这个有些不够优雅,而在start.S中用个.align 4就可以了。