0%

riscv spl linux 加载流程

以 sifive fu740 为例

编译流程

编译opensbi

1
2
3
4
git clone https://github.com/riscv/opensbi.git
cd opensbi
make PLATFORM=generic
export OPENSBI=<path to opensbi/build/platform/generic/firmware/fw_dynamic.bin>

生成 fw_dynamic.bin
编译uboot 和 spl

1
2
3
cd <U-Boot-dir>
make sifive_unmatched_defconfig
make

生成 spl/u-boot-spl.binu-boot.itb 文件

烧写

1
2
3
4
5
6
sudo sgdisk -g --clear -a 1 \
--new=1:34:2081 --change-name=1:spl --typecode=1:5B193300-FC78-40CD-8002-E86C45580B47 \
--new=2:2082:10273 --change-name=2:uboot --typecode=2:2E54B353-1271-4842-806F-E436D6AF6985 \
--new=3:16384:282623 --change-name=3:boot --typecode=3:0x0700 \
--new=4:286720:13918207 --change-name=4:root --typecode=4:0x8300 \
/dev/sdX

sdX 表示通配, sd 卡插到电脑上, sd 卡的节点可能是 sdb sdc 等, 这里以 sdX 表示

1
2
3
4
5
6
7
8
9
10
sudo sgdisk -g --clear -a 1 \
--new=1:34:2081 --change-name=1:spl --typecode=1:5B193300-FC78-40CD-8002-E86C45580B47 \
--new=2:2082:10273 --change-name=2:uboot --typecode=2:2E54B353-1271-4842-806F-E436D6AF6985 \
--new=3:16384:282623 --change-name=3:boot --typecode=3:0x8300 \
/dev/nbd0
sudo dd if=<payload.bin> of=/dev/nbd0p2 seek=2082 "将u-boot.itb 拷贝到sd卡的第二个分区"
sudo mount /dev/nbd0p3 <mnt_dir>
cp Image dtb <mnt_dir>
sudo umount mnt_dir
sudo qemu-nbd --disconnect

上述步骤在sd卡上建了gpt 的四个分区, 分区的 start end sector offset 见命令, 同时指定了spl和u-boot.itb 所在分区的uuid

u-boot.itb 由 fw_dynamic.bin, u-boot-nodtb.binhifive-unmatched-a00.dtb 组成

1
2
3
4
5
6
sudo mkfs.vfat /dev/sdX3   "第三个分区格式化为 vfat 文件系统"
sudo mkfs.ext4 /dev/sdX4 "第四个分区格式化为 ext4 文件系统"
sudo mount /dev/sdX3 /media/sdX3 "第三个分区挂载到pc上"
sudo cp Image.gz hifive-unmatched-a00.dtb /media/sdX3/ "将kernel 镜像 dtb 文件拷贝到 sd卡的第三个分区下"
sudo dd if=spl/u-boot-spl.bin of=/dev/sdX seek=34 "将u-boot-spl.bin 拷贝到sd卡的第一个分区"
sudo dd if=u-boot.itb of=/dev/sdX seek=2082 "将u-boot.itb 拷贝到sd卡的第二个分区"

启动kernel

1
2
3
fatload mmc 0:3 ${kernel_addr_r} Image.gz                "kernel_addr_r 为 ddr上运行地址"
fatload mmc 0:3 ${fdt_addr_r} hifive-unmatched-a00.dtb "dtb 地址"
booti ${kernel_addr_r} - ${fdt_addr_r} "手动启动kernel"

spl代码分析

先分析入口汇编

spl 运行在M-mode下, 其bin文件被烧到了 sd卡的第一个分区中, 由bootrom 加载, u-boot-spl.bin 包含两个部分: u-boot-spl-nodtb.bin 和 u-boot-spl.dtb
u-boot-spl.dtb 中为spl的dtb 描述文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
+ _start:
\ - mv tp, a0 "tp register save hartid"
| - la t0, trap_entry
| - csrw MODE_PREFIX(tvec), t0 "设置中断mtvec 入口为 trap_entry"
| - csrw MODE_PREFIX(ie), zero
| - li t0, MIE_MSIE
| - csrs   MODE_PREFIX(ie), t0 "初始化msie"
| -+ call_board_init_f "Set sp in internal/ex RAM to call board_init_f"
\ - li t1, CONFIG_SPL_STACK "设置初始栈基址"
| - li t0, -16
| - and   sp, t1, t0 "将堆栈16 bits对齐"
| -+ call_board_init_f_0 "从堆栈开始的地方,为u-boot的global data(struct global_data)分配空间"
\ -- mv a0, sp
| -+ jal board_init_f_alloc_reserve
\ - top -= CONFIG_VAL(SYS_MALLOC_F_LEN) "如定义了CONFIG_SYS_MALLOC_F_LEN,需预留出early malloc所需的空间"
| - slli t0, tp, CONFIG_STACK_SIZE_SHIFT "tp代表的是hartid, 每个hart 分配STACK_SIZE 空间"
| - sub sp, a0, t0 "sp = a0 - t0, 地址在初始栈基址上向上增长, a0 初始栈基址 top - malloc空间"
| - bnez   tp, secondary_hart_loop "其他hart 跳转到 secondary_hart_loop, hart 0 继续"
| -+ jal board_init_f_init_reserve "初始化uboot的global data, 置0"
\ - "如定义了SYS_MALLOC_F_LEN,则会初始化gd->malloc_base"
| - SREG tp, GD_BOOT_HART(gp) "/* save the boot hart id to global_data */"
| -+ wait_for_gd_init
\ - jal    icache_enable
| - jal    dcache_enable
| - mv a0, zero
| -+ jalr board_init_f
\ -+ spl_early_init()
\ -+ spl_common_init(true);
\ - fdtdec_setup() "扫描spl的dtb, 创建fdt blob"
\ - dm_init_and_scan(!CONFIG_IS_ENABLED(OF_PLATDATA)); "扫描注册了哪些driver, 绑定device"
| -+ riscv_cpu_setup(NULL, NULL);
\ -+ riscv_cpu_probe();
\ -+ cpu_probe_all(); "触发riscv cpu 驱动bind"
\ - riscv_cpu_bind() "主要工作是 riscv timer 相关初始化"
| - riscv_cpu_ops = {.get_desc  = riscv_cpu_get_desc, .get_info  = riscv_cpu_get_info,
.get_count = riscv_cpu_get_count,} "注册 riscv_cpu_ops"
| - check supports_extension('d') supports_extension('f') "通过前面注册的 riscv_cpu_get_desc 查询"
| - csr_write(CSR_MCOUNTEREN, GENMASK(2, 0)); "Enable perf counters for cycle, time,"
| -+ preloader_console_init();
\ - serial_init(); "初始化uart"
| -+ spl_board_init_f();
\ -+ spl_soc_init();
\ -+ uclass_get_device(UCLASS_RAM, 0, &dev); "ddr init"
\ - uclass_find_device(id, index, &dev);
| -+ uclass_get_device_tail(dev, ret, devp);
\ -+ device_probe(dev);
\ -+ sifive_ddr_probe(struct udevice *dev) "ddr probe"
\ - clk_get_by_index(dev, 0, &priv->ddr_clk); "从fdt 中获取"
| - dev_read_u32(dev, "clock-frequency", &clock);
| - clk_enable(&priv->ddr_clk);
| -+ sifive_ddr_setup(dev);
\ - dev_read_u32_array(dev, "sifive,ddr-params", (u32 *)&plat->ddr_params,
sizeof(plat->ddr_params) / sizeof(u32));
| - ... "ddr 硬件寄存器设置参数"
| - spl_clear_bss
| - spl_clear_bss_loop "初始化bss"
| -+ spl_stack_gd_setup
\ - jal    spl_relocate_stack_gd "什么也没干"
| -+ beqz   a0, spl_call_board_init_r
\ -+ jal    board_init_r(a0=zero,a1=zero)
\ -+ spl_set_bd();
\ - gd->bd = &bdata;
| - mem_malloc_init(CONFIG_SYS_SPL_MALLOC_START =0x3fe430000, CONFIG_SYS_SPL_MALLOC_SIZE); "堆内存相关"
| - memset(&spl_image, '\0', sizeof(spl_image));
| -+ board_boot_order(spl_boot_list); "调用到board 相关的"
\ -+ spl_boot_list[0] = spl_boot_device();
\ - mode_select = readl((void *)MODE_SELECT_REG);
| - if MODE_SELECT_SD: BOOT_DEVICE_MMC1 "默认和SD_SELECT 时为 BOOT_DEVICE_MMC1"
| -+ boot_from_devices(&spl_image, spl_boot_list, ARRAY_SIZE(spl_boot_list)
\ -+ for (i = 0; i < count && spl_boot_list[i] != BOOT_DEVICE_NONE; i++)
\ -+ loader = spl_ll_find_loader(spl_boot_list[i])
"定位到 SPL_LOAD_IMAGE_METHOD("MMC1", 0, BOOT_DEVICE_MMC1, spl_mmc_load_image);"
\ -+ spl_image_loader *drv = ll_entry_start(struct spl_image_loader, spl_image_loader);
\ - .u_boot_list_2_spl_image_loader_1 "定位到 该地址, 从 map section 找到的, 其函数为 spl_mmc_load_image"
| -+ int n_ents = ll_entry_count(struct spl_image_loader, spl_image_loader);
"从 u_boot_list_2_spl_image_loader_1 遍历到 u_boot_list_2_spl_image_loader_3"
| -+ for (entry = drv; entry != drv + n_ents; entry++)
\ - find boot_device == entry->boot_device
| -+ spl_load_image(spl_image, loader)
\ - bootdev.boot_device = loader->boot_device; "boot_device 为 BOOT_DEVICE_MMC1"
| -+ loader->load_image(spl_image, &bootdev); "调用到 spl_mmc_load_image"
\ -+ spl_mmc_load(spl_image, bootdev, CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR = 0x822 = 2082)
"这个地方指到了 第二个分区的 sector 位置"
\ -+ spl_mmc_find_device(&mmc, bootdev->boot_device)
\ -+ mmc_init_device(0);
\ - uclass_get_device(UCLASS_MMC, num, &dev);
| - m = mmc_get_mmc_dev(dev); "mmc 驱动需要提前加载"
| - err = mmc_init(mmc); "mmc 初始化"
| -+ boot_mode = spl_mmc_boot_mode(bootdev->boot_device);
\ - return MMCSD_MODE_RAW
| - raw_sect = spl_mmc_get_uboot_raw_sector(mmc); "MMCSD_MODE_EMMCBOOT case 分支并没有break"
| -+ mmc_load_image_raw_sector(spl_image, mmc, raw_sect);
\ - blk_dread(bd, sector, 1, header); "从 mmc raw_sect 处 读入header"
| -+ spl_load_simple_fit(spl_image, &load, sector, header);
"镜像为FIT 格式时 header->ih_magic = FDT_MAGIC 的情况 , 加载到指定位置, 信息填充到 spl_image下"
\ -+ spl_simple_fit_read(&ctx, info, sector, fit) "uboot.itb 的 fit 描述信息"
\ - buf = board_spl_fit_buffer_addr(size, sectors, info->bl_len);
| - count = info->read(info, sector, sectors, buf); "从mmc 中读出 FIT blob"
| - ctx->fit = buf; "FIT blob 指针赋给ctx, ctx 后面使用会比较频繁"
| -+ node = spl_fit_get_image_node(&ctx, FIT_FIRMWARE_PROP, 0); "从uboot.itb 的FIT blob 获取firmware node"
\ -+ spl_fit_get_image_name(ctx, type, index, &str);
\ - name = fdt_getprop(ctx->fit, ctx->conf_node, type, &len);
"找 configurations node 下的firmware节点, 节点value 填充到str, 这里为 opensbi"
| - node = fdt_subnode_offset(ctx->fit, ctx->images_node, str);
"查image下的 name 为 opensbi 的节点"
| -+ spl_load_fit_image(info, sector, &ctx, node, spl_image);
"将opensbi node 节点下的信息读到 spl_image中"
"这里主要是填充load_addr, entry_point , 同时将数据下载到 load_addr 处"
\ - fit_image_get_data_offset(fit, node, &offset) "解析data-offset 节点, 获取bin offset"
| - fit_image_get_data_size(fit, node, &len) "解析data-size 节点, 获取opensbi bin的长度"
| - src_ptr = map_sysmem(ALIGN(load_addr, ARCH_DMA_MINALIGN), len); "分配内存, 这里直接指向load_addr处"
| - info->read(info, sector + get_aligned_image_offset(info, offset), nr_sectors, src_ptr)
"下载数据到src_ptr 处"
| - src = src_ptr + overhead;
| - fit_image_verify_with_data(fit, node, gd_fdt_blob(), src, length) "校验hash"
| - memcpy(load_ptr, src, length);
| - image_info->load_addr = load_addr; "填充spl_image"
| - fit_image_get_entry(fit, node, &entry_point) "查entry节点"
| - image_info->entry_point = entry_point; "填充spl_image"
| -+ node = spl_fit_get_image_node(&ctx, "loadables", index); "查loadables节点, 这个节点对应uboot"
| - spl_load_fit_image(info, sector, &ctx, node, &uboot_image_info); "下载uboot 节点的数据"
| -+ spl_fit_append_fdt(&uboot_image_info, info, sector, &ctx);
\ - node = spl_fit_get_image_node(ctx, FIT_FDT_PROP, index++); "下载fdt 节点数据"
| - image_info.load_addr = uboot_image_info->load_addr + uboot_image_info->size;
"fdt load_addr 追加到uboot镜像后面"
| - uboot_image_info->fdt_addr = map_sysmem(image_info.load_addr, 0);
| - spl_load_fit_image(info, sector, ctx, node, &image_info); "下载fdt 到 fdt_addr 处"
| - spl_image->fdt_addr = uboot_image_info.fdt_addr; "将fdt的load_addr 拷贝给 opensbi的spl_image 信息"
| - spl_image->boot_device = spl_boot_list[i];
| - switch (spl_image.os)
| -+ case IH_OS_OPENSBI:
\ -+ spl_invoke_opensbi(&spl_image); "跳到opensbi"
\ - check spl_image->fdt_addr valid
| - spl_opensbi_find_uboot_node(spl_image->fdt_addr, &uboot_node); "查fdt 属性 fit images 下是否有uboot的节点"
| - fit_image_get_entry(spl_image->fdt_addr, uboot_node, &uboot_entry);
"uboot 节点下的entry (运行地址)信息填给 uboot_entry"
| - opensbi_info.next_addr = uboot_entry;
| - opensbi_info.next_mode = FW_DYNAMIC_INFO_NEXT_MODE_S;
| - opensbi_info.boot_hart = gd->arch.boot_hart
| - opensbi_entry = (void (*)(ulong, ulong, ulong))spl_image->entry_point; "opensbi的entry 起始运行地址"
| - invalidate_icache_all(); "刷新icache"
| - opensbi_entry(gd->arch.boot_hart, (ulong)spl_image->fdt_addr, (ulong)&opensbi_info);
"跳到opensbi entry 运行起始地址, 携带了opensbi 下一跳的信息"

注意点:
spl 本身带有dtb, 这部分dtb 描述了spl 初始化外设的信息, 包括cpu ddr uart 等的参数
而uboot.itb 的dtb 会从mmc 存储中load 出来, 其描述信息会被解析用来确定firmware的信息.

加载的流程为:

  1. 定位到 (u-boot + opensbi + fdt) 镜像所在的分区, 注意这个地方现在是写的死的位置 2082 sector, 由 CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR 定义
  2. 读取镜像的header, 判断ih_magic 是否是FIT格式还是legacy 模式
  3. FIT 格式的镜像由spl_load_simple_fit 函数加载
  4. 下载(opensbi uboot fdt) bin 数据 到其节点描述的load位置, 填充image_info结构, 保存各级 entry (运行起始地址) load 信息
  5. 跳转到opensbi, 此处opensbi 为 fw_dynamic, 需要携带下一跳 即uboot 的start_entry 和 fdt的load地址信息.

整个加载的核心代码在 spl_load_simple_fit 函数中.
对于不同的存储设备类型, 由spl_boot_device() 定义, 本例中默认为 BOOT_DEVICE_MMC1,
还有以下这些供选择. 对应不同的类型由 SPL_LOAD_IMAGE_METHOD 方法注册对应的load_image 的 ops 函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum {
BOOT_DEVICE_RAM,
BOOT_DEVICE_MMC1,
BOOT_DEVICE_MMC2,
BOOT_DEVICE_MMC2_2,
BOOT_DEVICE_NAND,
BOOT_DEVICE_ONENAND,
BOOT_DEVICE_NOR,
BOOT_DEVICE_UART,
BOOT_DEVICE_SPI,
BOOT_DEVICE_USB,
BOOT_DEVICE_SATA,
BOOT_DEVICE_I2C,
BOOT_DEVICE_BOARD,
BOOT_DEVICE_DFU,
BOOT_DEVICE_XIP,
BOOT_DEVICE_BOOTROM,
BOOT_DEVICE_NONE
};

不同device 的load_image的ops 对于FIT 镜像都会调用到 spl_load_simple_fit 函数.

在介绍该函数前, 需要先了解 uboot的FIT格式镜像, FIT(flattened image tree), 它利用了Device Tree Source files(DTS)的语法,生成的image文件也和dtb文件类似(称作itb),下面我们会详细描述

uboot.itb 中会集成 opensbi uboot-nodtb 和 dtb 三部分镜像, 先看下描述文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
dtc u-boot.dtb
...
binman {
multiple-images;

itb {
filename = "u-boot.itb";

fit {
description = "Configuration to load OpenSBI before U-Boot";
#address-cells = <0x1>;
fit,fdt-list = "of-list";

images {

uboot {
description = "U-Boot";
type = "standalone";
os = "U-Boot";
arch = "riscv";
compression = "none";
load = <0x80200000>;

blob-ext {
filename = "u-boot-nodtb.bin";
};
};

opensbi {
description = "OpenSBI fw_dynamic Firmware";
type = "firmware";
os = "opensbi";
arch = "riscv";
compression = "none";
load = <0x80000000>;
entry = <0x80000000>;

opensbi {
filename = "fw_dynamic.bin"; // filename 不是真实节点名
};
};

@fdt-SEQ {
description = "NAME";
type = "flat_dt";
compression = "none";
};
};

configurations {
default = "conf-1";

@conf-SEQ {
description = "NAME";
firmware = "opensbi";
loadables = "uboot";
fdt = "fdt-SEQ";
};
};
};
};
};

注意, 上面的filename 不是真实节点名称, 它类似于incbin, 在mkimage 打包时会解析该prop, 将prop 描述的文件塞到 itb 镜像中, 并记录其在镜像中的offset 和 size, 记为data-offset(或data-position) 和 data-size

pc host 端使用 ./tools/mkimage -l ./u-boot.itb 可以打印itb 中分区的信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 ./tools/mkimage -l ./u-boot.itb
FIT description: Configuration to load OpenSBI before U-Boot
Created: Mon Nov 21 18:13:47 2022
Image 0 (uboot)
Description: U-Boot
Created: Mon Nov 21 18:13:47 2022
Type: Standalone Program
Compression: uncompressed
Data Size: 641008 Bytes = 625.98 KiB = 0.61 MiB
Architecture: RISC-V
Load Address: 0x80200000
Entry Point: unavailable
Image 1 (opensbi)
Description: OpenSBI fw_dynamic Firmware
Created: Mon Nov 21 18:13:47 2022
Type: Firmware
Compression: uncompressed
Data Size: 116016 Bytes = 113.30 KiB = 0.11 MiB
Architecture: RISC-V
OS: RISC-V OpenSBI
Load Address: 0x80000000
Image 2 (fdt-1)
Description: hifive-unmatched-a00
Created: Mon Nov 21 18:13:47 2022
Type: Flat Device Tree
Compression: uncompressed
Data Size: 22004 Bytes = 21.49 KiB = 0.02 MiB
Architecture: Unknown Architecture
Default Configuration: 'conf-1'
Configuration 0 (conf-1)
Description: hifive-unmatched-a00
Kernel: unavailable
Firmware: opensbi
FDT: fdt-1
Loadables: uboot

而u-boot 中可以使用iminfo itb 文件load到的地址 查看真实的镜像信息, 会包含 “data-offset” 和 “data-size” 节点

对于多核启动流程, 上述过程并未提及.
下面说下spl中其他核的处理过程:

从核启动过程

在上面的 spl_invoke_opensbi 函数中, 最后处理了其他核的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-+ ret = smp_call_function((ulong)spl_image->entry_point,
(ulong)spl_image->fdt_addr,
(ulong)&opensbi_info, wait=1);
\ - ipi_data ipi = { .addr = entry_point, .arg0 = fdt_addr, .arg1 = opensbi_info, };
| -+ send_ipi_many(&ipi, wait);
\ - cpus = ofnode_path("/cpus");
| -+ ofnode_for_each_subnode(node, cpus) "扫描spl的fdt"
\ - ofnode_read_u32(node, "reg", &reg); "read hart ID of CPU 放在reg中"
| - !gd->arch.available_harts & (1 << reg) | continue; "跳过not available的"
| - reg == gd->arch.boot_hart | continue; "跳过主核"
| - gd->arch.ipi[reg].addr = ipi->addr;
| - gd->arch.ipi[reg].arg0 = ipi->arg0;
| - gd->arch.ipi[reg].arg1 = ipi->arg1;
| - riscv_send_ipi(reg); "主核给其他核发ipi中断"
| -+ while pending
\ - wait | riscv_get_ipi(reg, &pending); "查是否pending, 如果从核没处理完该pending中断, 主核不会跳到下一阶段即 opensbi"

spl 中从核的初始化流程并没有设置 mstatus.mie mie, 虽然设置了mie.MSIE 和 mtvec trap entry, 但其中断并没打开.

虽然中断没打开, 但是可以被外部中断信号从wfi 处叫起来.

从核停在 secondary_hart_loop, 一直在loop, wfi停掉, 重点看下这个函数:

注意CONFIG_IS_ENABLED 宏, 与CONFIG_SPL_BUILD/ CONFIG_TPL_BUILD 联合使用, 这个BUILD宏是由Makefile spl传入的.

1
2
3
4
5
6
7
#if defined(CONFIG_TPL_BUILD)
#define _CONFIG_PREFIX TPL_
#elif defined(CONFIG_SPL_BUILD)
#define _CONFIG_PREFIX SPL_
#else
#define _CONFIG_PREFIX
#endif

if CONFIG_SPL_BUILD is undefined and CONFIG_FOO is set to ‘y’,
if CONFIG_SPL_BUILD is defined and CONFIG_SPL_FOO is set to ‘y’,
if CONFIG_TPL_BUILD is defined and CONFIG_TPL_FOO is set to ‘y’,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 0000000008000260 <secondary_hart_loop>:
8000260: 10500073 wfi
#if CONFIG_IS_ENABLED(SMP) "拆解为 CONFIG_SPL_SMP 是否定义了"
8000264: 344022f3 csrr t0,mip
8000268: 0082f293 andi t0,t0, MIE_MSIE //前面已经设置了 MIE_MSIE
800026c: fe028ae3 beqz t0,8000260 <secondary_hart_loop>
8000270: 8512 mv a0,tp
8000272: 3f4000ef jal ra,8000666 <handle_ipi> // 进入handle_ipi
#endif
8000276: b7ed j 8000260 <secondary_hart_loop>

void handle_ipi(ulong hart)
{
int ret;
void (*smp_function)(ulong hart, ulong arg0, ulong arg1);
// 检查gd->arch.ipi[hart].valid, 在主核没发起ipi 前, 这个地方是无效的, 会直接退出 handle_ipi 回到 secondary_hart_loop
if (!__smp_load_acquire(&gd->arch.ipi[hart].valid))
return;

smp_function = (void (*)(ulong, ulong, ulong))gd->arch.ipi[hart].addr;
invalidate_icache_all();

/*
* Clear the IPI to acknowledge the request before jumping to the
* requested function.
*/
ret = riscv_clear_ipi(hart);
if (ret) {
pr_err("Cannot clear IPI of hart %ld (error %d)\n", hart, ret);
return;
}
// 执行主核设置的参数, 这个地方跳转到 opensbi 入口地址, arg0为fdt_addr, arg1 为 opensbi_info
smp_function(hart, gd->arch.ipi[hart].arg0, gd->arch.ipi[hart].arg1);
}

在主核给从核发送ipi后, 从核从wfi中唤醒, 进入handle_ipi中, 跳入opensbi, 设置a0 寄存器为 uboot.itb 中带的fdt的下载到的地址fdt_addr, a1 寄存器为 opensbi_info, 携带了opensbi下一级的启动信息(uboot_entry majic version boot_hart等)

opensbi 启动流程

spl 加载uboot.itb, 将opensbi 镜像下载到 ddr 0x80000000 位置, uboot 下载到0x80200000 位置
然后启动dynamic 的opensbi, 这里不一定是dynamic的, 可以定制为payload或jump的.
下面以dynamic 为例分析 opensbi的启动流程.

首先看下opensbi的布局:

img

从其布局中需要重点了解 属于hartid的 scratch space, 这个空间在启动过程以及消息传递过程会频繁用到.
下面看下启动过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
_start:
/* Find preferred boot HART id */
MOV_3R s0, a0, s1, a1, s2, a2 "a0 为hartid"
call fw_boot_hart "从spl 传递的opensbi_info 中找boot_hart 给到 a0 -> a6, 如果opensbi不是fw_dynamic的, a0->a6 为 -1"
add a6, a0, zero
MOV_3R a0, s0, a1, s1, a2, s2
li a7, -1
beq a6, a7, _try_lottery "fw_jump 情况下, a0=a6=-1, 主核进入_try_lottery, 从核也会进_try_lottery"
/* Jump to relocation wait loop if we are not boot hart */
bne a0, a6, _wait_relocate_copy_done "fw_dynamic a0=a6=0, 主核进入 _try_lottery, 从核a0 > 0, 进入_wait_relocate_copy_done. "
_try_lottery:
/* Jump to relocation wait loop if we don't get relocation lottery */
lla a6, _relocate_lottery
li a7, 1
amoadd.w a6, a7, (a6) "主核先抢到lottery, 不会进入_wati_relocate_copy_done, 从核进入_wait_relocate_copy_done"
bnez a6, _wait_relocate_copy_done

/* Save load address */
lla t0, _load_start
lla t1, _fw_start
REG_S t1, 0(t0)

relocate:
...

_relocate_done:
lla    t0, _boot_status
    li  t1, BOOT_STATUS_RELOCATE_DONE
    REG_S   t1, 0(t0) "设置_boot_status = BOOT_STATUS_RELOCATE_DONE = 1"
 
_fdt_reloc_done:
/* mark boot hart done */
li t0, BOOT_STATUS_BOOT_HART_DONE
lla t1, _boot_status
REG_S t0, 0(t1) "设置_boot_status = BOOT_STATUS_BOOT_HART_DONE = 2"
fence rw, rw
j _start_warm "主核relocate完 设置BOOT_STATUS_BOOT_HART_DONE = 2 后进入 _start_warm"
   
_wait_relocate_copy_done:
lla t0, _fw_start
lla t1, _link_start
REG_L t1, 0(t1)
beq t0, t1, _wait_for_boot_hart "进_wait_for_boot_hart"
lla t2, _boot_status
lla t3, _wait_for_boot_hart
sub t3, t3, t0
add t3, t3, t1 "t3 = _wait_for_boot_hart - _fw_start + _link_start"
1:
/* waitting for relocate copy done (_boot_status == 1) */
li t4, BOOT_STATUS_RELOCATE_DONE
REG_L t5, 0(t2)
/* Reduce the bus traffic so that boot hart may proceed faster */
nop
nop
nop
bgt t4, t5, 1b "等 _boot_status == (BOOT_STATUS_RELOCATE_DONE = 1)"
jr t3 "跳到 _wait_for_boot_hart"

_wait_for_boot_hart:
li t0, BOOT_STATUS_BOOT_HART_DONE
lla t1, _boot_status
REG_L t1, 0(t1)
/* Reduce the bus traffic so that boot hart may proceed faster */
nop
nop
nop
bne t0, t1, _wait_for_boot_hart "等 _boot_status == (BOOT_STATUS_BOOT_HART_DONE = 2)"

_start_warm: "主从核在 BOOT_STATUS_BOOT_HART_DONE = 2 后进入 _start_warm"
lla    a4, platform
    lwu s7, SBI_PLATFORM_HART_COUNT_OFFSET(a4) "从platform 中获取hart数量, ->s7"
    lwu s8, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(a4) "获取stack_size, ->s8"
    csrr   s6, CSR_MHARTID "读取hartid ->s6"
    "Find the scratch space based on HART index"
    lla tp, _fw_end
mul a5, s7, s8
add tp, tp, a5 "_fw_end + hart_cout * hart_stack_size -> tp"
mul a5, s8, s6
sub tp, tp, a5 "_fw_end + (hart_cout - hartid) * hart_stack_size -> tp
"为 hartid 所在的hart_stack空间"
li a5, SBI_SCRATCH_SIZE
sub tp, tp, a5 "hartid hart_stack - SBI_SCRATCH_SIZE 为 hartid的scratch 所在地址"
    csrw   CSR_MSCRATCH, tp "将 hartid的scratch 所在地址 赋给 mscratch"
lla    a4, _trap_handler "设置M-mode trap 入口为 _trap_handler"
csrw   CSR_MTVEC, a4
csrr a0, CSR_MSCRATCH
call sbi_init "主从核调用 sbi_init(mscratch)"

platform 为平台定义, 定义了平台相关信息

1
2
3
4
5
6
7
8
9
10
11
12
struct sbi_platform platform = {
.opensbi_version = OPENSBI_VERSION,
.platform_version =
SBI_PLATFORM_VERSION(CONFIG_PLATFORM_GENERIC_MAJOR_VER,
CONFIG_PLATFORM_GENERIC_MINOR_VER),
.name = CONFIG_PLATFORM_GENERIC_NAME,
.features = SBI_PLATFORM_DEFAULT_FEATURES,
.hart_count = SBI_HARTMASK_MAX_BITS,
.hart_index2id = generic_hart_index2id,
.hart_stack_size = SBI_PLATFORM_DEFAULT_HART_STACK_SIZE,
.platform_ops_addr = (unsigned long)&platform_ops
};

主从核进入 sbi_init 的c_code 后续处理流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
-+ sbi_init(mscratch)  "mscratch 地址指向hartid 所在的 scratch space"
\ - u32 hartid         = current_hartid(); "csr_read(CSR_MHARTID)"
| - sbi_platform *plat = sbi_platform_ptr(scratch);
| -+ next_mode_supported && atomic_xchg(&coldboot_lottery, 1) == 0
"主核抢到 coldboot_lottery, 设置前为0, 设置后为1, 从核抢不到, coldboot = FALSE"
\ - coldboot = TRUE "主核 coldboot 为 TRUE"
| -|+ if coldboot
\ -+ init_coldboot(scratch, hartid); "主核进"
\ - sbi_platform *plat = sbi_platform_ptr(scratch);
| - sbi_scratch_init(scratch);
| - sbi_domain_init(scratch, hartid);
| - sbi_hsm_init(scratch, hartid, TRUE);
| - sbi_platform_early_init(plat, TRUE);
| - sbi_hart_init(scratch, TRUE);
| - sbi_console_init(scratch);
| - sbi_pmu_init(scratch, TRUE);
| - sbi_irqchip_init(scratch, TRUE);
| - sbi_ipi_init(scratch, TRUE);
| - sbi_tlb_init(scratch, TRUE);
| - sbi_timer_init(scratch, TRUE);
| - sbi_ecall_init();
| - sbi_platform_final_init(plat, TRUE);
| - wake_coldboot_harts(scratch, hartid); "Send an IPI to all HARTs waiting for coldboot"
| - sbi_hsm_prepare_next_jump(scratch, hartid);
| - sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr,
scratch->next_mode, FALSE); "主核进下一级 uboot, 跳入uboot_entry, 携带uboot.itb中的dtb下到的地址"
-|+ else !codeboot
\ -+ init_warmboot(scratch, hartid); "其他从核进"
\ - wait_for_coldboot(scratch, hartid); "等主核 wake_coldboot_harts"
| - hstate = sbi_hsm_hart_get_state(sbi_domain_thishart_ptr(), hartid);
"从 hartid_to_domain_table[__hartid] 中拿hart的初始hsm_state"
| -|+ if (hstate == SBI_HSM_STATE_SUSPENDED)
\ - init_warm_resume(scratch); "从核是suspend状态时, 即从核从挂起状态唤醒, 走resume"
| -|+ else != SBI_HSM_STATE_SUSPENDED "第一次起来时为 SBI_HSM_STATE_STOPED 状态"
\ -+ init_warm_startup(scratch, hartid); "从核第一次boot时, 走startup"
\ - sbi_platform *plat = sbi_platform_ptr(scratch);
| -+ sbi_hsm_init(scratch, hartid, FALSE);
\ -+ sbi_hsm_hart_wait(scratch, hartid);
\ - csr_set(CSR_MIE, MIP_MSIP | MIP_MEIP); "Set MSIE and MEIE bits to receive IPI"
\ -+ while (atomic_read(&hdata->state) != SBI_HSM_STATE_START_PENDING)
\ - wfi(); "从核的opensbi第一次boot会停在这里, 等待接收ipi, 但是ipi 处理函数并不会置 state,
只有ecall SBI_EXT_HSM_HART_START才会设置state 为 SBI_HSM_STATE_START_PENDING,并发起ipi"
| - sbi_platform_early_init(plat, FALSE);
| - sbi_hart_init(scratch, FALSE);
| - sbi_pmu_init(scratch, FALSE);
| - sbi_irqchip_init(scratch, FALSE);
| - sbi_ipi_init(scratch, FALSE);
| - sbi_tlb_init(scratch, FALSE);
| - sbi_timer_init(scratch, FALSE);
| - sbi_platform_final_init(plat, FALSE);
| - sbi_hsm_prepare_next_jump(scratch, hartid);
"检查hsm_state 是否为 SBI_HSM_STATE_START_PENDING,
如果是的话, 设置为 SBI_HSM_STATE_STARTED; 如果不是 将从核hang住"
| - sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr, scratch->next_mode, FALSE);
"从核跳下一级, 注意ecall SBI_EXT_HSM_HART_START 的消息会重新设置 scratch的 next_arg1, next_addr, next_mode"

主从核的boot 走了不同的分支, 主核走init_coldboot, 从核走init_warm_startup
只有主核才跳入了uboot, 从核停在了检查hsm_state 为 SBI_HSM_STATE_START_PENDING的地方.
只有主核发 ecall SBI_EXT_HSM_HART_START, 主核设置了从核hartid 所属的scratch space中的hsm_state为 SBI_HSM_STATE_START_PENDING , 再把从核叫起后, 从核才会接着往下走.
ecall SBI_EXT_HSM_HART_START 的消息会重新设置 scratch的 next_arg1, next_addr, next_mode, 对应kernel的情况, 将next_arg1 设置为了 kernel的封装的私有数据地址, next_addr 设置为 secondary_start_sbi 地址, next_mode 设置为S-mode.
scratch的设置最终会影响到 sbi_hart_switch_mode 函数跳入下一级.

从目前的逻辑看, 从核貌似并没有机会进uboot, uboot里面只是发了ipi中断, 并没有ecall SBI_EXT_HSM_HART_START 的过程.
从核会直接跳到kernel中. 从逻辑上看, 从核也没有必要进uboot, 因为uboot是S-mode的, 它只是一个中间态.

启动链

sifive的信息, opensbi 0x80000000 uboot 0x80200000 kernel 先load到 0x84000000, relocate 到 0x80200000 执行
kernel 先从物理地址上运行, 执行relocate_enable_mmu 后, 再从虚拟地址执行
kernel 的map信息记录的都是虚拟地址, 在enable_mmu 之前, 只能进行相对跳转.

kernel before mmu 加载流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__HEAD
ENTRY(_start)
/*
* Image header expected by Linux boot-loaders. The image header data
* structure is described in asm/image.h.
* Do not modify it without modifying the structure and all bootloaders
* that expects this header format!!
*/
/* jump to start kernel */
j _start_kernel
/* reserved */
.word 0
.balign 8
#if __riscv_xlen == 64
/* Image load offset(2MB) from start of RAM */
.dword 0x200000
#else
/* Image load offset(4MB) from start of RAM */
.dword 0x400000
#endif
/* Effective size of kernel image */
.dword _end - _start
.dword __HEAD_FLAGS
.word RISCV_HEADER_VERSION
.word 0
.dword 0
.ascii RISCV_IMAGE_MAGIC
.balign 4
.ascii RISCV_IMAGE_MAGIC2
.word 0

.head.text section 开头为构建的PE header,
我们可以找到RISC-V架构的启动协议Documentation/riscv/boot-image-header.txt。事实上这个文件只是简单介绍了一下Image文件header的结构,更详细的启动协议还是处于TODO状态,我们需要从代码进行分析。前面提到RISC-V内核比较类似于ARM64内核的格式,如下:

1
2
3
4
u32 code0;                /* Executable code */
u32 code1; /* Executable code */
u64 text_offset; /* Image load offset, little endian */
u64 image_size; /* Effective Image size, little endian */

从bootloader的角度,装载并执行Image类型的内核只需要做两件事情:

  • 将整个Image文件放置到内存起始处向后偏移text_offset的内存地址
  • 跳转到code0的地址进行执行

后续的事情,内核自理。我们看到head.S中有如下代码:
code0code1放置的就是j _start_kernel生成的指令。那么我们就需要从_start_kernel开始看起。

_start_kernel可以看到,一开始主要做了三件事:

  1. 关闭所有的中断
  2. 设置gp寄存器指向对应的地址(该寄存器为ABI相关,用于存放__global_pointer$的地址,即GOT的位置,可以参考RISCV调用协定
  3. 关闭FPU,内核中是不用任何浮点指令的

bootloader将控制权交给内核时,a0寄存器保存的值即为当前CPU执行单元的ID(RISC-V中称为HART ID)。
_start_kernel接下来就开始执行一个简单的Boot Protocol,选出一个用于启动内核的CPU,其他CPU进入等待状态。RISC-V处理器在reset之后,所有的处理单元(HART)都会一起开始执行,而Linux内核启动时为主从模型,因此需要挑选出其中一个完成部分内核启动工作,之后告知其他的处理器继续执行开始处理任务。首先确定CPU ID是否合法,即有没有超出内核编译时选择的最大支持CPU数,如果超过则非法:

1
2
3
4
5
6
#ifdef CONFIG_SMP
li t0, CONFIG_NR_CPUS
blt a0, t0, .Lgood_cores
tail .Lsecondary_park
.Lgood_cores:
#endif

.Lsecondary_park分支实质上是循环调用wfi指令。接下来内核使用一个简单的策略选出用于启动的主CPU:先到先得。setup.c中定义了一个原子变量:

1
atomic_t hart_lottery __section(.sdata);

所有CPU都会试图通过原子操作将这个变量加1。RISC-V的原子操作指令会将原子变量的原有值保存在原子变量的目标寄存器中,也就是说,如果操作后目标寄存器的值为0的CPU为第一对其进行操作的CPU。

1
2
3
4
5
/* Pick one hart to run the main boot sequence */
la a3, hart_lottery
li a2, 1
amoadd.w a3, a2, (a3)
bnez a3, .Lsecondary_start

对于所有竞争失败的CPU,我们在后面进行分析,目前还是顺着主CPU进行分析。随后主CPU的操作基本如下:

  • 清空bss段,写为0
  • 设置临时内核栈与task_struct
  • 依次调用setup_vm以及relocate_enable_mmu设置内核的虚拟内存
    调用完 relocate_enable_mmu 后, mmu 已经准备完, 此时已经为整个kernel 运行空间建立了完整的页表映射.
    _start_end 0xffffffff80000000 - 0xffffffff81345000

最后调用C语言通用代码启动内核:

1
2
3
4
5
6
la tp, init_task "init_task 初始的 task_struct, 即内核的第一个任务(`init/init_task.c`)"
la sp, init_thread_union + THREAD_SIZE
"init_thread_union是通过链接脚本留出的一个PAGE(riscv平台上为4KB)大小的区域,用以充当临时内核栈"
/* Start the kernel */
call soc_early_init
tail start_kernel "启动内核"

tp 寄存器的作用: Supervisor模式下的用途:保存当前CPU运行的进程上下文,即task_struct结构体。查看asm/current.h

1
2
3
4
5
create_pte_mappingstatic __always_inline struct task_struct *get_current(void)
{
register struct task_struct *tp __asm__("tp");
return tp;
}

setup_vm

将内核所在的物理地址映射到PAGE_OFFSET所在虚拟地址区域
(即将内核二进制所在的物理地址加上一个PAGE_OFFSET减去_start所的的偏移量)。因此,这里的任务实质上是建立一个临时页表,即内核临时的虚拟地址空间映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
mv s1, a1    "a1 寄存器是opensbi  传过来的fdt 的物理地址"
mv a0, s1
call setup_vm
-+ setup_vm(dtb_pa)
\ - kernel_map.virt_addr = KERNEL_LINK_ADDR ; "0xffffffff800000000"
| - kernel_map.page_offset = _AC(CONFIG_PAGE_OFFSET, UL); "0xff60000000000000"
| - kernel_map.phys_addr = (uintptr_t)(&_start); "0x80200000"
| - kernel_map.size = (uintptr_t)(&_end) - kernel_map.phys_addr; "kernel bin size"
| - BUG_ON((kernel_map.phys_addr % PMD_SIZE) != 0); "检查物理地址2M字节对齐"
| -+ set_satp_mode(); "检查是否支持4级页表"
| - kernel_map.va_pa_offset = PAGE_OFFSET - kernel_map.phys_addr; "0xff60000000000000 - 0x80200000"
| - kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr;
"0xffffffff800000000 - 0x80200000"
| - riscv_pfn_base = PFN_DOWN(kernel_map.phys_addr); "右移一个page 0x80200"
| -+ pt_ops_set_early();
\ - pt_ops.alloc_pte = alloc_pte_early; " allc_pte_early里面是BUG(),对于临时页表,kernel不允许我们建立PTE
临时页表, 只用到大页"
| - pt_ops.get_pte_virt = get_pte_virt_early;
| - create_pgd_mapping(early_pg_dir, FIXADDR_START, fixmap_p4d, PGDIR_SIZE, PAGE_TABLE);
"FIXADDR_START 0xff1bfffffee00000 - fixmap_p4d pa 映射, early_pg_dir为基址"
| - create_p4d_mapping(fixmap_p4d, FIXADDR_START, fixmap_pud, P4D_SIZE, PAGE_TABLE);
| - create_pud_mapping(fixmap_pud, FIXADDR_START, fixmap_pmd, PUD_SIZE, PAGE_TABLE);
| - create_pmd_mapping(fixmap_pmd, FIXADDR_START, fixmap_pte, PMD_SIZE, PAGE_TABLE);
"建立了0xff1bfffffee00000 开始的 2M的大页 FIXADDR_START - fixmap_pte 映射"
| - create_pgd_mapping(trampoline_pg_dir, kernel_map.virt_addr,
trampoline_p4d, PGDIR_SIZE, PAGE_TABLE);
| - create_p4d_mapping(trampoline_p4d, kernel_map.virt_addr,
trampoline_pud, P4D_SIZE, PAGE_TABLE);
| - create_pud_mapping(trampoline_pud, kernel_map.virt_addr,
trampoline_pmd, PUD_SIZE, PAGE_TABLE);
| - create_pmd_mapping(trampoline_pmd, kernel_map.virt_addr,
kernel_map.phys_addr, PMD_SIZE, PAGE_KERNEL_EXEC);
"建立了 trampoline_pg_dir 为pgd 的 指向 kernel_map.virt_addr (0xffffffff800000000) 开始的2M的大页
kernel_map.virt_addr - kernel_map.phys_addr 0xffffffff800000000 - 0x80200000 映射"
| -+ create_kernel_page_table(early_pg_dir, true);
"建立了以 early_pg_dir 为pgd的页表映射, 为整个kernel bin 建立连续页表映射 early_pg_dir 地址 0x80c08000
0xffffffff800000000 - 0x80200000 映射"
\ - end_va = kernel_map.virt_addr + kernel_map.size;
| -+ for (va = kernel_map.virt_addr; va < end_va; va += 2M)
\ - create_pgd_mapping(pgdir, va, "va 虚拟地址"
kernel_map.phys_addr + (va - kernel_map.virt_addr), "对应的物理地址"
PMD_SIZE, "2M"
early ? PAGE_KERNEL_EXEC : pgprot_from_va(va)); "PAGE_KERNEL_EXEC 表示大页, 一个大页是2M"
| - create_fdt_early_page_table(early_pg_dir, dtb_pa); "为dtb 创建页表"
| -+ pt_ops_set_fixmap()
\ - pt_ops.alloc_pte = kernel_mapping_pa_to_va((uintptr_t)alloc_pte_fixmap);
| - pt_ops.get_pte_virt = kernel_mapping_pa_to_va((uintptr_t)get_pte_virt_fixmap);

上述过程中可以了解到最简单的一种建立五级页表映射的代码: pgd 为 trampoline_pg_dir, 建立虚拟地址 kernel_map.virt_addr 同 物理地址 kernel_map.phys_addr 的映射关系, 翻译过程为 satp-> trampoline_pg_dir -> trampoline_p4d -> trampoline_pud -> trampoline_pmd

1
2
3
4
5
6
7
8
| -  create_pgd_mapping(trampoline_pg_dir, kernel_map.virt_addr, 
trampoline_p4d, PGDIR_SIZE, PAGE_TABLE);
| - create_p4d_mapping(trampoline_p4d, kernel_map.virt_addr,
trampoline_pud, P4D_SIZE, PAGE_TABLE);
| - create_pud_mapping(trampoline_pud, kernel_map.virt_addr,
trampoline_pmd, PUD_SIZE, PAGE_TABLE);
| - create_pmd_mapping(trampoline_pmd, kernel_map.virt_addr,
kernel_map.phys_addr, PMD_SIZE, PAGE_KERNEL_EXEC);

建立映射时传入了 PGDIR_SIZE P4D_SIZE PUD_SIZE PMD_SIZE 这几个size 导致 create_pgd_mapping create_p4d_mapping create_pud_mapping create_pmd_mapping 函数只填充了数据里的元素, 并没有走后面的流程.

以 create_pgd_mapping 为例, 其中 PAGE_TABLE 表示页目录, PAGE_KERNEL_EXEC 表示该级为叶子页表项

1
2
3
4
5
6
7
   uintptr_t pgd_idx = pgd_index(va);
if (sz == PGDIR_SIZE) {
if (pgd_val(pgdp[pgd_idx]) == 0)
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(pa), prot);
return;
}
...

sv48 和 sv57 对应 4level 和 5level mmu 映射:

线性地址 mmu 层级 Linux user address space Linux kernel address space
Sv48 (48bit) 4level: pgd→pud→pmd→pte→page(4k) 0x00000000 00000000 - 0x00007FFF FFFFFFFF 0xFFFF8000 00000000 - 0xFFFFFFFF FFFFFFFF
Sv57 (57bit) 5level: pgd→p4d→pud→pmd→pte→page(4k) 0x00000000 00000000 - 0x00FFFFFF FFFFFFFF 0xFF000000 00000000 - 0xFFFFFFFF FFFFFFFF

in sv57: PMD_SIZE = 1<<21 PUD_SIZE = 1<<30 P4D_SIZE = 1<<39 PGDIR_SIZE = 1<<48

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void __init create_pgd_mapping(pgd_t *pgdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
pgd_next_t *nextp;
phys_addr_t next_phys;
uintptr_t pgd_idx = pgd_index(va);

if (pgd_val(pgdp[pgd_idx]) == 0) {
next_phys = alloc_pgd_next(va); // 调用 pt_ops.alloc_p4d(__va), 返回 early_p4d
pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(next_phys), PAGE_TABLE);
nextp = get_pgd_next_virt(next_phys); // pt_ops.get_p4d_virt return early_p4d
memset(nextp, 0, PAGE_SIZE);
} else {
next_phys = PFN_PHYS(_pgd_pfn(pgdp[pgd_idx]));
nextp = get_pgd_next_virt(next_phys);
}

create_pgd_next_mapping(nextp, va, pa, sz, prot);
create_p4d_mapping(__nextp, __va, __pa, __sz, __prot)
create_pud_mapping(nextp, va, pa, sz, prot);
create_pmd_mapping(nextp, va, pa, sz, prot);
create_pte_mapping(ptep, va, pa, sz, prot);
}
}

static void __init create_pmd_mapping(pmd_t *pmdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
pte_t *ptep;
phys_addr_t pte_phys;
uintptr_t pmd_idx = pmd_index(va);

if (sz == PMD_SIZE) { //这个地方满足条件 sz=PMD_SIZE=2M
if (pmd_none(pmdp[pmd_idx]))
pmdp[pmd_idx] = pfn_pmd(PFN_DOWN(pa), prot); "赋值大页"
return;
}
...
}

对于sv57 模式, pgd -> p4d -> pud -> pmd -> pte

每一级页 page 4k, 每个页项 占8个字节, 所以每一级可以代表 4k/8 = 512个页项
一个pte 项可以索引 4k
对于pmd 可表示 4k * 512 = 2M 地址字节
对于pud 可表示 4k * 512 * 512 = 1G 地址字节

对于kernel 本身而言, 其 bin 文件大概在128M 之内, 对于bin对应的连续物理地址, pmd 前面的pgd p4d pud仅需要一个页项就可以能表示这些物理地址了, 所以前面建页表过程可以看到只有一个early_pg_dir early_p4d early_pud, 到pmd 这一级才会分多个 pmd 多个pte 出来.

sv57 in RV64:

  • satp.PPN 给出了一级页表的基址, VA[56:48]给出了一级页号, 因此处理器会读取位于地址(satp.PPN × 4096 + VA[56:48] × 8)的页表项 pgd
  • pgd.PPN 给出了二级页表的基址, VA[47:39]给出了二级页号, 因此处理器会读取位于地址(pgd.PPN × 4096 + VA[47:39] × 8)的页表项 p4d
  • p4d.PPN 给出了三级页表的基址, VA[38:30]给出了三级页号, 因此处理器会读取位于地址(p4d.PPN × 4096 + VA[38:30] × 8)的页表项 pud
  • pud.PPN 包含四级页表的基址, VA[29:21]给出了四级页号, 因此处理器读取位于地址(pud.PPN × 4096 + VA[29:21] × 8)的页表项 pmd
  • pmd.PPN 包含了五级页表的基址, VA[20:12] 给出了五级页号, 处理器读取位于地址(pmd.PPN × 4096 + VA[20:12] × 8)的页表项 pte
  • 该页表项的PTE. PPN 就是物理地址对应的PPN * 4096 + VA[offset 11:0]得到物理地址
    sv57 上. 虚拟地址范围 57 位, 0-56 位 , 物理地址范围 56位(44位的PPN + 12位的offset), 0-55 位

上述页表建立过程中用到了大页, 在pmd 一级中, 最后的prot 赋值为了 PAGE_KERNEL_EXEC,
其组合为 V | R | A | W | G | X | D

1
_PAGE_READ | _PAGE_WRITE | _PAGE_PRESENT | _PAGE_ACCESSED | _PAGE_DIRTY | _PAGE_GLOBAL | _PAGE_EXEC

PTE 描述: 对应(pgd/p4d/pud/pmd/pte)项
image-20240416113156489
启用大页时, 在页项 V=1, 且RWX 不为0时, 表示为叶页表项
对于pmd 一级大页, pmd.PPN 包含了五级页表的基址, 地址(pmd.PPN × 4096 + VA[20:0]) 即是 va 对应的 物理地址.
pmd 大页可索引2M的地址字节.

setup_vm()在最开始就进行了kernel入口地址的对齐检查,要求入口地址2M对齐。假设内存起始地址为0x80000000,那么kernel只能放在0x80000000、0x80200000等2M对齐处。为什么会有这种对齐要求呢?
应该是为给opensbi预留了2M空间,因为kernel之前还有opensbi,而opensbi运行完之后,默认跳转地址就是偏移2M,kernel只是为了跟opensbi对应,所以设置了2M对齐。
opensbi需要占用2M这么大?实际上只需要几百KB,因此opensbi和kernel中间有一段内存是空闲的,没有人使用。

上述setup_vm 操作最主要的是建立了kernel的临时页表, 页表遍历需要从early_pg_dir 的pgd 开始

relocate_enable_mmu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
la a0, early_pg_dir
relocate_enable_mmu:
la a1, kernel_map "a1 - 0xffffffff80da0198 - 0xffffffff800000000 + 0x80200000 = 0x80fa0198"
XIP_FIXUP_OFFSET a1
REG_L a1, KERNEL_MAP_VIRT_ADDR(a1) "load a1, "a1 变为 0xffffffff80000000""
la a2, _start "a2 - 0x80200000"
sub a1, a1, a2 "a1 = a1-a2, a1 为 offset 虚拟地址与物理地址 offset"
add ra, ra, a1 "ra = ra + offset, 这个函数返回时就会用虚拟地址了, 所以直接+上面的offset由物理地址变到物理地址"

/* Point stvec to virtual address of intruction after satp write */
la a2, 1f "a2 为 label 1 物理地址"
add a2, a2, a1 "a2 = a2 + offset"
csrw CSR_TVEC, a2 "a2 0xffffffff80001048 写入stvec"

/* Compute satp for kernel page tables, but don't load it yet */
srl a2, a0, PAGE_SHIFT "a2 = a0 >> PAGE_SHIFT 构造PPN, a0 为 early_pg_dir"
la a1, satp_mode "satp_mode 中保存分页模式 为sv39/sv48/sv57等"
REG_L a1, 0(a1) "取出satp_mode 中存的 分页模式"
or a2, a2, a1 "分页模式 | ASID(NULL) | PPN " "a2 MODE(0xa0) | ASID(0) | PPN(0x80c08)" 为sv57模式

/*
* Load trampoline page directory, which will cause us to trap to
* stvec if VA != PA, or simply fall through if VA == PA. We need a
* full fence here because setup_vm() just wrote these PTEs and we need
* to ensure the new translations are in use.
*/
la a0, trampoline_pg_dir "a0 为 trampoline_pg_dir 物理地址 0x814e9000"
XIP_FIXUP_OFFSET a0
srl a0, a0, PAGE_SHIFT "取出页号 PPN 为 0x814e9"
or a0, a0, a1 "trampoline_pg_dir satp 分页模式 | ASID(NULL) | PPN"
"a0 MODE(0xa0) | ASID(0) | PPN(0x814e9)"
sfence.vma
csrw CSR_SATP, a0 "启用trampoline_pg_dir pgd 页表, 这个只映射了 kernel bin的前2M 数据"
1:
/* Set trap vector to spin forever to help debug */
la a0, .Lsecondary_park
csrw CSR_TVEC, a0
/* Reload the global pointer */
.option push
.option norelax
la gp, __global_pointer$
.option pop

/*
* Switch to kernel page tables. A full fence is necessary in order to
* avoid using the trampoline translations, which are only correct for
* the first superpage. Fetching the fence is guaranteed to work
* because that first superpage is translated the same way.
*/
csrw CSR_SATP, a2 "启用 early_pg_dir pgd 页表, 映射了完整的kernel bin"
sfence.vma

ret

临时页表分析

MMU开启前,需要建立好kernel、dtb、trampoline等页表。以便MMU开启后,并且在内存管理模块运行之前,kernel可以正常初始化,dtb可以正常地被解析。这部分页表都是临时页表,最终的页表在setup_vm_final()建立。

临时页表创建顺序:

首先为fixmap创建早期的PGD、PMD,这时PGD使用early_pg_dir。然后对从kernel开始的前2M内存建立二级页表,此时PGD使用trampoline_pg_dir,为这2M建立的页表也叫作superpage。再然后,对整个kernel创建二级页表,此时PGD使用early_pg_dir。最后为dtb预留4M大小创建二级页表。

内存布局:

1
2
3
4
5
6
7
[    0.000000]       fixmap : 0xff1bfffffee00000 - 0xff1bffffff000000   (2048 kB)
[ 0.000000] pci io : 0xff1bffffff000000 - 0xff1c000000000000 ( 16 MB)
[ 0.000000] vmemmap : 0xff1c000000000000 - 0xff20000000000000 (1024 TB)
[ 0.000000] vmalloc : 0xff20000000000000 - 0xff60000000000000 (16384 TB)
[ 0.000000] lowmem : 0xff60000000000000 - 0xff6000007fe00000 (2046 MB)
[ 0.000000] modules : 0xffffffff01345000 - 0xffffffff80000000 (2028 MB)
[ 0.000000] kernel : 0xffffffff80000000 - 0xffffffffffffffff (2047 MB)

附录:
relocate_enable_mmu 汇编分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
0x80201000:  auipc   a1,0xd9f
0x80201004: addi a1,a1,408 "a1 0x80fa0198"
0x80201008: ld a1,8(a1) "a1 = 0xffffffff80000000"
0x8020100a: auipc a2,0xfffff
0x8020100e: addi a2,a2,-10 "a2 = 0x80200000"
0x80201012: sub a1,a1,a2 "a1 0xfffffffeffe00000 虚拟地址与物理地址的offset"
0x80201014: add ra,ra,a1 "ra 0xffffffff80001134"
0x80201016: auipc a2,0x0
0x8020101a: addi a2,a2,50
0x8020101e: add a2,a2,a1
0x80201020: csrw stvec,a2 "a2 0xffffffff80001048 写入stvec"
0x80201024: srli a2,a0,0xc "a0 0x80c08000 a2 0x80c08"
0x80201028: auipc a1,0xd9f
0x8020102c: addi a1,a1,432 "a1 0x80fa01d8 存satp_mode的地址"
0x80201030: ld a1,0(a1) "satp_mode 0xa000000000000000"
0x80201032: or a2,a2,a1 "a2 MODE(0xa0) | ASID(0) | PPN(0x80c08)"
0x80201034: auipc a0,0x12e8
0x80201038: addi a0,a0,-52 "a0 trampoline_pg_dir 0x814e9000 - 0x80200000 实际位置"
0x8020103c: srli a0,a0,0xc "a0 0x814e9"
0x8020103e: or a0,a0,a1 "a0 MODE(0xa0) | ASID(0) | PPN(0x814e9)"
0x80201040: sfence.vma
0x80201044: csrw satp,a0 "写页表 MODE(0xa0) | ASID(0) | PPN(0x814e9), MMU被启用,这使得访问原有物理地址时触发异常并跳转到中断向量 0xffffffff80001048"
0x80201048: auipc a0,0x0
0x8020104c: addi a0,a0,124
0x80201050: csrw stvec,a0
0x80201054: auipc gp,0x12de
0x80201058: addi gp,gp,324
0x8020105c: csrw satp,a2
0x80201060: sfence.vma
0x80201064: ret

kernel setup_arch

Linux内核的codebase可以简单分成两个部分:架构相关部分和架构无关部分。为了支持多个架构,且最大限度地公用代码,又保留架构相关实现的灵活性,Linux内核的实现经过了精细的设计。内核底层对一些架构相关的操作进行了抽象,向内核通用代码提供了公共的接口。每一个内核支持的架构都对应一个arch/下的文件夹,里面存放着本架构相关的代码。

内核实现多架构支持的手段:

  • 条件编译。这个方法主要用在一些极端特殊场合,多用于驱动对特定平台的区别操作。内核提供了一些宏,用于检测当前架构。
  • weak函数。这个方法利用了ELF object文件中的weak symbol,具有这个属性的symbol在链接时,如果链接器可以在所有进行链接的object文件中找到同名symbol,则会用这个symbolweak symbol顶替掉。内核使用__weak(本质就是一个GCC扩展)标记weak函数。内核可以对所有架构实现一个通用的weak函数,如果有架构需要一个自己的版本,则可以直接定义,并将其顶替。
  • 平台相关函数。这类函数为强平台相关,内核一般定义一个共同函数原型及函数语意,由各架构自行实现该函数。这里其实也包括一部分宏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
dtb_early_va = (void *)DTB_EARLY_BASE_VA + (dtb_pa & (PMD_SIZE - 1));
-+ setup_arch(char **cmdline_p)
\ -+ parse_dtb();
\ -+ early_init_dt_scan(dtb_early_va) "前面在创建临时页表时已经为 DTB_EARLY_BASE_VA - dtb_pa 建立了 4M (2个PMD_SIZE) 的大页映射"
\ - early_init_dt_verify(params); "检查设备树的合法性"
| -+ early_init_dt_scan_nodes()
\ - early_init_dt_scan_root(); "Initialize {size,address}-cells info"
| - early_init_dt_scan_chosen(boot_command_line);
"扫描设备树的`chosen`节点,获取内核命令行,initrd等关键信息,其中内核并命令行被保存在`boot_command_line`字符数组中"
| -+ early_init_dt_scan_memory();
"获取设备树中关于内存的描述,并调用`early_init_dt_add_memory_arch`"
\ -+ early_init_dt_add_memory_arch()
"使用了内核默认的实现,即将内核区域添加到`memblock`中。也就是说,RISC-V架构下的启动内存管理器是memblock,memblock的实现比较独立"
| - early_init_dt_check_for_usable_mem_range();
"Handle linux,usable-memory-range property"
| - parse_early_param(); "用于解析`early param`。内核中的`early param`都会特殊标记起来,保存在一个特殊的section里,
在内核启动初期从内核命令行解析出来。"
| -+ paging_init() "初始化页表"
\ -+ setup_bootmem(); "初始化memblock"
\ - phys_addr_t vmlinux_start = __pa_symbol(&_start);
"找到_start对应的物理地址, 这个地方因为已经启用了mmu, &_start相对寻址为虚拟地址, 需要将其转换为物理地址"
| - phys_addr_t vmlinux_end = __pa_symbol(&_end);
| - phys_ram_end = memblock_end_of_DRAM();
| - phys_ram_base = memblock_start_of_DRAM();
| - min_low_pfn = PFN_UP(phys_ram_base); "最小pfn 页号"
| - max_low_pfn = max_pfn = PFN_DOWN(phys_ram_end); "最大pfn页号"
| - high_memory = (void *)(__va(PFN_PHYS(max_low_pfn)));
| - dma32_phys_limit = min(4UL * SZ_1G, (unsigned long)PFN_PHYS(max_low_pfn));
| - memblock_reserve(dtb_early_pa, fdt_totalsize(dtb_early_va)); "保留early dtb 物理地址映射"
| - early_init_fdt_scan_reserved_mem();
"设备树中存在`reserved memory`的描述,通过该函数扫描fdt 中 reserved memory描述,然后将对应参数进行保留"
| - dma_contiguous_reserve(dma32_phys_limit);
| -+ setup_vm_final(); "将memblock中管理的内存添加到到`swapper_pg_dir`页表中,然后启用该页表"
\ - create_pgd_mapping(swapper_pg_dir, FIXADDR_START, __pa_symbol(fixmap_p4d), PGDIR_SIZE, PAGE_TABLE);
"swapper_pg_dir pgd 中建立 FIXADDR_START -> fixmap_p4d的页目录"
| -+ for_each_mem_range(i, &start, &end)
\ - map_size = best_map_size(start, end - start);
| -+ for (pa = start; pa < end; pa += map_size)
\ - create_pgd_mapping(swapper_pg_dir, va, pa, map_size, pgprot_from_va(va));
| - create_kernel_page_table(swapper_pg_dir, false); "第二次调用这个函数, 与第一次的区别early 为false, 这个地方的意思是又映射了一遍
只不过pgd 由early_pd_dir 变成了 swapper_pg_dir,
而early 为false, prot 由 PAGE_KERNEL_EXEC 变成pgprot_from_va(va),
由于此时的va还是查的early_pg_dir 的prot, 落到pmd 一级仍然是 PAGE_KERNEL_EXEC, 即这个地方还是大页"
| - csr_write(CSR_SATP, PFN_DOWN(__pa_symbol(swapper_pg_dir)) | satp_mode); "启用swapper_pg_dir 页表"
| - local_flush_tlb_all(); "清tlb"
| -+ pt_ops_set_late();
\ -+ pt_ops.alloc_pte = alloc_pte_late; "在 pt_ops_set_early 时不允许分配pte, 只能分配大页, 到这里允许分配pte了"
\ ---+ alloc_pte_late(va)
\ - vaddr = __get_free_page(GFP_KERNEL); "调用分配内存函数分配虚拟地址"
| - return __pa(vaddr); "返回pte的物理地址"
| - pt_ops.get_pmd_virt = get_pmd_virt_late;
| - pt_ops.alloc_pmd = alloc_pmd_late;
| - pt_ops.alloc_pud = alloc_pud_late;
| - pt_ops.alloc_p4d = alloc_p4d_late;
| - ...
| - unflatten_device_tree(); "该函数为OF模块的代码,目的是将设备树转换成更高效的内存中表示。"
| - sbi_init(); "sbi 探测版本 extension 支持feature, 设定sbi接口用来对接opensbi"
| -+ setup_smp(); "多核初始化, fdt cpu node"
\ ---+ cpu_ops[cpuid] = &cpu_ops_sbi; "注册cpu_ops"
\ ---+ kernel_init "主核初始化完后, 会起一个task kernel_init"
\ -+ kernel_init_freeable
\ -+ smp_init()
\ -+ bringup_nonboot_cpus(setup_max_cpus);
\ -+ for_each_present_cpu(cpu)
\ -+ cpu_up(cpu, CPUHP_ONLINE)
\ -+ cpuhp_up_callbacks
\ -+ cpuhp_invoke_callback_range
\ -+ cpuhp_invoke_callback
\ -+ bringup_cpu
\ -+ start_secondary_cpu(cpuid, task)
\ - boot_addr = __pa_symbol(secondary_start_sbi);
| - sbi_hsm_hart_start(hartid, boot_addr, hsm_data);
------------------------> other cpu <----------------------
\ - secondary_start_sbi
| - secondary_start_common
| -+ smp_callin
\ - notify_cpu_starting(curr_cpuid);
| - set_cpu_online(curr_cpuid, 1);
| - cpu_startup_entry(CPUHP_AP_ONLINE_IDLE); "idle 状态"
------------------------> other cpu <----------------------
| - riscv_fill_hwcap() "riscv hw 指令集探测 imafdc等"


前面看到内核在setup_vm中初始化了一个early_pg_dir页表,仅仅映射了内核所占用内存和一个FDT的fixmap,而paging_init中的setup_vm_final则是该操作的延续。首先明确使用两级初始化的原因:

  • 在没有读取设备树之前,内核是不知道物理内存的大小的。如果非要缩成一步,那么只能从内核所在内存结尾处开始,猜一个大小然后进行映射。这种实现有巨大的不确定性,并不是一个好的选择
  • 紧接上一条,这么做有可能需要映射一些不存在的内存区域,使得页表占用更多空间

所以setup_vm_final的操作本质上就是将memblock中管理的内存添加到到swapper_pg_dir页表中,然后启用该页表。

riscv smp 初始化boot

首先看下riscv kernel smp_call 从核的堆栈:

1
2
3
4
#0  sbi_hart_switch_mode (arg0=arg0@entry=0, arg1=4265325392, next_addr=2149584998, next_mode=1, next_virt=next_virt@entry=0) at /home/liguang/program/riscv-lab/opensbi/lib/sbi/sbi_hart.c:736
#1 0x000000008000087c in init_warmboot (hartid=<optimized out>, scratch=<optimized out>) at /home/liguang/program/riscv-lab/opensbi/lib/sbi/sbi_init.c:445
#2 sbi_init (scratch=0x80039000) at /home/liguang/program/riscv-lab/opensbi/lib/sbi/sbi_init.c:515
#3 0x00000000800003c4 in _start_warm () at /home/liguang/program/riscv-lab/opensbi/firmware/fw_base.S:501

从opensbi 开始说起:

其他cpu M-mode 停在了 opensbi的 sbi_hsm_hart_wait 处, 初始hsm_state 为 SBI_HSM_STATE_STOPPED

1
2
3
4
struct sbi_hsm_data *hdata = sbi_scratch_offset_ptr(scratch, hart_data_offset);
while (atomic_read(&hdata->state) != SBI_HSM_STATE_START_PENDING) {
wfi();
};

当主cpu 从kernel sbi_hsm_hart_start 发送 SBI_EXT_HSM_HART_START extension 后, 将hartid saddr=secondary_start_sbi 以及封装的priv 私有数据地址 发送给了opensbi.

主核的 opensbi 接收后将saddr 保存到了 opensbi 为hart 预留的scratch 空间的next_addr处, 将hsm_state变为 SBI_HSM_STATE_START_PENDING状态, 接着调用 ipi 给hartid 发送ipi中断.

1
2
3
4
5
6
rscratch = sbi_hartid_to_scratch(hartid);
rscratch->next_arg1 = priv;
rscratch->next_addr = saddr;
rscratch->next_mode = smode;
hdata = sbi_scratch_offset_ptr(rscratch, hart_data_offset);
hstate = atomic_cmpxchg(&hdata->state, SBI_HSM_STATE_STOPPED, SBI_HSM_STATE_START_PENDING);

hartid 的从核收到ipi 后, 设置了 MIP.SSIE位

1
2
3
4
static void sbi_ipi_process_smode(struct sbi_scratch *scratch)
{
csr_set(CSR_MIP, MIP_SSIP);
}

退出中断上下文, 返回到了 sbi_hsm_hart_wait继续处理, 此时hsm_state为 SBI_HSM_STATE_START_PENDING, 退出wait 循环
接着执行sbi_hsm_hart_wait 后面的函数, 最终执行到 sbi_hart_switch_mode

1
2
3
4
5
6
7
8
9
10
11
sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr,
scratch->next_mode, FALSE);
{
csr_write(CSR_STVEC, next_addr);
csr_write(CSR_SSCRATCH, 0);
csr_write(CSR_SIE, 0);
csr_write(CSR_SATP, 0);
register unsigned long a0 asm("a0") = arg0; "hartid"
register unsigned long a1 asm("a1") = arg1; "scratch->next_arg1 = priv, priv为kernel传来的私有数据地址"
__asm__ __volatile__("mret" : : "r"(a0), "r"(a1)); //跳到next_addr 即 secondary_start_sbi 执行
}