Utils
内存布局
下面图“数据存储器”其实是 Data Memory.
Qemu 启动流程
- Step 1:
- Qemu CPU pc set to 0x1000,执行神秘指令,使得待会儿能跳转到 0x80000000:
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 # 这里 t0 值已经变成 0x80000000
0x1018: unimp
0x101a: 0x8000
0x101c: unimp
0x101e: unimp
- Step 2: 0x80000000
- 这里是 RUSTSBI,会执行很多指令,所以实操时在 0x80200000 打了个断点快速过去:
- SBI 做的事情有: 对部分硬件例如串口等进行初始化.
- 通过 mret 跳转到 payload 也就是 kernel 所在的起始地址。
- kernel 进行一系列的初始化后(内存管理,虚存管理,线程(进程)初始化等),通过 sret 跳转到应用程序的第一条指令开始执行。
- 这里是 RUSTSBI,会执行很多指令,所以实操时在 0x80200000 打了个断点快速过去:
0x0000000080000000 in ?? ()
(gdb) x/10i $pc
=> 0x80000000: auipc ra,0x2
0x80000004: jalr 834(ra)
0x80000008: auipc ra,0x0
0x8000000c: jalr 116(ra)
0x80000010: j 0x80001690
0x80000014: unimp
0x80000016: addi sp,sp,-80
0x80000018: sd ra,72(sp)
0x8000001a: ld a1,40(a0)
0x8000001c: ld a2,32(a0)
- Step 3: 0x80200000 自己的 Rust 程序:
(gdb) x/5i $pc
=> 0x80200000: li ra,100
0x80200004: unimp
0x80200006: unimp
0x80200008: unimp
0x8020000a: unimp
What is RustSBI?
[from comment] SBI 是 RISC-V Supervisor Binary Interface 规范的缩写,OpenSBI 是RISC-V官方用C语言开发的SBI参考实现;RustSBI 是用Rust语言实现的SBI。
BIOS 是 Basic Input/Output System,作用是引导计算机系统的启动以及硬件测试,并向OS提供硬件抽象层。
机器上电之后,会从ROM中读取引导代码,引导整个计算机软硬件系统的启动。而整个启动过程是分为多个阶段的,现行通用的多阶段引导模型为:
ROM -> LOADER -> RUNTIME -> BOOTLOADER -> OS
Loader 要干的事情,就是内存初始化,以及加载 Runtime 和 BootLoader 程序。而Loader自己也是一段程序,常见的Loader就包括 BIOS 和 UEFI,后者是前者的继任者。
Runtime 固件程序是为了提供运行时服务(runtime services),它是对硬件最基础的抽象,对OS提供服务,当我们要在同一套硬件系统中运行不同的操作系统,或者做硬件级别的虚拟化时,就离不开Runtime服务的支持。SBI就是RISC-V架构的Runtime规范。
BootLoader 要干的事情包括文件系统引导、网卡引导、操作系统启动配置项设置、操作系统加载等等。常见的 BootLoader 包括GRUB,U-Boot,LinuxBoot等。
而 BIOS/UEFI 的大多数实现,都是 Loader、Runtime、BootLoader 三合一的,所以不能粗暴的认为 SBI 跟 BIOS/UEFI 有直接的可比性。
如果把BIOS当做一个泛化的术语使用,而不是指某个具体实现的话,那么可以认为 SBI 是 BIOS 的组成部分之一。
练习
- 应用程序在执行过程中,会占用哪些计算机资源? > 占用 CPU 计算资源(CPU 流水线,缓存等),内存(内存不够还会占用外存)等
- 请用相关工具软件分析并给出应用程序A的代码段/数据段/堆/栈的地址空间范围。
readelf -all ch1-6_1 | nvim
# 输出
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000000002e0 000002e0
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 00000000000002fc 000002fc
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.bu[...] NOTE 000000000000031c 0000031c
0000000000000024 0000000000000000 A 0 0 4
...
# 解释
- 列解释:
- [Nr]: 节的编号。
- Name: 节的名称。
- Type: 节的类型。
- Address: 节在内存中的地址。
- Offset: 节在文件中的偏移量。
- Size: 节的大小。
- EntSize: 节中每个条目的大小(如果适用)。
- Flags: 节的标志,描述了节的属性。
- Link: 与该节相关的另一个节的编号。
- Info: 依赖于节类型的额外信息。
- Align: 节的对齐方式。
- 行解释:
- [ 0]: 这是 NULL 节,每个 ELF 文件都有一个 NULL 节,它不占用空间,主要用于对齐和占位。
- Type: NULL
- Address: 0x0000000000000000
- Offset: 0x00000000
- Size: 0x0000000000000000
- Flags: 0 (没有标志)
- Link: 0
- Info: 0
- Align: 0
- [ 1] .interp: 这个节包含程序解释器的路径名,通常是动态链接器。
- Type: PROGBITS
- Address: 0x00000000000002e0
- Offset: 0x000002e0
- Size: 0x000000000000001c (28字节)
- Flags: A (可分配的)
- Link: 0
- Info: 0
- Align: 1
还可以:cat /proc/[pid]/maps
,因为 应用程序的堆栈是由内核为其动态分配的,需要在运行时查看。
- ** 为何应用程序员编写应用时不需要建立栈空间和指定地址空间?
应用程度对内存的访问需要通过 MMU 的地址翻译完成,应用程序运行时看到的地址和实际位于内存中的地址是不同的,栈空间和地址空间需要内核进行管理和分配。应用程序的栈指针在 trap return 过程中初始化。此外,应用程序可能需要动态加载某些库的内容,也需要内核完成映射。