[rCore学习笔记 013]GDB跟踪程序
题目要求
请学习 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
- 运行一些初始化并且跳转到
rustsbi
- 读取当前的 Hart ID CSR
mhartid
写入寄存器a0
- (我们还没有用到:将 FDT (Flatten device tree) 在物理内存中的地址写入
a1
) - 跳转到
start_addr
,在我们实验中是 RustSBI 的地址
- 读取当前的 Hart ID CSR
- 运行
RustSBI
进行硬件的初始化 - 运行
entry.asm
分配启动栈,然后把控制权交给rust
- 运行内核
所以其实我们现在感兴趣的就是进行了什么样的初始化?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
,我们需要将负责第二阶段的 bootloaderrustsbi-qemu.bin
放在以物理地址0x80000000
开头的物理内存中,这样就能保证0x80000000
处正好保存 bootloader 的第一条指令。在这一阶段,bootloader 负责对计算机进行一些初始化工作,并跳转到下一阶段软件的入口,在 Qemu 上即可实现将计算机控制权移交给我们的内核镜像os.bin
。这里需要注意的是,对于不同的 bootloader 而言,下一阶段软件的入口不一定相同,而且获取这一信息的方式和时间点也不同:入口地址可能是一个预先约定好的固定的值,也有可能是在 bootloader 运行期间才动态获取到的值。我们选用的 RustSBI 则是将下一阶段的入口地址预先约定为固定的0x80200000
,在 RustSBI 的初始化工作完成之后,它会跳转到该地址并将计算机控制权移交给下一阶段的软件——也即我们的内核镜像。 - 第三阶段:为了正确地和上一阶段的 RustSBI 对接,我们需要保证内核的第一条指令位于物理地址
0x80200000
处。为此,我们需要将内核镜像预先加载到 Qemu 物理内存以地址0x80200000
开头的区域上。一旦 CPU 开始执行内核的第一条指令,证明计算机的控制权已经被移交给我们的内核,也就达到了本节的目标。
我们可以看到作为bootloader
的RustSBI
的位置在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
首先我们就可以欣喜地观察到:
0x0000000000001000 in ?? ()
这一行,对应了[[013 GDB跟踪程序#^776ff0|"Qemu 实际执行的第一条指令位于物理地址0x1000
"]]0x101a: 0x8000
貌似是[[08 内核第一条指令#^b2fc42|原来笔记中]]提到的,跳转到0x80000000
的关键,但是同时也观察到这里存储的是0x8000
而不是0x80000000
.结合后边0x101c: unimp
和0x101e: unimp
两段相邻内存中的unimp
(当数据为 0 的时候则会被反汇编为unimp
指令),可以找到跳转的线索- 实际上到跳转貌似代码不多,可以一步步对应地观察,记得之前提到的跳转到
RustSBI
之前的[[013 GDB跟踪程序#^ed00ab|一些操作]],我们要尝试看看能不能对应上.
查询[[00 总览#^531b44|RISCV手册]],或者直接使用GPT
进行解析,其实这一段指令和[[011 第1章作业题#^038649|作业题中问答题第四题]]的注释部分是对应的:
auipc t0,0x0
auipc
是一个原子更新即时数(Atomic Update Immediate Plus Constant)指令,它将PC(程序计数器)的高20位与一个20位的立即数相加,并将结果存储到目的寄存器中。在这里,目的寄存器是t0
,立即数是0x0
,这意味着auipc
将把PC的高20位复制到t0
中,实质上是将当前指令的地址(去除低12位)存储到t0
中。
addi a2,t0,40
addi
是一个带立即数的加法指令,它将t0
寄存器的值与一个12位的立即数相加,并将结果存储到a2
寄存器中。在这里,立即数是40
(十进制),因此此指令将t0
中的值(即PC的高20位)与40相加,结果存储在a2
中。
csrr a0,mhartid
csrr
是一个从CSR(Control and Status Register)读取指令,它将指定的CSR寄存器的值读取到目的寄存器中。在这里,它从mhartid
CSR读取值,并将结果存储在a0
寄存器中。mhartid
CSR存储了当前Hart(硬件线程)的ID。
ld a1,32(t0)
ld
是一个长整型(64位)的加载指令,它从内存中加载一个64位的值到目的寄存器中。在这里,它从t0
寄存器指向的地址加上32的内存位置加载数据,并将结果存储在a1
寄存器中。
ld t0,24(t0)
- 类似于上一条
ld
指令,这条指令也是从内存中加载一个64位的值,但是这次是加载到t0
寄存器中,从t0
指向的地址加上24的内存位置加载数据。
- 类似于上一条
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
上看源码有点太累了,我们可以把源码clone
到workspace
.
git clone https://github.com/rustsbi/rustsbi-qemu.git
我们可以在/rustsbi-qemu/rustsbi-qemu/src
,找到main.rs
,找到官方推荐我们阅读的L146
.
这里因为中间的实现思路我们是不知道的,只看rust_main
函数里的注释和一些函数名称,我们大概可以看出实际上RustSBI
是初始化了一个Console
,在USART
的基础上实现了dbcn
和clint
的功能,最终实现了一个Console
.
TODO 可能需要更了解RISCV
和SBI
的要求才能完成这一部分的理解.
[rCore学习笔记 013]GDB跟踪程序的更多相关文章
- Linux学习笔记15——GDB 命令详细解释【转】
GDB 命令详细解释 Linux中包含有一个很有用的调试工具--gdb(GNU Debuger),它可以用来调试C和C++程序,功能不亚于Windows下的许多图形界面的调试工具. 和所有常用的调试工 ...
- python学习笔记013——模块
1 模块module 1.1 模块是什么 模块是包含一系列的变量,函数,类等程序组 模块通常是一个文件,以.py结尾 1.2 模块的作用 1. 让一些相关的函数,变量,类等有逻辑的组织在一起,使逻辑更 ...
- 【原】Java学习笔记013 - 阶段测试
package cn.temptation; import java.util.Scanner; public class Sample01 { public static void main(Str ...
- python学习笔记013——推导式
1 推导式简介 推导式comprehensions(又称解析式),是Python的一种独有特性. 推导式是可以从一个数据序列构建另一个新的数据序列的结构体. 推导式有三种形式: 1)列表推导式 (li ...
- python学习笔记013——模块中的私有属性
1 私有属性的使用方式 在python中,没有类似private之类的关键字来声明私有方法或属性.若要声明其私有属性,语法规则为: 属性前加双下划线,属性后不加(双)下划线,如将属性name私有化,则 ...
- python学习笔记013——包package
1 包(模块包)package 1.1 包的定义 包是将模块以文件夹的组织形式进行分组管理的方法 1.2 作用 分类管理,有利于防止命名冲突 可以在需要时加载一个或部分模块,而不是全部模块 mypac ...
- python学习笔记013——内置函数dir()
1 描述 dir() 函数 不带参数时,返回当前范围内的变量.方法和定义的类型列表: 带参数时,返回参数的属性.方法列表. 如果参数包含方法__dir__(),该方法将被调用. 如果参数不包含__di ...
- 《软件调试的艺术》学习笔记——GDB使用技巧摘要
<软件调试的艺术>学习笔记——GDB使用技巧摘要 <软件调试的艺术>,因为名是The Art of Debugging with GDB, DDD, and Eclipse. ...
- MIT 6.828 JOS学习笔记2. Lab 1 Part 1.2: PC bootstrap
Lab 1 Part 1: PC bootstrap 我们继续~ PC机的物理地址空间 这一节我们将深入的探究到底PC是如何启动的.首先我们看一下通常一个PC的物理地址空间是如何布局的: ...
- C语言学习笔记之成员数组和指针
成员数组和指针是我们c语言中一个非常重要的知识点,记得以前在大学时老师一直要我们做这类的练习了,但是最的还是忘记了,今天来恶补一下. 单看这文章的标题,你可能会觉得好像没什么意思.你先别下这个 ...
随机推荐
- flutter 打包web应用指定上下文
使用flutter build web命令打包的应用不包含上下文,只能部署在根目录.如何指定上下文,部署在子目录下呢? 有两种办法: 1.修改web/index.html文件 修改 <base ...
- itest(爱测试)开源接口测试&敏捷测试&极简项目管理 6.6.6 发布,新增接口mock
(一)itest 简介及更新说明 itest 开源敏捷测试管理,testOps 践行者,极简的任务管理,测试管理,缺陷管理,测试环境管理,接口测试,接口Mock 6合1,又有丰富的统计分析.可按测试包 ...
- 使用Vulkan-Loader将ncnn代码改成Dynamic Loader Vulkan的形式
原本你写的程序是静态链接的系统的vulkan-1.dll,如果系统不存在vulkan-1.dll,则会直接崩溃. 关于将ncnn静态链接vulkan改成动态加载vulkan的形式,然后提供这两个函数 ...
- js获取指定日期的前一天/后一天
date代表指定日期,格式:2018-09-27 day代表天数,-1代表前一天,1代表后一天 // date 代表指定的日期,格式:2018-09-27// day 传-1表始前一天,传1表始后一天 ...
- 时间格式化转换及时间比较compareTo,Controller层接收参数格式化,从数据源头解决时间格式错误数据对接口的影响
时间格式化转换及时间比较compareTo,Controller层接收参数格式化,从数据源头解决时间格式错误数据对接口的影响 /** * 时间格式的转换:在具体报错的地方做转换,可能不能从根本上面解决 ...
- FreeRTOS简单内核实现5 阻塞延时
0.思考与回答 0.1.思考一 为什么 FreeRTOS简单内核实现3 任务管理 文章中实现的 RTOS 内核不能看起来并行运行呢? Task1 延时 100ms 之后执行 taskYIELD() 切 ...
- Linux系统与网络管理
0. 背景 0.1 Unix Unix诞生于1969年 特点 多任务 多用户 多平台 保护模式 可移植操作系统接口(POSIX) 0.2 Linux 与Unix关系 类Unix系统,完全按照Unix的 ...
- 简约-Markdown教程
##注意 * 两个元素之间最好有空行 * 利用\来转义 我是一级标题 ==== 我是二级标题 ---- #我是一级标题 ##我是二级标题 ##<center>标题居中显示</cent ...
- dotnet 融合 Avalonia 和 UNO 框架
现在在 .NET 系列里面,势头比较猛的 UI 框架中,就包括了 Avalonia 和 UNO 框架.本文将告诉大家如何尝试在一个解决方案里面融合 Avalonia 和 UNO 两个框架,即在一个进程 ...
- 小米节假日API, 查询调休
小米的节假日API, 用于查询一年中的第X天是否正在放假或是在调休. 在浏览器中打开保存下来, 一年只需要调用一次即可. https://api.comm.miui.com/holiday/holid ...