文章目录
前言
本文会对 x v 6 xv6 xv6 系统的启动流程做解析,先对 R I S C V RISCV RISCV 栈帧结构做解释,随后侧重于启动前中断委派,陷阱入口,时钟中断等的设置,对于虚拟内存,控制台 U A R T UART UART 等内容则只简单提及,后续再做详细解释。
R I S C V RISCV RISCV栈帧结构
下图为
R
I
S
C
V
RISCV
RISCV 调用约定:
其中,
s
0
/
f
p
s0/fp
s0/fp 为帧指针,
s
p
sp
sp 为堆栈指针。参数传递方式:从寄存器
a
0
−
a
7
a0-a7
a0−a7 为参数传递寄存器,超过8个参数,则通过堆栈传递。
打开反编译的文件,找到函数起始位置和结束位置,其格式一般为如下所示:
addi sp,sp,-48
sd ra,40(sp)
sd s0,32(sp)
sd s1,24(sp)
sd s2,16(sp)
sd s3,8(sp)
addi s0,sp,48
......
ld ra,40(sp)
ld s0,32(sp)
ld s1,24(sp)
ld s2,16(sp)
ld s3,8(sp)
addi sp,sp,48
ret
函数进入之后栈指针和帧指针变化如下图所示,旧
f
p
fp
fp 应当指向
r
e
t
u
r
n
a
d
d
r
e
s
s
return \ address
return address 的下面:
另外,
64
64
64 位下要求堆栈按照
16
16
16 字节对齐,所以每次
s
p
sp
sp 向上减小的长度都是
16
16
16 的倍数,所以可能会有空余空间。旧的
r
e
t
u
r
n
a
d
d
r
e
s
s
return \ address
return address 是在函数调用时通过
a
u
i
p
c
+
j
a
l
r
auipc+jalr
auipc+jalr 指令配合实现
c
a
l
l
call
call 指令的效果并把返回地址放入
r
a
ra
ra 寄存器之中,关于
a
u
i
p
c
auipc
auipc 和
j
a
l
r
jalr
jalr 配合实现
c
a
l
l
call
call 指令的效果具体见文末知识扩展部分做了解释。
函数结束,
s
p
sp
sp 和
f
p
fp
fp 寄存器以及需要
c
a
l
l
e
e
callee
callee 保存的寄存器
s
0
−
s
3
s0-s3
s0−s3 分别恢复旧值,根据
r
e
t
u
r
n
a
d
d
r
e
s
s
return \ address
return address 返回上一层调用栈。
打开一个本地变量较多的函数,使用 o b j d u m p objdump objdump 保留原始C语言代码,观察其反汇编之后函数入口及调用别的函数处格式为:
void
ls(char *path)
{
b4: d9010113 addi sp,sp,-624
b8: 26113423 sd ra,616(sp)
bc: 26813023 sd s0,608(sp)
c0: 24913c23 sd s1,600(sp)
c4: 25213823 sd s2,592(sp)
c8: 25313423 sd s3,584(sp)
cc: 25413023 sd s4,576(sp)
d0: 23513c23 sd s5,568(sp)
d4: 1c80 addi s0,sp,624
d6: 892a mv s2,a0
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
......
if(fstat(fd, &st) < 0){
e8: d9840593 addi a1,s0,-616
ec: 00000097 auipc ra,0x0
f0: 4aa080e7 jalr 1194(ra) # 596 <fstat>
f4: 08054163 bltz a0,176 <ls+0xc2>
......
函数本地变量较多,使得该函数在栈上使用了大量空间,大小为
624
624
624 。
另外,关于本地变量的存取可以看到都是通过帧指针
s
0
/
f
p
s0/fp
s0/fp 实现,下面
i
f
if
if 语句中函数调用时
f
d
fd
fd 参数已经位于
a
0
a0
a0 寄存器,
e
8
e8
e8 位置指令通过帧指针寄存器定位参数
s
t
st
st 的地址并放入
a
1
a1
a1 ,参数传递完成。而后通过
a
u
i
p
c
+
j
a
l
r
auipc+jalr
auipc+jalr 实现函数调用。
本地变量都位于帧指针上方的小地址位置。也就是当本地变量较多无法优化为有限的寄存器能够完成的计算时就需要栈来辅助。
综上,
R
I
S
C
V
RISCV
RISCV 函数调用栈帧结构如下图所示:
图中最上面为最新的当前函数栈帧,其堆栈的空间分布在数值上也最小。
e n t r y . S entry.S entry.S
文件 k e r n e l . l d kernel.ld kernel.ld 确保 _ e n t r y \_entry _entry 位于地址 0 x 80000000 0x80000000 0x80000000位置。这个地址是 q e m u qemu qemu 启动后执行内核代码的初始位置,就像 x 86 x86 x86 开机后 C S CS CS设置为 0 x F F F F 0xFFFF 0xFFFF, I P IP IP 设置为 0 x 0000 0x0000 0x0000 一样,该位置放置的代码也就是用户自定义内核的第一行代码或者说第一条指令。初始启动,系统出于 M a c h i n e − M o d e Machine-Mode Machine−Mode 之下。
文件 e n t r y . S entry.S entry.S 汇编语言代码如下所示:
.section .text
.global _entry
_entry:
# set up a stack for C.
# stack0 is declared in start.c,
# with a 4096-byte stack per CPU.
# sp = stack0 + (hartid * 4096)
la sp, stack0 # sp此时为stack0初始基地址
li a0, 1024*4 # a0此时为4096
csrr a1, mhartid # a1为当前CPU的id号
addi a1, a1, 1 # a1加1
mul a0, a0, a1 # id号加1再乘以4096
add sp, sp, a0 # sp原本为基地址,加偏移之后为堆栈指针初始值
# jump to start() in start.c
call start # 调用start函数
spin:
j spin
其中,
s
t
a
c
k
0
stack0
stack0 的定义位于文件kernel/start.c
中,该变量地址按照
16
16
16 字节对齐,原因在于
64
64
64 位下栈按照
16
16
16 字节对齐。__attribute__ ((aligned (16))) char stack0[4096 * NCPU];
该变量的作用就是为CPU的每一个核设置初始启动时的C语言栈帧空间。
由于堆栈指针SP需要指向栈的底部,以便向上增长,所以对于取出来的从0开始的核心号,需要加1处理之后,再乘以4096加到 s t a c k 0 stack0 stack0 的基地址之上作为 S P SP SP 的初始值。随后调用 s t a r t start start 函数。该函数位于文件 k e r n e l / s t a r t . c kernel/start.c kernel/start.c 中。如果调用失败或该函数返回,则一直执行死循环。
c a l l call call 指令调用 s t a r t start start 函数,此时 r a ra ra 寄存器中的内容为指令 j s p i n \textcolor{blue}{j \ spin} j spin 的地址。
以 C P U 0 CPU0 CPU0 栈帧的角度来看,此时栈帧还未形成,但是 S P SP SP 的值为 s t a c k 0 + 4096 stack0+4096 stack0+4096
s t a r t . c start.c start.c
该文件负责设置时钟中断,并设置内核陷阱入口,随后通过 m r e t mret mret 指令将内核从 M − M o d e M-Mode M−Mode 引入 S u p e r v i s o r − M o d e Supervisor-Mode Supervisor−Mode 并执行内核的 m a i n main main 函数,正式在 S − M o d e S-Mode S−Mode 下开始初始化页表,文件系统,用户第一个进程等内容。
s t a r t . c start.c start.c 文件内容如下所示:
void main();
void timerinit();
// 每个CPU的一个初始栈
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];
// 为每个CPU时钟中断准备的临时区域
// a scratch area per CPU for machine-mode timer interrupts.
uint64 timer_scratch[NCPU][5];
// M-Mode下时钟中断响应程序
extern void timervec();
// entry.S jumps here in machine mode on stack0.
void
start()
{
// 设置旧模式为Supervisor Mode,方便mret返回
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
// 为mret指令设置 M Exception Program Counter 寄存器值为 main函数地址
// requires gcc -mcmodel=medany
w_mepc((uint64)main);
// 禁用分页机制
w_satp(0);
// 把中断和异常委派到Supervisor Mode
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
// 物理内存保护机制Physical Memory Protection,见知识扩展部分介绍
// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);
// 时钟中断初始化
timerinit();
// CPU的hartid存入tp寄存器,方便S-Mode读取
int id = r_mhartid();
w_tp(id);
// mret返回并切换模式到Supervisor Mode,执行main.c:main函数
asm volatile("mret");
}
s t a r t start start 函数的核心功能是要初始化好时钟中断的响应工作,主要原因在于时钟中断和软件中断无法委派到 S − M o d e S-Mode S−Mode,只能在 M − M o d e M-Mode M−Mode 下处理好。
物理内存保护部分在文末知识扩展中会详细解释。简单理解为此处 x v 6 xv6 xv6 为了实现上简单易懂,把物理内存设置了读写执行的权限。
其次需要委派好中断和异常到 S − M o d e S-Mode S−Mode 并使 S − M o d e S-Mode S−Mode 中断和异常使能。构造好一个供 m r e t mret mret 返回的 S u p e r v i s o r − M o d e Supervisor-Mode Supervisor−Mode 去执行 k e r n e l / m a i n . c : m a i n kernel/main.c:main kernel/main.c:main 函数的 S − M o d e S-Mode S−Mode 现场。函数最后通过内嵌汇编 m r e t mret mret 返回,切换当前特权级到 S − M o d e S-Mode S−Mode ,执行 m a i n main main 函数。
m
r
e
t
mret
mret 指令执行时几步操作如下:
0:把
s
e
p
c
sepc
sepc 的内容放入
P
C
PC
PC ,其为
m
a
i
n
main
main 函数的地址。
1:切换特权级到
S
−
M
o
d
e
S-Mode
S−Mode ,这一步主要是把
m
s
t
a
t
u
s
mstatus
mstatus 寄存器中
M
P
P
MPP
MPP 位设置为当前模式。代码中已经设置了
m
s
t
a
t
u
s
mstatus
mstatus 寄存器
M
P
P
MPP
MPP 为
S
−
M
o
d
e
S-Mode
S−Mode 。
2:把
m
s
t
a
t
u
s
mstatus
mstatus 寄存器中
M
I
E
MIE
MIE 位设置为
M
P
I
E
MPIE
MPIE 。
M
P
P
MPP
MPP 置0,
M
P
I
E
MPIE
MPIE 置1。(
M
P
I
E
MPIE
MPIE 位用于在陷入
M
−
M
o
d
e
M-Mode
M−Mode 时记录进入之前的中断使能与否)
以
C
P
U
0
CPU0
CPU0 栈帧的角度来看,在
s
t
a
r
t
start
start 函数中栈帧一层,如下所示:
t i m e r i n i t timerinit timerinit函数
该函数主要用于时钟中断初始化工作,确保时钟中断在M-Mode
发生时能够正确处理。
函数内容如下所示:
void
timerinit()
{
// 每个CPU有一个单独的时钟中断源,CLINT单独控制
int id = r_mhartid();
// 设置CLINT,控制时钟中断发生频率
int interval = 1000000; // 设置时钟中断发生的间隔,以中断控制器的周期为单位,时间约为0.1s
*(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;
// 在scratch数组中设置时钟中断所需要的信息,把地址放入该CPU的mscratch寄存器中
// scratch[0..2] : 前三个位置再timervec时钟中断响应程序中用于作为辅助空间存储a1-a3
// scratch[3] : 记录当前CPU的MTIMECMP地址
// scratch[4] : 记录所需时钟中断之间的间隔
uint64 *scratch = &timer_scratch[id][0];
scratch[3] = CLINT_MTIMECMP(id); //第4个位置存储当前CPU所对应的CLINT中MTIMECMP寄存器映射地址
scratch[4] = interval; //第5个位置存储时钟中断之间间隔周期数
w_mscratch((uint64)scratch); //把scratch数组该CPU所对应的位置地址写入mscratch寄存器
// 设置M-Mode时钟中断处理函数位timervec
w_mtvec((uint64)timervec);
// M-Mode下中断使能
w_mstatus(r_mstatus() | MSTATUS_MIE);
// M-Mode下时钟中断使能
w_mie(r_mie() | MIE_MTIE);
}
M − M o d e M-Mode M−Mode 下也有类似 S − M o d e S-Mode S−Mode 下的 s c r a t c h scratch scratch 寄存器,暂存寄存器,但在特定模式下专用。 x v 6 xv6 xv6 在把 m s c r a t c h mscratch mscratch 寄存器用于存储和时钟中断配置相关的内容,在 M − M o d e M-Mode M−Mode 下时钟中断响应时方便找到位置并更新配置,响应中断。用法和 S − M o d e S-Mode S−Mode 中一样, s c r a t c h scratch scratch 寄存器和 a 0 a0 a0 交换,腾出空间,以便后续操作。
具体使用 m s c r a t c h mscratch mscratch 寄存器的位置在 M − M o d e M-Mode M−Mode 下时钟中断响应程序 t i m e r v e c timervec timervec 中。
t i m e r v e c timervec timervec函数
M − M o d e M-Mode M−Mode 时钟中断响应程序如下所示:
.globl timervec
.align 4
timervec:
# start中已经设置了M-Mode下寄存器mscratch寄存器指向当前CPU所属scratch内存地址
# scratch[0,8,16] : 为寄存器临时存放的保留地址
# scratch[24] : 当前CPU在CLINT映射空间中MTIMECMP 寄存器的地址.
# scratch[32] : 两次时钟中断之间的间隔数
# mscratch首先和a0交换,用scratch的前三个空间存储a1-a3寄存器返回时恢复
csrrw a0, mscratch, a0
sd a1, 0(a0) # 存储a1到scratch[0]
sd a2, 8(a0) # 存储a2到scratch[1]
sd a3, 16(a0) # 存储a3到scratch[2]
# 在mtimecmp中在原有数的基础上再加intervel以便触发下一次时钟中断
ld a1, 24(a0) # CLINT_MTIMECMP(hart)
ld a2, 32(a0) # interval
ld a3, 0(a1)
add a3, a3, a2
sd a3, 0(a1)
# arrange for a supervisor software interrupt
# after this handler returns.
li a1, 2
csrw sip, a1 # 设置S-Mode下sip.SSIP位,触发S-Mode下的软件中断
ld a3, 16(a0) # 恢复a3-a1
ld a2, 8(a0)
ld a1, 0(a0)
csrrw a0, mscratch, a0 # 交换回a0的值,中断返回
mret
由于 s t v e c stvec stvec 寄存器的 M o d e Mode Mode 域的存在,所以设置 s t v e c stvec stvec 基地址都需要为4的整数倍,故而有 . a l i g n 4 .align \ 4 .align 4 的限制。
m a i n . c main.c main.c
m r e t mret mret 返回 m a i n main main 函数执行之后, m a i n main main 函数启动剩余初始化工作,包括 U A R T UART UART ,页表和文件系统等等。具体源码如下,此时调用的各初始化函数不再详细展开:
volatile static int started = 0;
// start() jumps here in supervisor mode on all CPUs.
void
main()
{
if(cpuid() == 0){
consoleinit();
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator,初始化物理页分配器
kvminit(); // create kernel page table,初始化内核页表,映射内核栈
kvminithart(); // turn on paging,内核使用页表机制
procinit(); // process table,给每一个进程分配内核栈
trapinit(); // trap vectors,时钟中断锁
trapinithart(); // install kernel trap vector,设置内核中断向量
plicinit(); // set up interrupt controller,设置外中断VIRTIO0和UART0
plicinithart(); // ask PLIC for device interrupts
binit(); // buffer cache层
iinit(); // inode table层
fileinit(); // file table层
virtio_disk_init(); // emulated hard disk,虚拟磁盘初始化
userinit(); // first user process,初始化第一个用户程序,打开终端
__sync_synchronize();
started = 1;
} else {
while(started == 0)
;
__sync_synchronize();
printf("hart %d starting\n", cpuid());
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}
scheduler(); //开始调度
}
最后需要注意:每一个 C P U CPU CPU 最终都会有一个调度器 s c h e d u l e r scheduler scheduler 进程,他们不在 s c h e d u l e r scheduler scheduler 调度器调度的目标进程之中,他们使用的内核栈就是我们上文提及的 s t a c k 0 stack0 stack0 变量为每一个 C P U CPU CPU 核心提供的栈。这个进程就是每一个 C P U CPU CPU 核心都要执行一遍的从 e n t r y . S entry.S entry.S 到 s t a r t . c : s t a r t start.c:start start.c:start 再到 m a i n main main 函数而后转到调度器函数 s c h e d u l e r scheduler scheduler 函数的初始进程。这过程中用到的内核栈都是 s t a c k 0 stack0 stack0 为每个 C P U CPU CPU 单独提供的空间。
以
C
P
U
0
CPU0
CPU0 栈帧的角度来看,在
m
a
i
n
main
main 函数中栈帧两层,由于
m
r
e
t
mret
mret 指令不设置
r
a
ra
ra 寄存器,
r
a
ra
ra 寄存器为
s
t
a
r
t
start
start 函数在
m
r
e
t
mret
mret 之前最新的
r
a
ra
ra 值,由于
s
t
a
r
t
start
start 函数调用过
t
i
m
e
r
i
n
i
t
timerinit
timerinit 函数,故
r
a
ra
ra 寄存器最新的内容为
t
i
m
e
r
i
n
i
t
timerinit
timerinit 函数调用下面语句的地址。栈帧内容如下所示:
返回地址验证:修改
m
a
i
n
main
main 函数内容为一个
r
e
t
u
r
n
return
return 语句,以单
C
P
U
CPU
CPU 启动
q
e
m
u
−
g
d
b
qemu-gdb
qemu−gdb 并设置
m
a
i
n
main
main 函数断点,单步执行指令,观察返回位置。启动内核。结果如下所示:
结合
s
t
a
r
t
start
start 函数源码,
a
u
i
p
c
+
j
a
l
r
auipc+jalr
auipc+jalr 配合调用函数
t
i
m
e
r
i
n
i
t
timerinit
timerinit,后方指令为代码
int id = r_mhartid();
的汇编表示,结合后续两条指令我们知道是要给
t
p
tp
tp 寄存器赋当前
C
P
U
CPU
CPU 的
i
d
id
id 值。
验证完毕。读者可自行实验验证正确性。
知识扩展 a u i p c + j a l r auipc+jalr auipc+jalr
由于
A
d
d
r
e
s
s
l
a
y
o
u
t
r
a
n
d
o
m
i
z
a
t
i
o
n
(
A
S
L
R
)
Address \ layout \ randomization (ASLR)
Address layout randomization(ASLR) 地址空间随机的存在,系统每次加载可执行文件时该文件在内存中的位置都随机,这也是为什么链接时需要重定位的机制。同时,在编译文件为可重定位目标程序
(
.
o
/
.
o
b
j
)
(.o/.obj)
(.o/.obj) 文件时会使用对于文件起始的相对地址来方便重定位程序的操作。就像
R
I
S
C
V
RISCV
RISCV 标准中写的:The unconditional jump instructions all use PC-relative addressing to help support position-independent code.
无条件跳转指令都使用
p
c
pc
pc 相对寻址来帮助支持与位置无关的代码。
a
u
i
p
c
auipc
auipc 指令编码方式(指令长度为4):
指令作用:
A
U
I
P
C
AUIPC
AUIPC 指令用于建立
p
c
pc
pc 相对地址。
A
U
I
P
C
AUIPC
AUIPC 把无符号数
U
−
i
m
m
e
d
i
a
t
e
U -immediate
U−immediate 左移
12
12
12位形成一个
32
32
32位偏移量,用
0
0
0填充最低的
12
12
12位,符号扩展结果为
64
64
64位,将其与
A
U
I
P
C
AUIPC
AUIPC 指令的地址相加之后将结果放入寄存器
r
d
rd
rd 。
例如指令:
a
u
i
p
c
r
d
,
0
x
0
auipc \ rd,0x0
auipc rd,0x0 多用于获取当前
P
C
PC
PC 的值并存入寄存器
r
d
rd
rd 中。
j
a
l
r
jalr
jalr 指令编码方式(指令长度为4):
指令作用:实现间接跳转,跳转目标地址为
r
s
1
rs1
rs1 寄存器的值加12位立即数符号扩展到字长之后相加,跟随在
j
a
l
r
jalr
jalr 指令后的指令地址:
(
P
C
+
4
)
(PC+4)
(PC+4) 会被放入
r
d
rd
rd 寄存器中,当不指定
r
d
rd
rd 时默认为寄存器
x
1
(
r
a
r
e
t
u
r
n
a
d
d
r
e
s
s
)
x1(ra \ return \ address)
x1(ra return address) 。当不需要返回地址时,可以把
X
0
X0
X0 寄存器作为
r
d
rd
rd 寄存器使用。
例如指令:
j
a
l
r
x
0
,
0
(
x
1
)
jalr \ x0, 0(x1)
jalr x0,0(x1) 跳转到寄存器
x
1
x1
x1 的地址执行,不在意返回地址值的设置。
j
a
l
r
t
1
jalr \ t1
jalr t1 会扩展为
j
a
l
r
r
a
,
0
(
t
1
)
jalr \ ra, 0(t1)
jalr ra,0(t1)
c
a
l
l
call
call 指令多用于在一个程序中调用另一个程序,其一般格式为
c
a
l
l
o
f
f
s
e
t
call \ offset
call offset。
o
f
f
s
e
t
offset
offset 为待调用程序相对当前指令的偏移。
R
I
S
C
V
RISCV
RISCV 架构中编译器在遇到函数调用时会使用
a
u
i
p
c
+
j
a
l
r
auipc+jalr
auipc+jalr 指令代替
c
a
l
l
call
call 指令达到函数调用的效果。另外,
j
a
l
r
jalr
jalr 指令中
r
s
1
rs1
rs1 和
r
d
rd
rd 两个寄存器需要相同,一般都为
x
1
(
r
a
r
e
t
u
r
n
a
d
d
r
e
s
s
)
x1(ra \ return \ address)
x1(ra return address) ,这么做的好处在于可以实现
m
a
c
r
o
o
p
f
u
s
i
o
n
macro \ op \ fusion
macro op fusion,简单来说这是一种硬件机制,用于加速相邻两条指令的执行,具体机制请读者自行了解,(MOP Fusion)参考链接。
我们可打开使用 o b j d u m p objdump objdump 反编译后的文件,定位到函数调用部分,看到 a u i p c + j a l r auipc+jalr auipc+jalr 的配合调用。例如下文代码中调用 o p e n open open 系统调用和 c a t cat cat 自定义函数的部分。
28 if((fd = open(argv[i], 0)) < 0){
27 b4: 4581 li a1,0
26 b6: 00093503 ld a0,0(s2)
25 ba: 00000097 auipc ra,0x0
24 be: 310080e7 jalr 784(ra) # 3ca <open>
23 c2: 84aa mv s1,a0
22 c4: 02054d63 bltz a0,fe <main+0x74>
21 fprintf(2, "cat: cannot open %s\n", argv[i]);
20 exit(1);
19 }
18 cat(fd);
17 c8: 00000097 auipc ra,0x0
16 cc: f38080e7 jalr -200(ra) # 0 <cat>
假设待调用程序相对于 a u i p c auipc auipc 指令所在地址的偏移地址为 o f f s e t offset offset ,要配合实现 c a l l call call 指令的效果,编译器对于 j a l r jalr jalr 指令和 a u i p c auipc auipc 指令分别需要一个由 o f f s e t offset offset 计算而来的参数作为指令中的立即数部分。
以上文中函数调用 c a t ( f d ) cat(fd) cat(fd) 为例, c a t cat cat 函数位于文件开头,其指令地址在未重定向之前位于 0 0 0位置。 a u i p c auipc auipc 指令地址为 0 x c 8 0xc8 0xc8 十进制为 200 200 200 。结合下文 j a l r jalr jalr 指令,其跳转目标地址为 r a − 200 = 0 ra-200=0 ra−200=0。也就是 c a t cat cat 函数的地址。另外会把下条指令地址记录到返回地址寄存器,即 0 x c c + 4 − > r a 0xcc+4 \ ->ra 0xcc+4 −>ra 。
下面的C语言程序代表了编译器一种可能的计算两个指令所需要的立即数参数的计算过程。其中, j a l r jalr jalr 的参数和 a u i p c auipc auipc 的参数分别由程序中变量 j a l r _ a r g & 0 x f f f f f jalr\_arg \ \& \ 0xfffff jalr_arg & 0xfffff 和 a u i p c _ a r g & 0 x f f f auipc\_arg \ \& \ 0xfff auipc_arg & 0xfff 分别表示。
核心: o f f s e t offset offset 是待调用程序相对于指令 a u i p c auipc auipc 指令地址 P C PC PC 的偏移
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv){
if (argc != 2) return 1;
int offset = atoi(argv[1]);
int jalr_arg = (offset << 20) >> 20;
int auipc_arg = (offset - jalr_arg) >> 12;
printf("Offset = %d 0x%08x\n", offset, offset);
printf("auipc ra, %d # 0x%05x\n", auipc_arg, auipc_arg & 0xfffff);
printf("jalr ra, %d(ra) # 0x%03x\n", jalr_arg, jalr_arg & 0xfff);
return 0;
}
大致分两种情况:
当后12bit中最高有效位为0时,高20位数据右移12位作为
a
u
i
p
c
auipc
auipc 的参数,低12位符号扩展(形式和0扩展一样)作为
j
a
l
r
jalr
jalr 的参数。
当后12bit中最高有效位为1时,高20位数据右移12位并加1作为
a
u
i
p
c
auipc
auipc 的参数,低12位符号扩展为负数作为
j
a
l
r
jalr
jalr 的参数。
这里原理主要在于:
当后
12
b
i
t
12bit
12bit 中最高有效位为0时,
j
a
l
r
_
a
r
g
jalr\_arg
jalr_arg 在计算之后为低12bit表示的非负数。随后的减法使得低
12
b
i
t
12bit
12bit 直接置0。以
o
f
f
s
e
t
offset
offset 为
0
x
12345678
0x12345678
0x12345678 为例:
o
f
f
s
e
t
j
a
l
r
_
a
r
g
a
u
i
p
c
_
a
r
g
a
u
i
p
c
_
a
r
g
&
0
x
f
f
f
f
f
j
a
l
r
_
a
r
g
&
0
x
f
f
f
0
x
12345678
0
x
678
0
x
12345
0
x
12345
0
x
678
\def\arraystretch{1.5} \begin{array}{c:c:c:c:c} offset& jalr\_arg & auipc\_arg & auipc\_arg \& 0xfffff & jalr\_arg \& 0xfff\\ \hline 0x12345678 & 0x678 & 0x12345 & 0x12345 & 0x678\\ \end{array}
offset0x12345678jalr_arg0x678auipc_arg0x12345auipc_arg&0xfffff0x12345jalr_arg&0xfff0x678
当后
12
b
i
t
12bit
12bit 中最高有效位为
1
1
1 时,假设
j
a
l
r
_
a
r
g
jalr\_arg
jalr_arg所表示的数字值为
a
a
a 十六进制表示
0
x
f
f
f
f
f
x
y
z
0xfffffxyz
0xfffffxyz ,
−
a
-a
−a 的计算则需要对
0
x
f
f
f
f
f
x
y
z
0xfffffxyz
0xfffffxyz 取反加1。取反操作对于高位的
f
f
f 取反后变为全0,但低
12
b
i
t
12bit
12bit 与
o
f
f
s
e
t
offset
offset 的低
12
b
i
t
12bit
12bit 刚好相反,相加之后使得结果后
12
b
i
t
12bit
12bit 为全1。加1之后向第
12
b
i
t
12bit
12bit (从0开始)位进
1
1
1。以
o
f
f
s
e
t
offset
offset 为
0
x
12345876
0x12345876
0x12345876 为例:
o
f
f
s
e
t
j
a
l
r
_
a
r
g
o
f
f
s
e
t
−
j
a
l
r
_
a
r
g
a
u
i
p
c
_
a
r
g
a
u
i
p
c
_
a
r
g
&
0
x
f
f
f
f
f
j
a
l
r
_
a
r
g
&
0
x
f
f
f
0
x
12345876
0
x
f
f
f
f
f
876
0
x
12346000
0
x
12346
0
x
12346
0
x
876
\def\arraystretch{1.5} \begin{array}{c:c:c:c:c:c} offset& jalr\_arg & offset - jalr\_arg&auipc\_arg & auipc\_arg \& 0xfffff & jalr\_arg \& 0xfff\\ \hline 0x12345876 & 0xfffff876 &0x12346000 & 0x12346 & 0x12346 & 0x876\\ \end{array}
offset0x12345876jalr_arg0xfffff876offset−jalr_arg0x12346000auipc_arg0x12346auipc_arg&0xfffff0x12346jalr_arg&0xfff0x876
0
x
12345876
0x12345876
0x12345876 十进制值为
305420406
305420406
305420406 ,所以待跳转地址为
P
C
+
o
f
f
s
e
t
=
P
C
+
305420406
PC+offset=PC+305420406
PC+offset=PC+305420406 ,
0
x
876
0x876
0x876 按照
12
b
i
t
12bit
12bit 数字表示为负数,符号扩展为
32
b
i
t
32bit
32bit 表示为
0
x
f
f
f
f
f
876
0xfffff876
0xfffff876 其十进制值为
−
1930
-1930
−1930。
指令
a
u
i
p
c
r
a
,
0
x
12346
auipc \ ra,0x12346
auipc ra,0x12346 执行之后,
r
a
ra
ra 的值为
P
C
+
0
x
12346000
PC+0x12346000
PC+0x12346000 。
指令
j
a
l
r
r
a
,
−
1930
(
r
a
)
jalr \ ra, -1930(ra)
jalr ra,−1930(ra) 之后,跳转位置为:
P
C
+
0
x
12346000
−
1930
=
P
C
+
305420406
=
P
C
+
o
f
f
s
e
t
PC+0x12346000-1930=PC+305420406=PC+offset
PC+0x12346000−1930=PC+305420406=PC+offset
知识扩展 物理内存保护 P M P PMP PMP
R
I
S
C
V
RISCV
RISCV 提供物理内存保护机制,用于限制处于
S
−
M
o
d
e
S-Mode
S−Mode 和
U
−
M
o
d
e
U-Mode
U−Mode 下的物理内存访问必须符合访问该物理内存所必须具备的权限,否则就会引发异常并执行异常处理函数。
物理内存保护由一个
P
M
P
PMP
PMP 项来定义一个访问控制区域。一个
P
M
P
PMP
PMP 项包括一个 8 位控制寄存器和一个地址寄存器,仅可在
M
−
M
o
d
e
M-Mode
M−Mode 下访问,处理器支持最多 64 个
P
M
P
PMP
PMP 项,其中控制寄存器命名为
(
p
m
p
0
c
f
g
−
p
m
p
63
c
f
g
)
(pmp0cfg-pmp63cfg)
(pmp0cfg−pmp63cfg),和控制寄存器对应的地址寄存器命名为
(
p
m
p
a
d
d
r
0
−
p
m
p
a
d
d
r
63
)
(pmpaddr0-pmpaddr63)
(pmpaddr0−pmpaddr63)。
P
M
P
PMP
PMP 访问控制区域的大小是可设置的,最小可支持 4 字节大小的区域。
P M P PMP PMP控制寄存器和地址寄存器
一个
P
M
P
PMP
PMP 控制寄存器包含8位,共分为
L
、
A
、
X
、
W
、
R
L、A、X、W、R
L、A、X、W、R 五个字段。各字段区域如下所示:
W
W
W 控制该区域的写。
R
R
R 控制该区域的读。
X
X
X 控制该区域内容的执行。
A
A
A 字段控制区域计算规则。具体包括以下四种:
p
m
p
c
f
g
.
A
pmpcfg.A
pmpcfg.A 字段为0,则禁用物理内存保护。
p
m
p
c
f
g
.
A
pmpcfg.A
pmpcfg.A 字段为1,则表示和上一
P
M
P
PMP
PMP项的结束地址相连续。若当前控制寄存器为pmp1cfg,对应地址寄存器表示地址为PMP1ADDR,
p
m
p
0
c
f
g
pmp0cfg
pmp0cfg 对应地址寄存器表示地址为
P
M
P
0
A
D
D
R
PMP0ADDR
PMP0ADDR 时,那么当前区域地址范围是
[
P
M
P
0
A
D
D
R
,
P
M
P
1
A
D
D
R
)
[PMP0ADDR , PMP1ADDR)
[PMP0ADDR,PMP1ADDR)。特别的,当前
p
m
p
pmp
pmp控制寄存器为
p
m
p
0
c
f
g
pmp0cfg
pmp0cfg 时,范围表示为
[
0
,
P
M
P
0
A
D
D
R
)
[0 , PMP0ADDR)
[0,PMP0ADDR)
p
m
p
c
f
g
.
A
pmpcfg.A
pmpcfg.A 字段为2,表示地址按照4字节区域控制,每个区域大小为4字节。
p
m
p
c
f
g
.
A
pmpcfg.A
pmpcfg.A 字段为3,表示按照2的次幂大小字节数来控制区域。具体按照2的多少次幂作为区域大小需要根据控制寄存器对应的地址寄存器的内容来确定。规则如下表所示:
当
p
m
p
c
f
g
pmpcfg
pmpcfg 控制寄存器A字段为2即代表
N
A
4
NA4
NA4 时,区域大小为4字节。
当
p
m
p
c
f
g
pmpcfg
pmpcfg 控制寄存器A字段为3,对应地址寄存器最后一位为0,则按照8字节对齐。
当
p
m
p
c
f
g
pmpcfg
pmpcfg 控制寄存器A字段为3,对应地址寄存器为全1,则按照
2
X
L
E
N
+
3
2^{XLEN+3}
2XLEN+3 字节对齐。
32位下地址寄存器格式,一共32位,
R
V
32
RV32
RV32 支持
34
b
i
t
34bit
34bit 物理地址空间,地址寄存器存储34位地址中从第2到33位,从0-1两位不记录:
64位下地址寄存器格式,
R
V
64
RV64
RV64 支持
S
V
39
SV39
SV39,
S
V
48
SV48
SV48 等模式,共
56
b
i
t
56bit
56bit 物理地址空间,地址寄存器不记录0-1两位,记录2-55共54位,地址寄存器的高10位直接置零:
根据地址寄存器格式大家可以发现,在
R
I
S
C
V
RISCV
RISCV 内存保护机制中,地址默认按照
w
o
r
d
32
b
i
t
word \ 32bit
word 32bit 对齐。
p m p c f g . L pmpcfg.L pmpcfg.L 位用于设置 l o c k lock lock 是否打开。当 l o c k lock lock 打开时, M − M o d e M-Mode M−Mode 下的访问也会强制按照权限访问,权限不匹配则触发异常。 l o c k lock lock 关闭时, M − M o d e M-Mode M−Mode 下的访问都会成功。
控制寄存器的分组
为了方便上下文切换, R I S C V RISCV RISCV 把 p m p pmp pmp 控制寄存器放入 C S R CSR CSR 状态控制寄存器来设置。
32
b
i
t
32bit
32bit 模式下,
C
S
R
CSR
CSR状态控制寄存器长度为
32
b
i
t
32bit
32bit,可存储4个
p
m
p
pmp
pmp 控制寄存器。pmp控制寄存器的命名方式为
(
p
m
p
0
c
f
g
−
p
m
p
63
c
f
g
)
(pmp0cfg-pmp63cfg)
(pmp0cfg−pmp63cfg)。一个
C
S
R
CSR
CSR可存储4个
p
m
p
pmp
pmp控制寄存器,
R
I
S
C
V
RISCV
RISCV 规定最多支持64个pmp控制寄存器,那么在
32
b
i
t
32bit
32bit 下,
C
S
R
CSR
CSR 用于
p
m
p
pmp
pmp 控制的寄存器一共有16个,命名为
(
p
m
p
c
f
g
0
−
p
m
p
c
f
g
15
)
(pmpcfg0-pmpcfg15)
(pmpcfg0−pmpcfg15)。如下所示:
64
b
i
t
64bit
64bit 模式下,
C
S
R
CSR
CSR 状态控制寄存器长度为
64
b
i
t
64bit
64bit ,可存储8个
p
m
p
pmp
pmp 控制寄存器。
p
m
p
pmp
pmp 控制寄存器的命名方式为
(
p
m
p
0
c
f
g
−
p
m
p
63
c
f
g
)
(pmp0cfg-pmp63cfg)
(pmp0cfg−pmp63cfg) 。一个
C
S
R
CSR
CSR可存储8个
p
m
p
pmp
pmp控制寄存器,
R
I
S
C
V
RISCV
RISCV 规定最多支持64个
p
m
p
pmp
pmp 控制寄存器,在
64
b
i
t
64bit
64bit 下为了降低软件成本与
32
b
i
t
32bit
32bit 保持一致,
C
S
R
CSR
CSR 用于
p
m
p
pmp
pmp控制的寄存器一共有8个,命名为
(
p
m
p
c
f
g
0
−
p
m
p
c
f
g
14
)
(pmpcfg0-pmpcfg14)
(pmpcfg0−pmpcfg14) 但只采用偶数计数,没有单数表示。如下所示:
x v 6 xv6 xv6物理地址保护设置
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);
首先设置了
p
m
p
a
d
d
r
0
pmpaddr0
pmpaddr0 地址寄存器中内容为全1,同时设置状态控制寄存器
C
S
R
CSR
CSR
p
m
p
c
f
g
0
pmpcfg0
pmpcfg0 中的内容为
0
x
f
0xf
0xf,即设置
p
m
p
0
c
f
g
pmp0cfg
pmp0cfg 即与地址寄存器
p
m
p
a
d
d
r
0
pmpaddr0
pmpaddr0 相对应的控制寄存器内容为
0
x
0
f
0x0f
0x0f,设置读写执行并设置地址匹配模式为
T
O
R
TOR
TOR。那么此次设置受保护的物理内存地址范围为
[
0
,
0
x
3
f
f
f
f
f
f
f
f
f
f
f
f
f
00
)
[0, 0x3fffffffffffff00)
[0,0x3fffffffffffff00)。
同时设置的权限为可读可写可执行。
x
v
6
xv6
xv6 的设置方式非常直观,也没使用
N
A
P
O
T
NAPOT
NAPOT 模式等内容。
总结
简单记录了 R I S C V RISCV RISCV 的栈帧结构以及 x v 6 xv6 xv6 的启动前 M − M o d e M-Mode M−Mode 下的准备流程和跳转机制。最后部分研究了 a u i p c + j a l r auipc+jalr auipc+jalr 的实现 c a l l call call 指令效果机制以及 R I S C V RISCV RISCV 的物理内存保护机制。
参考
关于 R I S C V RISCV RISCV 中断的处理响应委派机制参考了大佬的博客,收益良多。博客详细地址
R
I
S
C
V
RISCV
RISCV 指令集手册 priv-isa-asciidoc.pdf