RISC-V from Scratch 2

RISC-V from scratch 2

今天,我们继续翻译 RISC-V from scratch 系列的第二部分,原文链接这是该系列的 github 库。

简介

快速回顾,通过 RISC-V from scratch 系列课程,我们将会探索很多与 RISC-V 及其生态相关的底层概念(例如编译、链接、原语运行时、汇编等)。在第一篇博文中,我们简短的讨论一下 RISC-V 以及为什么它很重要,并搭建起 RISC-V 的工具链,最后在 RISC-V 模拟器和 SiFive’s freedom-e-sdk 的帮助下构建并运行一个简单的 C 程序。

Freedom-e-sdk 使我们在仿真或真正的 RISC-V 处理器上编译,调试和运行任何 C 程序变得很简单。不必担心什么链接脚本、编写运行时来设置堆栈,调用main等的运行时。如果你希望快速提高工作效率,那就太好了,但是这些细节正是我们想要学习的东西!

在这篇文章中,我们将摆脱 freedom-e-sdk 。我们将编写并尝试调试自己的 C 程序,揭示隐藏在 main 后面的秘密,并检查 qemu 虚拟机的硬件布局。然后,我们将检查和修改链接器脚本,编写自己的 C 运行时以设置并运行我们的程序,最后调用 GDB 并逐步执行程序。

搭建环境

如果你还未看本系列博客的第一部分,没有安装 riscv-qemu 和 RISC-V 工具链,那么赶紧点击上面标题的链接,跳转到 “QEMU and RISC-V toolchain setup”

之后,再将博主创建的 github 库下载下来,作为我们的工作点。

1
2
3
4
5
6
git clone git@github.com:twilco/riscv-from-scratch.git
# or `git clone https://github.com/twilco/riscv-from-scratch.git` to clone
# via HTTPS rather than SSH
# alternatively, if you are a GitHub user, you can fork this repo.
# https://help.github.com/en/articles/fork-a-repo
cd riscv-from-scratch/work

译注:亲测无需下载 github 库也可实现下面的实验。

天真的方法

好,让我们写一个简单的 C 程序,开始我们的旅途!

1
2
3
4
5
6
7
8
9
10
// file: riscv-from-scratch/work/add.c

int main() {
int a = 4;
int b = 12;
while (1) {
int c = a + b;
}
return 0;
}

我们想要跑该程序,第一步就是编译它,生成相应的可执行文件。

1
2
3
4
# -O0 to disable all optimizations. Without this, GCC might optimize 
# away our infinite addition since the result 'c' is never used.
# -g to tell GCC to preserve debug info in our executable.
riscv64-unknown-elf-gcc add.c -O0 -g

编译器生成了 a.out 文件,这是 gcc 在没有给定生成文件名字的情况下的默认名。现在,我们可以在 qemu 里面运行它了:

1
2
3
4
5
6
7
8
9
# -machine tells QEMU which among our list of available machines we want to
# run our executable against. Run qemu-system-riscv64 -machine help to list
# all available machines.
# -m is the amount of memory to allocate to our virtual machine.
# -gdb tcp::1234 tells QEMU to also start a GDB server on localhost:1234 where
# TCP is the means of communication.
# -kernel tells QEMU what we're looking to run, even if our executable isn't
# exactly a "kernel".
qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out

我们选择了 virt RISC-V 虚拟机,它是 riscv-qemu 自带的

既然我们的程序已经在 QEMU 中运行,并且在主机端口 1234 打开了 TCP 连接,用于连接 GDB ,那么我们在另一个终端,打开 GDB 与之相连吧:

1
2
3
4
# --tui gives us a (t)extual (ui) for our GDB session.
# While we can start GDB without any arguments, specifying 'a.out' tells GDB
# to load debug symbols from that file for the newly created session.
riscv64-unknown-elf-gdb --tui a.out

进入了 GDB 的界面:

1
2
3
4
5
6
7
8
9
10
11
This GDB was configured as "--host=x86_64-apple-darwin17.7.0 --target=riscv64-unknown-elf".           │
Type "show configuration" for configuration details. │
For bug reporting instructions, please see: │
<http://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"... │
Reading symbols from a.out... │
(gdb)

当然,我们还需要告诉 GDB 有一个已经在运行的程序在等着它调试,这和平时使用 GDB 调试程序不同,因为现在我们要调试的程序运行在另一个”机器“上。我们需要打开 TCP 连接,并选择相应的端口,使 GDB 与 程序相连:

1
2
(gdb) target remote :1234                                                                             │
Remote debugging using :1234

现在,我们设置断点了

1
2
3
4
(gdb) b main
Breakpoint 1 at 0x1018e: file add.c, line 2.
(gdb) b 5 # this is the line within the forever-while loop. int c = a + b;
Breakpoint 2 at 0x1019a: file add.c, line 5.

最后,让程序继续运行,直到遇见断点。

1
2
(gdb) c
Continuing.

很快,你就会发现程序一直卡死在这里,不会遇到我们之前设置的断点。这到底是怎么一回事呢?我们可以先看一下 GDB 给我们提供的信息:

看一下图中的几个红框:

  1. GDB 无法找到源代码,这原本是展示源代码和断点位置的地方
  2. GDB 不知道现在运行到第几行,并且 PC 值是 0x0。
  3. 圈出来的值全是 0x0000,很明显 GDB 不知道具体断点位置

揭开 -v 的面纱

为了探明之前究竟发生了什么,我们必须先了解一下 C 程序到底是(尤其是在我们看不见的地方)怎么工作的。我们的程序都有一个 main 函数,但是究竟什么是 main 函数?为什么我们把它叫做 main 而不是 originbegin 或者 entry?很多人都知道我们的程序从 main 开始运行,但究竟是什么魔力使它如此运作?

为了回答这些问题,我们要重新使用 GCC -v 编译一下之前的程序,-v 可以帮助我们获取实际操作的详细输出。

1
2
# In the `riscv-from-scratch/work` directory...
riscv64-unknown-elf-gcc add.c -O0 -g -v

第一件我们需要明白的事情就是,虽然 GCC 是 “GNU C Compiler” 的缩写,gcc 还是会默认链接我们的代码,并且汇编它(加-c 才会告诉 GCC 只进行编译)。那么这和我们之前要探讨的问题有何关系呢?

接下来,我们再细看一下刚刚 GCC 给我们打印出来的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
# The actual `gcc -v` command outputs full paths, but those are quite
# long, so pretend these variables exist.
# $RV_GCC_BIN_PATH = /Users/twilcock/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/
# $RV_GCC_LIB_PATH = $RV_GCC_BIN_PATH/../lib/gcc/riscv64-unknown-elf/8.2.0

$RV_GCC_BIN_PATH/../libexec/gcc/riscv64-unknown-elf/8.2.0/collect2 \
...truncated...
$RV_GCC_LIB_PATH/../../../../riscv64-unknown-elf/lib/rv64imafdc/lp64d/crt0.o \
$RV_GCC_LIB_PATH/riscv64-unknown-elf/8.2.0/rv64imafdc/lp64d/crtbegin.o \
-lgcc --start-group -lc -lgloss --end-group -lgcc \
$RV_GCC_LIB_PATH/rv64imafdc/lp64d/crtend.o
...truncated...
COLLECT_GCC_OPTIONS='-O0' '-g' '-v' '-march=rv64imafdc' '-mabi=lp64d'

不得不承认,即使我裁剪了很多信息,这些信息依然太过于复杂。我必须再详细解释一下。在第一行,gcc 在运行一个名叫 collect2 的程序,并且把参数比如 crt0crtbegin.ocrtend.o,并设置了 -lgcc --start-group等一些 flag 。从 collect2 来看,简而言之,collect2 在开始阶段将很多初始化函数一个一个地链接起来。

知道了这些后,就可以明白事实上 GCC 是把多个不同的 crt 文件和我们自己写的代码链接起来,crt是 “C runtime” 的缩写,你可以仔细阅读了解一下每个 crt 是用来干嘛的,不过不用担心,我们目前只关注 crt0这个文件,它有个很重要的作用:

This object [crt0] is expected to contain the _start symbol, which takes care of bootstrapping the initial execution of the program.

目标对象 crt0 应该包含 _start 符号,该符号用于引导程序的初始执行。

执​​行的这种初始引导还是要取决于所使用的平台,但是通常它包括重要的任务,例如设置堆栈框架,传递命令行参数以及调用 main。是的,我们终于回答了本节开头的问题——_start 调用了我们的 main 函数!

找到我们的堆栈

终于解决了一个问题,但你可能更想知道这和我们最初的目标有什么关系,即能够逐步使用 GDB 来完成简单的 C 程序。在那之前,我们还需要解决另一些问题,首先要解决的问题是 crt0 设置堆栈的方式。

我们之前看到,gcc 链接了 crt0 文件,这个 crt0被选中,是根据如下几点做出的决策:

  • 目标平台,包括机器、供应商、操作系统,在本文中,指的是 riscv64-unknown-elf
  • 目标 ISA rv64imafdc
  • 目标 ABI lp64d

之前提到过,crt0 的一个工作是建立堆栈,但如果我们不知道 CPU 会把哪里当作堆栈,我们还能怎么办呢?确实,神仙来了也办不了,因此,我们需要更多的信息。

回到我们最初开始运行 qemu 的地方,qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out 可以看到我们使用了 virt 机器,可喜的是,qemu 把这个机器的 dump 信息全都给了我们,它放在了 dtb格式的文件中。

1
2
3
4
5
6
7
# In the `riscv-from-scratch/work` directory...

# Use qemu to dump info about the 'virt' machine in dtb (devicetree blob)
# format.
# The data in this file represents hardware components of a given
# machine / device / board.
qemu-system-riscv64 -machine virt -machine dumpdtb=riscv64-virt.dtb

然而 dtb 格式人类是无法轻易看懂的,但有一个工具 dtc (devicetree compiler) 可以转换成我们可读的内容。

1
2
3
4
5
# I'm running MacOS, so I use Homebrew to install this. If you're
# running another OS you may need to do something else.
brew install dtc
# Convert our .dtb into a human-readable .dts (devicetree source) file.
dtc -I dtb -O dts -o riscv64-virt.dts riscv64-virt.dtb

它生成了 riscv64-virt.dts 文件,里面包含了很多关于 virt 的信息,例如 CPU 核数量,外围设备(例如:UART 挖个小坑)的内存映射地址,以及 RAM 。我们想让我们的堆栈放在合适的位置,那么我们就找到它:

1
2
3
4
5
grep memory riscv64-virt.dts -A 3
memory@80000000 {
device_type = "memory";
reg = <0x00 0x80000000 0x00 0x8000000>;
};

可以看到 device_type 是 “memory” ,而其值,reg = <...>可以告诉我们想要的,比如内存从哪里开始,有多长。

参考the devicetree specification,我们看到 reg 的语法是任意数量的 (base_address,length) 对。但是,reg 内部有四个值——定义一个 memory 不应该只需要两个值吗?

再看一下the devicetree specification,我了解到,指定地址和长度所需的 <u32> 单元数由节点的父节点(或节点本身)中的 #address-cells#size-cells 属性确定。这些值未在我们的内存节点中指定,并且内存节点的父节点只在文件的根部分,让我们在其中查找以下值:

1
2
3
4
5
6
7
8
head -n8 riscv64-virt.dts
/dts-v1/;

/ {
#address-cells = <0x02>;
#size-cells = <0x02>;
compatible = "riscv-virtio";
model = "riscv-virtio,qemu";

它用了两个 32-bit 的值来确定一个地址,两个 32-bit 的值确定长度,这意思着, reg = <0x00 0x80000000 0x00 0x8000000> ,那么我们的内存起始于 0x00 + 0x80000000,并且长度为 0x00+0x8000000 字节,意味着它结束于 0x88000000,更简洁的说法是,始于 0x80000000的长度为128M 的内存。

链接起来

好,使用 qemudtc ,我们可以成功地找到 RAM 的位置、长度,我们也知道 GCC 会链接默认的 crt0 ,并建起一个不是我们想要的堆栈,那么基于这些信息,我们到底该怎么做,才能得到一个可以运行、调试的程序呢?

好吧,看来默认的 crt0 并没有完成我们想要的工作,因此我们必须编写自己的 crt0,然后将其编译,并与我们写的 C 程序链接。我们的 crt0 需要知道栈顶的起始位置,以进行初始化。虽然不是很推荐,但简便起见,我们在 crt0 中将此值硬编码为 0x80000000。这可能会引起不便,例如,当我们想使用具有不同内存属性的其他经过 qemu 化的 CPU(例如 sifive_e )时会发生什么?

好在这个问题还很遥远,且存在一个很好的解决方案。 GNU 的链接程序 ld 为我们提供了一种定义可以从 crt0 访问的符号的方法。除了 ld 提供的外,我们还可以使用它来创建 __stack_top 符号,它在多个不同的 CPU 之间具有相当的灵活性。

与其从头开始编写我们自己的链接器,不如将 ld 使用的默认链接器脚本稍加修改,增加我们想要的符号。你可能想知道什么是链接描述文件?此文总结甚好:

The main purpose of the linker script is to describe how the sections in the input files should be mapped into the output file, and to control the memory layout of the output file.

清楚了不?现在我们开始将默认的链接器脚本拷贝下来:

1
2
3
# In the `riscv-from-scratch/work` directory...
# Copy the default linker script into riscv64-virt.ld
riscv64-unknown-elf-ld --verbose > riscv64-virt.ld

文件里有很多有意思的信息,包括 ld 的版本号,可支持的架构等,当然这些东西的存在与否,完全不影响脚本的正常工作,可以将等于号之前的东西全部删掉的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vim riscv64-virt.ld

# Remove everything above and including the ============ line
GNU ld (GNU Binutils) 2.32
Supported emulations:
elf64lriscv
elf32lriscv
using internal linker script:
==================================================
/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2019 Free Software Foundation, Inc.
Copying and distribution of this script, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. */
OUTPUT_FORMAT("elf64-littleriscv", "elf64-littleriscv",
"elf64-littleriscv")
...rest of the linker script...

之后,我们要做的第一件事,就是用 MEMORY 命令告诉 ld 我们要手动控制内存布局。这为我们能够正确定义 __stack_top 的位置铺平了道路。然后,找到以 OUTPUT_ARCH (riscv) 开头的行,该行应位于文件顶部,并在其下面添加我们的 MEMORY 命令:

1
2
3
4
5
6
7
8
9
OUTPUT_ARCH(riscv)
/* >>> Our addition. <<< */
MEMORY
{
/* qemu-system-risc64 virt machine */
RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M
}
/* >>> End of our addition. <<< */
ENTRY(_start)

这样,我们就创建了一个叫 RAM 的 memory,权限是 rwx,可读可写可执行。

好的,这样一来,我们定义的内存布局就和 virt机器完全一致了。但除非我们接着做什么,否则空空一个 RAM 也完全没有用。我们要把自己的堆栈建在 RAM 里面,这就需要定义 __stack_top

这也很简单,打开 riscv64-virt.ld ,按照以下做即可:

1
2
3
4
5
6
7
8
9
10
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x10000));
. = SEGMENT_START("text-segment", 0x10000) + SIZEOF_HEADERS;
/* >>> Our addition. <<< */
PROVIDE(__stack_top = ORIGIN(RAM) + LENGTH(RAM));
/* >>> End of our addition. <<< */
.interp : { *(.interp) }
.note.gnu.build-id : { *(.note.gnu.build-id) }

可以看到,我们使用 PROVIDE 命令,定义了符号 __stack_top,它可被任何链接了该脚本的程序访问到,__stack_top的值是 ORIGIN(RAM) ,即 0x80000000 加上 0x8000000,其位置是 0x88000000

前方高能 运行时

终于,我们快要完成了。创建一个文件 crt0.s ,然后加入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.section .init, "ax"
.global _start
_start:
.cfi_startproc
.cfi_undefined ra
.option push
.option norelax
la gp, __global_pointer$
.option pop
la sp, __stack_top
add s0, sp, zero
jal zero, main
.cfi_endproc
.end

值得注意的是,有很多以 . 开头的行。这是一个汇编文件,这是因为,阅读文件的程序是汇编器,在GNU 中它是 as 文件。以 .s 开头的行是伪指令,伪指令向汇编程序提供信息,而不是像 RISC-V 汇编指令(例如 jaladd)那样成为可执行代码。

鉴于汇编语言不是很好读懂,我接下来会给大家一一讲解这程序在干嘛。

1
.section .init, "ax"

参考 GNU as manual 该行旨在告诉编译器接下来的代码会要进入名为 .init 的 section ,且权限是 allocatable and executable 。.init section 也是通常遵循的惯例 ,用于在操作系统内运行代码。可笑的是我们现在还没操作系统(那是因为我们正在写),关于 .init section ,这个解释更佳:

This section holds executable instructions that contribute to the process initialization code. That is, when a program starts to run the system arranges to execute the code in this section before the main program entry point (called main in C programs).

1
2
.global _start
_start:

.global 是必须的,这是要让 ld 能看见这个定义的符号,在链接时,ld 会根据链接器脚本ENTRY(_start) 寻找 _start ,找到程序开始执行的地方。

1
2
3
4
5
_start:
.cfi_startproc
.cfi_undefined ra
...other stuff...
.cfi_endproc

这些 .cfi 指令会把 frame 结构以及如何展开等信息通知给汇编器、异常展开器等工具。.cfi_startproc.cfi_endproc 指示了该函数的开始与结束。.cfi_undefined ra 告诉编译器寄存器 ra 不应当被恢复为以前的值 。因为 ra 内含的通常是返回地址,其值在第一个开始执行的函数 _start 前是不确定的。

1
2
3
4
.option push
.option norelax
la gp, __global_pointer$
.option pop

这些 .option 指令可内联汇编代码来修改汇编程序,这在必须使用一组特定的选项汇编指令序列时非常有用。该链接详细说明了为什么这对上面的代码段很重要,因此我将直接引用它(事实上原博主直接抄的那个手册:)):

…since we relax addressing sequences to shorter GP-relative sequences when possible, the initial load of GP must not be relaxed and should be emitted as something like:

1
2
3
4
.option push
.option norelax
la gp, __global_pointer$
.option pop

in order to produce, after linker relaxation, the expected:

1
2
auipc gp, %pcrel_hi(__global_pointer$)
addi gp, gp, %pcrel_lo(__global_pointer$)

instead of just:

1
addi gp, gp, 0

最后,看一下这部分代码:

1
2
3
4
5
6
7
_start:
...other stuff...
la sp, __stack_top
add s0, sp, zero
jal zero, main
.cfi_endproc
.end

这里我们终于用到了 __stack_top 符号,la 是 RISC-V 的伪汇编指令,意为 “load address” ,它获取 __stack_top 定义的地址数据,传递给 sp (stack pointer) 寄存器,这样一来后面的程序就可以使用这个栈了。

接下来,add s0, sp, zero 就是将 sp 的值加 0 后存入 s0s0 在某些方面是一个特殊的寄存器。首先,它是所谓的“保存寄存器”,这意味着它的值可以在函数调用之间保留。其次,s0 有时用作帧指针(frame pointer),这使每个函数调用都可以在堆栈上有自己的空间,用于存储传递该函数的参数。函数调用、堆栈指针和帧指针等是一个非常有趣的话题,但是目前,仅知道初始化帧指针 s0 是我们运行时的重要任务就可以了。

下一句 jal zero mainjal 是 “jump and link” 的缩写,其意思是无条件跳转到 main 符号点。由于 zero 的寄存器 x0 恒为0,因此该语句除了无条件跳转外无副作用。初次接触 RISC-V 的读者可能会觉得奇怪,为何使用 zero 寄存器作为目标寄存器,来实现一个无条件且无副作用的跳转。为什么要这样做呢……就不能额外加一个明确的无条件跳转指令?

实际上,这是一种巧妙的优化。每多支持一个的指令就意味着更大、更昂贵的处理器,因此 ISA 越简单越好。因而 RISC-V ISA 并不同时支持 jal 和无条件跳转指令,而是仅要求 jal,但通过 jal zero main 来支持无条件跳转。

RISC-V 中有许多类似的优化,其中大多数采用的是伪指令的形式。伪指令是汇编器知道如何转换为其他实际的硬件实现的指令的指令。例如,有一个无条件跳转伪指令 j offset_address,RISC-V 汇编程序将其转换为 jal zero,offset_address。有关正式支持的伪指令的完整列表,请在 RISC-V规范的v2.2查看。

1
2
3
4
5
_start:
...other stuff...
jal zero, main
.cfi_endproc
.end

最后一行,仍是一个汇编器指令,.end 指示了程序文件的结束。

真正开始调试

在开始前,回顾一下迄今我们做了什么,我们首先使用 qemudtc 找到了在 virt 虚拟机中的内存信息,然后使用这些信息,我们开始通过修改 riscv64-unknown-elf-ld 的链接器脚本,来”手动“控制内存的布局,最后,我们通过使用自定义的符号创建了一个自己的 crt0.S 文件,创建了栈和全局指针,并最后调用了 main 函数,好接下来,我们一鼓作气,开始真正的调试工作。

1
2
3
riscv64-unknown-elf-gcc -g -ffreestanding -O0 -Wl,--gc-sections \
-nostartfiles -nostdlib -nodefaultlibs -Wl,-T,riscv64-virt.ld \
crt0.s add.c

使用 gcc 编译、链接。不过这边突然多了一大堆的选项,令人头秃。

-ffreestanding 告诉编译器标准库可能不存在,因此不能做任何假设。在主机环境中运行应用程序时,此选项不是必需的,但是我们没有这样做,因为重要的是告诉编译器该信息。

-Wl 是逗号分隔的标志列表,以传递给链接器 ld--gc-sections 代表“垃圾收集 section”,告诉ld 在链接后删除未使用的节。 -nostartfiles-nostdlib-nodefaultlibs 分别告诉链接器不要链接任何标准系统启动文件(例如默认 crt0),任何标准系统 stdlib 实现或任何标准系统默认可链接库。我们提供了自己的 crt0 和链接描述文件,因此传递这些标志以告知编译器,我们不希望使用这些默认设置中的任何一个。

-T 允许你将你的链接器脚本路径传给链接器,在我们这次实验中就是 riscv64-virt.ld 。最后,加上我们想要编译的文件 crt0.sadd.c 。然后,我们得到了 a.out ,再使用 qemu 开启虚拟机:

1
2
3
# -S freezes execution of our executable (-kernel) until we explicitly tell 
# it to start with a 'continue' or 'c' from our gdb client
qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -S -kernel a.out

再另开一个终端,打开 gdb ,装载入 a.out 的符号表,并链接目标机器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
riscv64-unknown-elf-gdb --tui a.out

GNU gdb (GDB) 8.2.90.20190228-git
Copyright (C) 2019 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-apple-darwin17.7.0 --target=riscv64-unknown-elf".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://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"...
Reading symbols from a.out...
(gdb)
1
2
(gdb) target remote :1234                                                                             │
Remote debugging using :1234

设置断点并运行:

1
2
3
4
5
6
7
(gdb) b main
Breakpoint 1 at 0x8000001e: file add.c, line 2.

(gdb) c
Continuing.

Breakpoint 1, main () at add.c:2

啊哈,你会注意到程序真的在你的断点处停下来了,并且 GDB 内部还有很多相关的地址、数据信息,要是想查看寄存器值,使用命令 info all-registers 就可以了:

接下来

在我们的下一篇文章中,我们将通过在virt QEMU 机器上开始实现 UART 的驱动程序,继续在 RISC-V组装上积累知识。期望了解 UART 是什么以及它如何工作,其他设备树(device tree)属性,实现与NS16550A 兼容的 UART 驱动程序所需的基本构造块等。


RISC-V from Scratch 2
https://dingfen.github.io/2020/07/26/2020-7-26-riscv-from-scratch-2/
作者
Bill Ding
发布于
2020年7月26日
更新于
2024年4月9日
许可协议