题目要求

请学习 gdb 调试工具的使用(这对后续调试很重要),并通过 gdb 简单跟踪从机器加电到跳转到 0x80200000 的简单过程。只需要描述重要的跳转即可,只需要描述在 qemu 上的情况。

启动调试和监听的指令

使用[[010 基于 SBI 服务完成输出和关机#^fb8fca|之前学到的指令]],开启两个bash,在GDB进行监听的那个bash里进行调试.

题目中提到的tips可以帮助我们专注于调试,而不关心具体的指令.

cd ~/App/rCore-Tutorial-v3/os
make debug

这里的运行都没有问题,但是通过自己的能力去读RISCV的汇编未免有些太难了.但是我们仍然可以选择啃下来,让我们看看官方给了什么tips:

  • 事实上进入 rustsbi 之后就不需要使用 gdb 调试了。可以直接阅读代码。rustsbi起始代码 。
  • 可以使用示例代码 Makefile 中的 make debug 指令。
  • 一些可能用到的 gdb 指令:
    • x/10i 0x80000000 : 显示 0x80000000 处的10条汇编指令。
    • x/10i $pc : 显示即将执行的10条汇编指令。
    • x/10xw 0x80000000 : 显示 0x80000000 处的10条数据,格式为16进制32bit。
    • info register: 显示当前所有寄存器信息。
    • info r t0: 显示 t0 寄存器的值。
    • break funcname: 在目标函数第一条指令处设置断点。
    • break *0x80200000: 在 0x80200000 处设置断点。
    • continue: 执行直到碰到断点。
    • si: 单步执行一条汇编指令。

实际上第一条tip是我们刚刚忽略了的,如果可以直接阅读rustsbi那么明白上电之后会做什么就很简单了,我们从问答题中已知qemu上电之后的步骤: ^ed00ab

  1. 运行一些初始化并且跳转到rustsbi

    • 读取当前的 Hart ID CSR mhartid 写入寄存器 a0
    • (我们还没有用到:将 FDT (Flatten device tree) 在物理内存中的地址写入 a1
    • 跳转到 start_addr ,在我们实验中是 RustSBI 的地址
  2. 运行RustSBI进行硬件的初始化
  3. 运行entry.asm分配启动栈,然后把控制权交给rust
  4. 运行内核

所以其实我们现在感兴趣的就是进行了什么样的初始化?RustSBI的起始地址在哪?后边的行为能不能对应上,这样使得我们对开发的内容更加的熟悉更加的融会贯通.

参考官方给的启动流程,和[[08 内核第一条指令#^6e433b|我们自己的笔记]]:

在Qemu模拟的 virt 硬件平台上,物理内存的起始物理地址为 0x80000000 ,物理内存的默认大小为 128MiB ,它可以通过 -m 选项进行配置。如果使用默认配置的 128MiB 物理内存则对应的物理地址区间为 [0x80000000,0x88000000) 。如果使用上面给出的命令启动 Qemu ,那么在 Qemu 开始执行任何指令之前,首先把两个文件加载到 Qemu 的物理内存中:即作把作为 bootloader 的 rustsbi-qemu.bin 加载到物理内存以物理地址 0x80000000 开头的区域上,同时把内核镜像 os.bin 加载到以物理地址 0x80200000 开头的区域上。

为什么加载到这两个位置呢?这与 Qemu 模拟计算机加电启动后的运行流程有关。一般来说,计算机加电之后的启动流程可以分成若干个阶段,每个阶段均由一层软件或 固件 负责,每一层软件或固件的功能是进行它应当承担的初始化工作,并在此之后跳转到下一层软件或固件的入口地址,也就是将计算机的控制权移交给了下一层软件或固件。Qemu 模拟的启动流程则可以分为三个阶段:第一个阶段由固化在 Qemu 内的一小段汇编程序负责;第二个阶段由 bootloader 负责;第三个阶段则由内核镜像负责。

  • 第一阶段:将必要的文件载入到 Qemu 物理内存之后,Qemu CPU 的程序计数器(PC, Program Counter)会被初始化为 0x1000 ,因此 Qemu 实际执行的第一条指令位于物理地址 0x1000 ,接下来它将执行寥寥数条指令并跳转到物理地址 0x80000000 对应的指令处并进入第二阶段。从后面的调试过程可以看出,该地址 0x80000000 被固化在 Qemu 中,作为 Qemu 的使用者,我们在不触及 Qemu 源代码的情况下无法进行更改。 ^776ff0
  • 第二阶段:由于 Qemu 的第一阶段固定跳转到 0x80000000 ,我们需要将负责第二阶段的 bootloader rustsbi-qemu.bin 放在以物理地址 0x80000000 开头的物理内存中,这样就能保证 0x80000000 处正好保存 bootloader 的第一条指令。在这一阶段,bootloader 负责对计算机进行一些初始化工作,并跳转到下一阶段软件的入口,在 Qemu 上即可实现将计算机控制权移交给我们的内核镜像 os.bin 。这里需要注意的是,对于不同的 bootloader 而言,下一阶段软件的入口不一定相同,而且获取这一信息的方式和时间点也不同:入口地址可能是一个预先约定好的固定的值,也有可能是在 bootloader 运行期间才动态获取到的值。我们选用的 RustSBI 则是将下一阶段的入口地址预先约定为固定的 0x80200000 ,在 RustSBI 的初始化工作完成之后,它会跳转到该地址并将计算机控制权移交给下一阶段的软件——也即我们的内核镜像。
  • 第三阶段:为了正确地和上一阶段的 RustSBI 对接,我们需要保证内核的第一条指令位于物理地址 0x80200000 处。为此,我们需要将内核镜像预先加载到 Qemu 物理内存以地址 0x80200000 开头的区域上。一旦 CPU 开始执行内核的第一条指令,证明计算机的控制权已经被移交给我们的内核,也就达到了本节的目标。

我们可以看到作为bootloaderRustSBI的位置在0x80000000.

那么我们不能看代码,需要啃汇编的部分其实就很少了,就是所谓的[[013 GDB跟踪程序#^776ff0|"固化在 Qemu 内的一小段汇编程序"]].其实刚好页对应了[[08 内核第一条指令#^e44d27|原本笔记]]中需要我们探索的部分.

在GDB中键入x/10i $pc,显示10行等待执行的反汇编:

0x0000000000001000 in ?? ()
│(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
│ 0x101e: unimp

首先我们就可以欣喜地观察到:

  1. 0x0000000000001000 in ?? ()这一行,对应了[[013 GDB跟踪程序#^776ff0|"Qemu 实际执行的第一条指令位于物理地址 0x1000 "]]
  2. 0x101a: 0x8000貌似是[[08 内核第一条指令#^b2fc42|原来笔记中]]提到的,跳转到0x80000000的关键,但是同时也观察到这里存储的是0x8000而不是0x80000000.结合后边0x101c: unimp0x101e: unimp两段相邻内存中的unimp(当数据为 0 的时候则会被反汇编为 unimp 指令),可以找到跳转的线索
  3. 实际上到跳转貌似代码不多,可以一步步对应地观察,记得之前提到的跳转到RustSBI之前的[[013 GDB跟踪程序#^ed00ab|一些操作]],我们要尝试看看能不能对应上.

查询[[00 总览#^531b44|RISCV手册]],或者直接使用GPT进行解析,其实这一段指令和[[011 第1章作业题#^038649|作业题中问答题第四题]]的注释部分是对应的:

  1. auipc t0,0x0

    • auipc 是一个原子更新即时数(Atomic Update Immediate Plus Constant)指令,它将PC(程序计数器)的高20位与一个20位的立即数相加,并将结果存储到目的寄存器中。在这里,目的寄存器是t0,立即数是0x0,这意味着auipc将把PC的高20位复制到t0中,实质上是将当前指令的地址(去除低12位)存储到t0中。
  2. addi a2,t0,40
    • addi 是一个带立即数的加法指令,它将t0寄存器的值与一个12位的立即数相加,并将结果存储到a2寄存器中。在这里,立即数是40(十进制),因此此指令将t0中的值(即PC的高20位)与40相加,结果存储在a2中。
  3. csrr a0,mhartid
    • csrr 是一个从CSR(Control and Status Register)读取指令,它将指定的CSR寄存器的值读取到目的寄存器中。在这里,它从mhartid CSR读取值,并将结果存储在a0寄存器中。mhartid CSR存储了当前Hart(硬件线程)的ID。
  4. ld a1,32(t0)
    • ld 是一个长整型(64位)的加载指令,它从内存中加载一个64位的值到目的寄存器中。在这里,它从t0寄存器指向的地址加上32的内存位置加载数据,并将结果存储在a1寄存器中。
  5. ld t0,24(t0)
    • 类似于上一条ld指令,这条指令也是从内存中加载一个64位的值,但是这次是加载到t0寄存器中,从t0指向的地址加上24的内存位置加载数据。
  6. jr t0
    • jr 是跳转寄存器指令,它将程序计数器(PC)设置为t0寄存器的值。这通常用于实现子程序的返回或循环的迭代。

第三条指令就可以对应上[[011 第1章作业题#^67887c|笔记中]]关于mhartid的存储的描述.

其余的指令可以对应上[[08 内核第一条指令#^e44d27|这里]],对于0x1000 和 0x100c 两条指令的重视.首先第一条指令把pc的值和0x0相加储存在t0中,实际上就是储存了pc的值在t0中,此时t0应该为0x1000,因为之前也说了[[013 GDB跟踪程序#^776ff0|"Qemu 实际执行的第一条指令位于物理地址 0x1000 "]].

这里我们可以直接使用指令来验证:

si
info r t0

得到的结果为:

t0             0x1000   4096

第四条指令从t0寄存器指向的地址加上32的内存位置(即0x1020)加载64位数据,并将结果存储在a1寄存器中,那么目前a1的数据我们不知道,但是可以根据[[011 第1章作业题#^67887c|笔记中]]的作用知道这一句是将 FDT (Flatten device tree) 在物理内存中的地址写入 a1,但是可以用GDB调试验证:

si
info r a1

得到的结果为:

a1             0x87000000       2264924160

第五条指令为从内存中加载一个64位的值,但是这次是加载到t0寄存器中,从t0指向的地址加上24的内存位置(即0x1018)加载64位数据,可以看到0x1018后边每一个地址存四位16进制,这里有个点要注意在RISCV中,数据是小端的,也就是从0x1018读取的数据放在最后4位,这样读出来是0000 0000 8000 0000.

│   0x1018:      unimp
│ 0x101a: 0x8000
│ 0x101c: unimp
│ 0x101e: unimp

同样可以使用如下指令验证:

x/1xw 0x1018
x/1xw 0x1019
x/1xw 0x101a
x/1xw 0x101b
x/1xw 0x101c

得到的结果为,可以证明是小端储存的:

(gdb) x/1xw 0x1018
│0x1018: 0x80000000
│(gdb) x/1xw 0x1019
│0x1019: 0x00800000
│(gdb) x/1xw 0x101a
│0x101a: 0x00008000
│(gdb) x/1xw 0x101b
│0x101b: 0x00000080
│(gdb) x/1xw 0x101c
│0x101c: 0x00000000

那么同样可以使用验证t0的值:

si
info r t0

得到的结果:

t0             0x80000000       2147483648

这时候第六条指令就可以完成跳转到0x80000000的任务,后续的动作我们就可以看RustSBI的源码了.

查看RustSBI源码

官方也给出了RustSBI源码的具体位置,但是在GitHub上看源码有点太累了,我们可以把源码cloneworkspace.

git clone https://github.com/rustsbi/rustsbi-qemu.git

我们可以在/rustsbi-qemu/rustsbi-qemu/src,找到main.rs,找到官方推荐我们阅读的L146.

这里因为中间的实现思路我们是不知道的,只看rust_main函数里的注释和一些函数名称,我们大概可以看出实际上RustSBI是初始化了一个Console,在USART的基础上实现了dbcnclint的功能,最终实现了一个Console.

TODO 可能需要更了解RISCVSBI的要求才能完成这一部分的理解.

[rCore学习笔记 013]GDB跟踪程序的更多相关文章

  1. Linux学习笔记15——GDB 命令详细解释【转】

    GDB 命令详细解释 Linux中包含有一个很有用的调试工具--gdb(GNU Debuger),它可以用来调试C和C++程序,功能不亚于Windows下的许多图形界面的调试工具. 和所有常用的调试工 ...

  2. python学习笔记013——模块

    1 模块module 1.1 模块是什么 模块是包含一系列的变量,函数,类等程序组 模块通常是一个文件,以.py结尾 1.2 模块的作用 1. 让一些相关的函数,变量,类等有逻辑的组织在一起,使逻辑更 ...

  3. 【原】Java学习笔记013 - 阶段测试

    package cn.temptation; import java.util.Scanner; public class Sample01 { public static void main(Str ...

  4. python学习笔记013——推导式

    1 推导式简介 推导式comprehensions(又称解析式),是Python的一种独有特性. 推导式是可以从一个数据序列构建另一个新的数据序列的结构体. 推导式有三种形式: 1)列表推导式 (li ...

  5. python学习笔记013——模块中的私有属性

    1 私有属性的使用方式 在python中,没有类似private之类的关键字来声明私有方法或属性.若要声明其私有属性,语法规则为: 属性前加双下划线,属性后不加(双)下划线,如将属性name私有化,则 ...

  6. python学习笔记013——包package

    1 包(模块包)package 1.1 包的定义 包是将模块以文件夹的组织形式进行分组管理的方法 1.2 作用 分类管理,有利于防止命名冲突 可以在需要时加载一个或部分模块,而不是全部模块 mypac ...

  7. python学习笔记013——内置函数dir()

    1 描述 dir() 函数 不带参数时,返回当前范围内的变量.方法和定义的类型列表: 带参数时,返回参数的属性.方法列表. 如果参数包含方法__dir__(),该方法将被调用. 如果参数不包含__di ...

  8. 《软件调试的艺术》学习笔记——GDB使用技巧摘要

    <软件调试的艺术>学习笔记——GDB使用技巧摘要 <软件调试的艺术>,因为名是The Art of Debugging with GDB, DDD, and Eclipse. ...

  9. MIT 6.828 JOS学习笔记2. Lab 1 Part 1.2: PC bootstrap

    Lab 1 Part 1: PC bootstrap 我们继续~ PC机的物理地址空间 这一节我们将深入的探究到底PC是如何启动的.首先我们看一下通常一个PC的物理地址空间是如何布局的:        ...

  10. C语言学习笔记之成员数组和指针

    成员数组和指针是我们c语言中一个非常重要的知识点,记得以前在大学时老师一直要我们做这类的练习了,但是最的还是忘记了,今天来恶补一下.     单看这文章的标题,你可能会觉得好像没什么意思.你先别下这个 ...

随机推荐

  1. flutter 打包web应用指定上下文

    使用flutter build web命令打包的应用不包含上下文,只能部署在根目录.如何指定上下文,部署在子目录下呢? 有两种办法: 1.修改web/index.html文件 修改 <base ...

  2. itest(爱测试)开源接口测试&敏捷测试&极简项目管理 6.6.6 发布,新增接口mock

    (一)itest 简介及更新说明 itest 开源敏捷测试管理,testOps 践行者,极简的任务管理,测试管理,缺陷管理,测试环境管理,接口测试,接口Mock 6合1,又有丰富的统计分析.可按测试包 ...

  3. 使用Vulkan-Loader将ncnn代码改成Dynamic Loader Vulkan的形式

    原本你写的程序是静态链接的系统的vulkan-1.dll,如果系统不存在vulkan-1.dll,则会直接崩溃. 关于将ncnn静态链接vulkan改成动态加载vulkan的形式,然后提供这两个函数 ...

  4. js获取指定日期的前一天/后一天

    date代表指定日期,格式:2018-09-27 day代表天数,-1代表前一天,1代表后一天 // date 代表指定的日期,格式:2018-09-27// day 传-1表始前一天,传1表始后一天 ...

  5. 时间格式化转换及时间比较compareTo,Controller层接收参数格式化,从数据源头解决时间格式错误数据对接口的影响

    时间格式化转换及时间比较compareTo,Controller层接收参数格式化,从数据源头解决时间格式错误数据对接口的影响 /** * 时间格式的转换:在具体报错的地方做转换,可能不能从根本上面解决 ...

  6. FreeRTOS简单内核实现5 阻塞延时

    0.思考与回答 0.1.思考一 为什么 FreeRTOS简单内核实现3 任务管理 文章中实现的 RTOS 内核不能看起来并行运行呢? Task1 延时 100ms 之后执行 taskYIELD() 切 ...

  7. Linux系统与网络管理

    0. 背景 0.1 Unix Unix诞生于1969年 特点 多任务 多用户 多平台 保护模式 可移植操作系统接口(POSIX) 0.2 Linux 与Unix关系 类Unix系统,完全按照Unix的 ...

  8. 简约-Markdown教程

    ##注意 * 两个元素之间最好有空行 * 利用\来转义 我是一级标题 ==== 我是二级标题 ---- #我是一级标题 ##我是二级标题 ##<center>标题居中显示</cent ...

  9. dotnet 融合 Avalonia 和 UNO 框架

    现在在 .NET 系列里面,势头比较猛的 UI 框架中,就包括了 Avalonia 和 UNO 框架.本文将告诉大家如何尝试在一个解决方案里面融合 Avalonia 和 UNO 两个框架,即在一个进程 ...

  10. 小米节假日API, 查询调休

    小米的节假日API, 用于查询一年中的第X天是否正在放假或是在调休. 在浏览器中打开保存下来, 一年只需要调用一次即可. https://api.comm.miui.com/holiday/holid ...