RISC-V Linux 启动流程分析

Author:  通天塔 985400330@qq.com
Date:    2022/05/15
Revisor: lzufalcon falcon@tinylab.org
Project: RISC-V Linux 内核剖析




c4940092eb67551ed5c65083aa8d6baf.jpeg

说明:RISC-V Linux 内核兴趣小组旨在围绕 RISC-V 处理器架构系统地研究 Linux 内核以及上下栈中的技术,为国内 RISC-V 生态做出泰晓科技 Linux 内核社区应有的贡献,活动已经持续了整整 3 个月,有数百人参与、已经产出了数十篇专题文章和十数次线上技术交流分享,目前还在火热地开展中。


欢迎在校学生和在职工程师报名参加,参与度和贡献度较高的同学都可以申请开源补贴。参与方式请查看:RISC-V Linux 内核剖析活动进入第 2 阶段并开放实习与兼职岗位

左下角阅读原文可访问外部链接的文章。

RISC-V Linux 启动流程分析

RISC-V Linux 目录分布

通过文章 将 Linux 移植到新的处理器架构,第 1 部分:基础 可知,我们进行一个新的处理器架构的移植,需要做到以下 3 点:

  1. 确定这是不是一个新的架构移植。

  2. 了解我们要移植的硬件。

  3. 了解内核的基本概念。

在 RISC-V 已经被移植支持的情况下,我们现在要做的是分析,Linux 内核是如何支持 RISC-V 架构的。

- configs/:支持系统的默认配置 (i.e. *_defconfig files)
- include/asm/ :Linux 源码内部使用的头文件
- include/uapi/asm: 对于要导出到用户空间(例如 libc )的头文件
- kernel/:通用内核管理
- lib/:优化过的那套函数 (e.g. memcpy(), memset(), etc.)
- mm/:内存管理
  • configs 文件中主要是一些配置文件,编译时可以选择默认配置进行编译,配置项较多,我们暂时不进行分析。

  • include/asm/ 目录下定义了大量头文件,用于内核编译时使用。

  • include/uapi/asm 目录下定义了很多结构体以及宏定义,可以供应用层使用,可以更方便的与内核统一使用一些定义好的数据。

  • kernel/ 目录下有许多 C 文件,包含 CPU 获取 id,信号,中断,ops,smp,time 等功能。

  • lib/ 目录下供 9 个文件,其中 5 个为汇编实现的代码。用于底层基础函数的实现。mm/ 目录下进行内存的管理,包括虚拟内存分配,页错误处理,cache 刷新等。

架构相关的 include 目录存在于架构相关文件夹,非架构相关的存在与 include/asm-gereric 目录下。

内核第一个运行的地方——head.S

kernel_entry*start_kernelsetup_arch*trap_init*mm_initmem_init*init_IRQ*time_init*rest_initkernel_threadkernel_threadcpu_startup_entry

内核的整体启动流程如上所示,我们从代码中进行分析,具体内核在启动过程中做了什么。

首先我们找到 head.S 文件。

ENTRY(_start_kernel)/* Mask all interrupts */csrw CSR_IE, zerocsrw CSR_IP, zero

在内核启动时,一开始就关闭了所有中断。Technical Report UCB/EECS-2016-129 一文中讲了,CSR 的寄存器分布。

关闭中断后,关闭了 FPU 功能,以检测内核空间内非法使用的定位点。后面是通过一系列的宏定义进行一些环境的配置,使得一些功能能够跑起来。

这些宏定义有:

ENTRY(_start_kernel)关闭所有中断
#ifdef CONFIG_RISCV_M_MODE/* 刷新icache *//* 复位所有寄存器,除了 ra, a0, a1 *//*设置一个 PMP 以允许访问所有内存。有些机器可能不会实现 pmp,因此我们设置了一个快速陷阱处理程序来跳过接触任何陷阱上的 pmp。*//*a0 中的 hardtid 稍后才会出现,我们没有固件可以处理它。*/
#endif /* CONFIG_RISCV_M_MODE *//* 加载全局指针 *//**关闭 FPU,检测内核空间中非法使用浮点数的情况*/
#ifdef CONFIG_RISCV_BOOT_SPINWAIT/* 彩票系统只需要自旋等待启动方法 */
#ifndef CONFIG_XIP_KERNEL/* 选择一个 hart 来运行主启动序列 */
#else/* Hart_lottery 在 flash 中包含一个神奇的数字 *//* 如果在 RAM 中没有设置 hart_lottery,这是第一次 */
#endif /* CONFIG_XIP */
#endif /* CONFIG_RISCV_BOOT_SPINWAIT */
#ifdef CONFIG_XIP_KERNEL
/*恢复 a- 的复制*/
#endif
#ifndef CONFIG_XIP_KERNEL/*为展开的无 ELF 的镜像清除 BSS 段 */
#endif/* 保存 hart ID 和 DTB 物理地址*//* 初始化页表并重新定位到虚拟地址 */
#ifdef CONFIG_BUILTIN_DTB
#else
#endif /* CONFIG_BUILTIN_DTB */
#ifdef CONFIG_MMU
#endif /* CONFIG_MMU */* Restore C environment */
#ifdef CONFIG_KASAN
#endif/* 启动内核 */
#if CONFIG_RISCV_BOOT_SPINWAIT/* 设置陷阱向量永远旋转以帮助调试 *//*
这个人没有中彩票,所以我们等待中奖的人在启动过程中走得足够远,它应该继续。*//* FIXME: 我们应该 WFI,以节省一些能源在这里。*/
#endif /* CONFIG_RISCV_BOOT_SPINWAIT */
END(_start_kernel)

内核运行的第一个 C 文件——init/main.c

第一个运行的 C 语言函数为 start_kernel,在该函数中进行内核的第一个线程的创建。在创建之前,会执行架构相关的函数,从而适配硬件。

kernel_entry*start_kernelsetup_arch*trap_init*mm_initmem_init*init_IRQ*time_init*rest_initkernel_threadkernel_threadcpu_startup_entry

setup_arch()

首先分析 setup_arch 这个函数,该函数属于架构相关函数,对应的文件在 arch/riscv/kernel 文件下。

parse_dtb()

这个函数首先要执行的是解析设备树,这说明 RISC-V 像 arm 一样,使用设备树进行设备驱动的管理,我们查看 x86 架构下的 setup_arch 则无设备树相关的配置。设备树解析函数通过 drivers/of 目录下的设备树驱动进行解析,并取出设备树中 model 名称。

设备树解析调用的函数是 parse_dtb,函数中调用了一个全局变量 dtb_early_va,这个变量是在 head.S 中进行的赋值,head.S 中调用该函数时,提前将变量放置于寄存器 a0 中,用于 C 函数的传参。

设备树地址传参代码:

#ifdef CONFIG_BUILTIN_DTBla a0, __dtb_startXIP_FIXUP_OFFSET a0
#elsemv a0, s1
#endif /* CONFIG_BUILTIN_DTB */call setup_vm

setup_initial_init_mm()

设备树解析完成后,进行了早期内存的初始化,给出了代码段的起始与结束位置,数据段的结束位置,堆地址结束位置。

[0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80200000
[0.000000] Machine model: riscv-virtio,qemu
[0.000000]start_code=0x80002000,end_code=0x806ae52c,end_data=0x812d2a00,brk=0x81322000

通过以上打印信息可知各个段的分配地址。CPU 内部的 RAM 寻址需要预留一些空间,所有 ram 起始地址就从 0x80000000 开始,地址空间分配完成之后将 boot_command_line 地址传出,供后续使用。

early_ioremap_setup()

早期 ioremap 初始化,将 I/O 的物理地址映射到虚拟地址。当 CPU 读取一段物理地址时,它可以读取到映射了 I/O 设备的物理 RAM 区域。ioremap 就是用来把设备内存映射到内核地址空间的。

该函数是一个架构不相关的函数,位于 mm/early_ioremap.c

jump_label_init()

架构无关函数,位于 kernel 目录下,初始化 jump-label 子系统,jump-label 用于取消 if 判断分支,通过运行时修改代码,来提高执行的效率。

大家可以阅读这个系列连载的文章:RISC-V Linux jump_label 详解,第 1 部分:技术背景

parse_early_param()

架构无关函数,解析早期传入的参数。

efi_init()

暂未分析,应该和 UEFI 有关。大家可以看一下这个系列的文章:RISC-V UEFI 架构支持详解,第 1 部分 - OpenSBI/U-Boot/UEFI 简介

paging_init()

完成系统分页机制的初始化工作,建立页表,从而内核可以完成虚拟内存的映射和转换工作,这一个函数执行完成之后,就可以通过虚拟地址来访问实际的物理地址了。

misc_mem_init()

该函数主要工作如下:

  • 测试 ram 是否正常

  • numa 架构初始化

  • 内存模型 sparse 初始化

  • 初始化 zone,用于管理物理内存地址区域

  • 保留内核崩溃时内核信息导出时所用的内存区域

  • 打印内存分配情况 __memblock_dump_all(),实际未输出

init_resources()

初始化内存资源,把系统的 ram 以及其他需要保留的 ram 进行保留

sbi_init()

可能与 sbi 有关,大家可以看一下这个系列的文章:RISC-V OpenSBI 快速上手

函数相关打印如下,具体作用暂未分析:

[    0.000000] SBI specification v0.2 detected
[    0.000000] SBI implementation ID=0x1 Version=0x9
[    0.000000] SBI TIME extension detected
[    0.000000] SBI IPI extension detected
[    0.000000] SBI RFENCE extension detected
[    0.000000] SBI HSM extension detected

kasan_init()

初始化 kasan 动态监测内存错误的工具,初始化完成之后,可以在内存使用越界或者释放后访问时,产生出错报告,帮助分析内核异常。

setup_smp()

配置 SMP 系统,使芯片可以多核运行。

riscv_fill_hwcap()

从设备树中读取处理器的 ISA,并写入 ELF 的 hwcap 字段中,以告知应用程序它们正在运行在怎样的处理器上。

打印信息如下:

[    0.000000] riscv: ISA extensions acdfimsu
[    0.000000] riscv: ELF capabilities acdfim

trap_init()

未分析到

mem_init()

mem_init() 是架构相关函数,我们分析一下该函数具体做了哪些工作。

void __init mem_init(void)
{
#ifdef CONFIG_FLATMEMBUG_ON(!mem_map);
#endif /* CONFIG_FLATMEM */#ifdef CONFIG_SWIOTLBif (swiotlb_force == SWIOTLB_FORCE ||max_pfn > PFN_DOWN(dma32_phys_limit))swiotlb_init(1);//软件DMA映射,解决部分DMA外设无法访问高地址内存的问题。elseswiotlb_force = SWIOTLB_NO_FORCE;
#endifmemblock_free_all();//释放空闲页面给伙伴分配器print_vm_layout();//打印内存分布情况
}

init_IRQ()

中断初始化是一个架构相关的函数,首先从设备树中取出中断控制器 interrupt-controller 这一节点。

通过命令将 qemu 的 DTB 文件导出。

sudo qemu-system-riscv64 -M virt,dumpdtb=my.dtb ...

并将 dtb 文件反编译成 dts 文件。

dtc -I dtb -O dts -o qemu-virt.dts my.dtb

初始化 IRQ 的函数调用关系如下:

init_IRQ() -> irqchip_init() -> of_irq_init()

of_irq_init() 中遍历设备树,通过 __irq_of_table 进行匹配,匹配成功后进行 irq 初始化。

查看设备树,找到 interrupt-controllercompatibleriscv,cpu-intc

cpu@0 {phandle = <0x07>;device_type = "cpu";reg = <0x00>;status = "okay";compatible = "riscv";riscv,isa = "rv64imafdcsu";mmu-type = "riscv,sv48";interrupt-controller {#interrupt-cells = <0x01>;interrupt-controller;compatible = "riscv,cpu-intc";phandle = <0x08>;};
};

通过匹配,最终调用的驱动是 driver/irqchip/irq-riscv-intc.c

static int __init riscv_intc_init(struct device_node *node,struct device_node *parent)
{int rc, hartid;pr_info("[nfk test] %s-%s-%d\r\n",__FILE__,__FUNCTION__,__LINE__);hartid = riscv_of_parent_hartid(node);//获取CPU idif (hartid < 0) {pr_warn("unable to find hart id for %pOF\n", node);return 0;}else{pr_info("[nfk test] get hartid=%d\r\n",hartid);}/** The DT will have one INTC DT node under each CPU (or HART)* DT node so riscv_intc_init() function will be called once* for each INTC DT node. We only need to do INTC initialization* for the INTC DT node belonging to boot CPU (or boot HART).*/if (riscv_hartid_to_cpuid(hartid) != smp_processor_id())return 0;//每一个 CPU 都会有其 DT NODE,当前我们只需要初始化//boot CPU 的 DT NODEintc_domain = irq_domain_add_linear(node, BITS_PER_LONG,&riscv_intc_domain_ops, NULL);//向系统注册一个 irq domain,//最终调用 __irq_domain_add(),进行内存申请,domain 回调函数配置,此处仅完成了 irq_domain 的注册,后面的中断映射关系还需要在具体驱动中实现。if (!intc_domain) {//intc_domain 就是 interrupt-controller 的软件抽象pr_err("unable to add IRQ domain\n");return -ENXIO;}rc = set_handle_irq(&riscv_intc_irq);//配置中断处理函数if (rc) {pr_err("failed to set irq handler\n");return rc;}cpuhp_setup_state(CPUHP_AP_IRQ_RISCV_STARTING,"irqchip/riscv/intc:starting",riscv_intc_cpu_starting,riscv_intc_cpu_dying);//对热插拔函数进行配置pr_info("%d local interrupts mapped\n", BITS_PER_LONG);return 0;
}

[    0.000000] riscv-intc: [nfk test] drivers/irqchip/irq-riscv-intc.c-riscv_intc_init-99 [    0.000000] riscv-intc: get hartid=0 [    0.000000] riscv-intc: hartid 0,cpuid 1 not smp processor_id [    0.000000] riscv-intc: [nfk test] drivers/irqchip/irq-riscv-intc.c-riscv_intc_init-99 [    0.000000] riscv-intc: get hartid=1 [    0.000000] riscv-intc: hartid 1,cpuid 2 not smp processor_id [    0.000000] riscv-intc: [nfk test] drivers/irqchip/irq-riscv-intc.c-riscv_intc_init-99 [    0.000000] riscv-intc: get hartid=2 [    0.000000] riscv-intc: hartid 2,cpuid 3 not smp processor_id [    0.000000] riscv-intc: [nfk test] drivers/irqchip/irq-riscv-intc.c-riscv_intc_init-99 [    0.000000] riscv-intc: get hartid=3 [    0.000000] riscv-intc: 64 local interrupts mapped

中断初始化的打印如上所示。

time_init()

架构相关函数 time_init()

void __init time_init(void)
{struct device_node *cpu;u32 prop;/*设备树中解析 CPU,并且读取他的 timebase-frequency*/cpu = of_find_node_by_path("/cpus");if (!cpu || of_property_read_u32(cpu, "timebase-frequency", &prop))panic(KERN_WARNING "RISC-V system with no 'timebase-frequency' in DTS\n");of_node_put(cpu);//减少引用计数riscv_timebase = prop;lpj_fine = riscv_timebase / HZ; //遍历设备树,进行时钟初始化,类似于 of_irq_init(),linux-lab-disk 中的虚拟开发板当前匹配为空of_clk_init(NULL);timer_probe();
}

timer_probe() 中遍历设备树,通过 __timer_of_table 进行匹配,匹配成功后进行初始化 timer。

void __init timer_probe(void)
{struct device_node *np;const struct of_device_id *match;of_init_fn_1_ret init_func_ret;unsigned timers = 0;int ret;pr_info("[nfk test] %s-%s-%d\n",__FILE__,__FUNCTION__,__LINE__);for_each_matching_node_and_match(np, __timer_of_table, &match) {//遍历设备树,匹配 timerif (!of_device_is_available(np))continue;pr_info("[nfk test] %s-%s-%d\n",__FILE__,__FUNCTION__,__LINE__);init_func_ret = match->data;ret = init_func_ret(np);//timer 初始化if (ret) {if (ret != -EPROBE_DEFER)pr_err("Failed to initialize '%pOF': %d\n", np,ret);continue;}timers++;}timers += acpi_probe_device_table(timer);//注册 timerif (!timers)pr_crit("%s: no matching timers found\n", __func__);pr_info("[nfk test] %s-%s-%d\n",__FILE__,__FUNCTION__,__LINE__);
}

添加调试信息,打印如下:

[    0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-23
[    0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-28
[    0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-28
[    0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-28
[    0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-28
[    0.000000] riscv_timer_init_dt: Registering clocksource cpuid [0] hartid [3]
[    0.000000] clocksource: riscv_clocksource: mask: 0xffffffffffffffff max_cycles: 0x24e6a1710, max_idle_ns: 440795202120 ns
[    0.000126] sched_clock: 64 bits at 10MHz, resolution 100ns, wraps every 4398046511100ns
[    0.002668] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-46

通过以上信息,可知,匹配到了 4 次 timer,通过中间的相关打印信息,找到驱动 drivers/clocksource/timer-riscv.c

static int __init riscv_timer_init_dt(struct device_node *n)
{int cpuid, hartid, error;struct device_node *child;struct irq_domain *domain;hartid = riscv_of_processor_hartid(n);//获取 node 所在的hartidif (hartid < 0) {pr_warn("Not valid hartid for node [%pOF] error = [%d]\n",n, hartid);return hartid;}cpuid = riscv_hartid_to_cpuid(hartid);//获取 cpu idif (cpuid < 0) {pr_warn("Invalid cpuid for hartid [%d]\n", hartid);return cpuid;}if (cpuid != smp_processor_id())return 0;//判断是否未 boot cpudomain = NULL;child = of_get_compatible_child(n, "riscv,cpu-intc");if (!child) {//获取中断的 domainpr_err("Failed to find INTC node [%pOF]\n", n);return -ENODEV;}domain = irq_find_host(child);of_node_put(child);if (!domain) {pr_err("Failed to find IRQ domain for node [%pOF]\n", n);return -ENODEV;}riscv_clock_event_irq = irq_create_mapping(domain, RV_IRQ_TIMER);//建立中断映射if (!riscv_clock_event_irq) {pr_err("Failed to map timer interrupt for node [%pOF]\n", n);return -ENODEV;}pr_info("%s: Registering clocksource cpuid [%d] hartid [%d]\n",__func__, cpuid, hartid);error = clocksource_register_hz(&riscv_clocksource, riscv_timebase);//注册 timerif (error) {pr_err("RISCV timer register failed [%d] for cpu = [%d]\n",error, cpuid);return error;}sched_clock_register(riscv_sched_clock, 64, riscv_timebase);error = request_percpu_irq(riscv_clock_event_irq,riscv_timer_interrupt,"riscv-timer", &riscv_clock_event);//注册中断处理函数if (error) {pr_err("registering percpu irq failed [%d]\n", error);return error;}error = cpuhp_setup_state(CPUHP_AP_RISCV_TIMER_STARTING,"clockevents/riscv/timer:starting",riscv_timer_starting_cpu, riscv_timer_dying_cpu);//热插拔配置if (error)pr_err("cpu hp setup state failed for RISCV timer [%d]\n",error);return error;
}

关于设备树匹配函数分析

循环匹配函数

以下函数是进行循环匹配的函数。

for_each_matching_node_and_match(np, __timer_of_table, &match)
for_each_matching_node_and_match(np, __irqchip_of_table, &match)

我们找到他的根本调用,参数描述如下,分别是设备树节点,要扫描的结构体,匹配到的结构体。

/*** of_find_matching_node_and_match - Find a node based on an of_device_id*                                   match table.* @from:       The node to start searching from or NULL, the node*              you pass will not be searched, only the next one*              will; typically, you pass what the previous call*              returned. of_node_put() will be called on it* @matches:    array of of device match structures to search in* @match:      Updated to point at the matches entry which matched** Return: A node pointer with refcount incremented, use* of_node_put() on it when done.*/

of_find_matching_node_and_match 最终调用的设备树匹配函数为 __of_device_is_compatible。其中输入参数 matches 就是要进行匹配的结构体。

匹配函数入参 table 的由来

搞清楚入参之后,我们找一下 __timer_of_table 从何处定义。

#define TIMER_OF_DECLARE(name, compat, fn) \OF_DECLARE_1_RET(timer, name, compat, fn)

下一层宏定义

#define OF_DECLARE_1_RET(table, name, compat, fn) \ _OF_DECLARE(table, name, compat, fn, of_init_fn_1_ret)

下一层宏定义

#define _OF_DECLARE(table, name, compat, fn, fn_type)                   \static const struct of_device_id __of_table_##name              \__used __section("__" #table "_of_table")               \__aligned(__alignof__(struct of_device_id))             \= { .compatible = compat,                              \.data = (fn == (fn_type)NULL) ? fn : fn  }

所以我们根据宏定义 TIMER_OF_DECLARE 寻找与设备树节点可以匹配的驱动。

我们找到相关的 TIMER_OF_DECLARE

// drivers/clocksource/timer-riscv.cTIMER_OF_DECLARE(riscv_timer, "riscv", riscv_timer_init_dt)

根据宏定义展开可得:

static const struct of_device_id
__of_table_riscv_timer              \__used __section("__timer_of_table")               \__aligned(__alignof__(struct of_device_id))             \= {.compatible = "riscv",                              \.data = (riscv_timer_init_dt == (of_init_fn_1_ret)NULL) ? riscv_timer_init_dt : riscv_timer_init_dt  }

这个地方就是 __of_table_timer 的由来。

__of_table_timer 如何被生成为表

我们可以看到,设备树匹配时,是通过 for 循环进行遍历的,也就是说__of_table_timer 中有多个结构体供查询。

__used __section("__" timer "_of_table")

展开为:

#define __used   __attribute__((__used__))
#define __section(S)  __attribute__((__section__(#S)))
__attribute__((__used__)) __attribute__((__section__(__timer_of_table")))

GNU C 的一大特色就是 __attribute__ 机制。__attribute__ 可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。

当前使用的 section 关键字可以将变量属性设置为“定义至指定的输入段中”。也就是说 __of_table_riscv_timer 这个结构体被定义到了指定的段中。所以最终结果是 __of_table_timer 是一个表,这个表代表着一个数据段,这个数据段中存着我们保存的结构体变量。

如何查看数据段变量

通过查看 System.map 可以看到数据段的分配,在 __timer_of_table 中分配了一个结构体。

分配情况如下:

ffffffff80a0df28 T __reservedmem_of_table
ffffffff80a0dff0 t __rmem_of_table_sentinel
ffffffff80a0e0b8 t __of_table_riscv_timer
ffffffff80a0e0b8 T __timer_of_table
ffffffff80a0e180 t __timer_of_table_sentinel
ffffffff80a0e248 T __cpu_method_of_table
ffffffff80a0e260 T __dtb_end
ffffffff80a0e260 T __dtb_start
ffffffff80a0e260 T __irqchip_of_table
ffffffff80a0e260 t __of_table_riscv

计算一下大小:__of_table_riscv_timer=0x80a0e0b8-0x80a0dff0=200 bytes

ubuntu@linux-lab:/labs/linux-lab/build/riscv64/virt/linux/v5.17$ readelf vmlinux -a |grep __timer_of_table57868: ffffffff80a0e0b8     0 NOTYPE  GLOBAL DEFAULT    5 __timer_of_table
ubuntu@linux-lab:/labs/linux-lab/build/riscv64/virt/linux/v5.17$ readelf vmlinux -a |grep __of_table_riscv_timer40119: ffffffff80a0e0b8   200 OBJECT  LOCAL  DEFAULT    5 __of_table_riscv_timer

与 vmlinux 中的数据可以匹配上。

实际计算结构体大小也是 200 字节(32+32+128+8=200);

/** Struct used for matching a device*/
struct of_device_id {char name[32];char type[32];char compatible[128];const void *data;
};

小结

本文对 RISC-V 架构下的 Linux 的启动流程进行了梳理,在梳理过程中遇到了设备树解析方面的问题,在后面也进行了设备树解析流程的深入分析。本文更关注于流程,在深度上存在欠缺,大家可以针对于流程中的某个点进行更深入的分析。

延申阅读

  • 如何分析 Linux 内核 RISC-V 架构相关代码

  • RISC-V UEFI 架构支持详解,第 1 部分 - OpenSBI/U-Boot/UEFI 简介

  • RISC-V OpenSBI 快速上手

  • 将 Linux 移植到新的处理器架构,第 1 部分:基础

  • RISC-V Linux jump_label 详解,第 1 部分:技术背景

fcf17ac77d36895955fc2a1a14d550c0.png

转自nihui图

b063243aca1eb1a16cad17d2dacbcf87.jpeg

c54f607f5774ca3988537ddd001b2c71.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/463889.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

两个栈来实现一个队列的C++代码

转载&#xff1a;http://blog.csdn.net/stpeace/article/details/46765343 利用两个栈来实现一个队列&#xff0c; 这个问题很常见。 最关键的是要有好的思路&#xff0c; 至于实现&#xff0c; 那是很简单的事情了。 在本文中&#xff0c; 也想说说自己的思路&#xff0c; 但…

解读设计模式----单例模式(Singleton Pattern)

单例模式可以保证一个类有且只有一个实例,并提供一个访问它的全局访问点.在程序设计中,有很多情况需要确保一个类只能有一个实例.从这句话可以看出,Singleton模式的核心&#xff1a;如何控制用户使用new对一个类的实例构造器的任意调用。如何绕过常规的构造器&#xff0c;提供一…

网站开发的小知识参考(慢慢收集备用)

inetinfo.exe占用80端口的解决方法 我在本机配置PHP环境时&#xff0c;安装的时候遇到80端口被占用&#xff0c;经过查询才知道&#xff0c;原来是Inetinfo.exe占用了80端口&#xff0c;于是把Inetinfo.exe服务关掉就可以正常安装PHP环境了。解决方法在windows服务管理里&#…

Linux下如何搭建Java环境

软件环境 虚拟机&#xff1a;VMware Workstation系统&#xff1a;Linux&#xff1a;CentOS-7-x86_64工具&#xff1a; 安装工具&#xff1a;SSH客户端搭建步骤 1&#xff1a;查询当前系统版本 cat /proc/version2&#xff1a;首先确认当前系统是否已经存在java环境 java -vers…

程序中提升几毫秒、节省几 kB 的内存有必要吗?

我记得我上小学的时候有一篇课文&#xff0c;说是有一个人很节省&#xff0c;不仅他自己家里节省&#xff0c;如果别人家吃饭乱扔的剩饭&#xff0c;他都会去收拾起来晒干存起来。然后刚好有一年饥荒&#xff0c;大家都没有米吃了&#xff0c;他就把他那些年存起来的米给大家吃…

pthread_join()函数理解

pthread_t tid; 使用方式&#xff1a;pthread_join(tid,NULL); 为什么要使用pthread_join()&#xff1f; 在很多情况下&#xff0c;主线程生成并起动了子线程&#xff0c;如果子线程里要进行大量的耗时的运算&#xff0c;主线程往往将于子线程之前结束&#xff0c; 但是如果主…

SmartTemplate学习入门一

php最简单的模板 Array的变量是由SmartTemplate内建函数assign&#xff08;&#xff09;来赋值的 具体语法如下 assign ( 模版中的变量, 要替换的内容 ) 或 assign ( Array内容 ) 和其他程序的变量一样&#xff0c;smartTemplate的变量是由特殊的{}所包含的。里面的内容可以是S…

人生感言

马云说的一些话&#xff0c;还是值得思考的&#xff1a; (1)、细节好的人格局一般比较差 (2)、态度比能力重要&#xff0c;选择同样也比能力重要 (3)、领导比员工多什么&#xff1f; 领导永远不要跟下属比技能&#xff0c;下属肯定比你强&#xff1b;如果不比你强&#x…

当MCU死机了,先把硬件抓过来~

关于软件开发中的偶发性问题&#xff0c;有些处理办法看似不是很难&#xff0c;但其实最重要的还是对问题的敏感度&#xff0c;而这份敏感度就来源于你对整个系统的理解和把握。当你能够尽快缩小问题代码的范围&#xff0c;在一定程度上就已经加快了解决问题的进度。之前我曾提…

青蛙学Linux—NFS

NFS&#xff0c;Network File Syttem&#xff0c;网络文件系统。它允许网络上运行不同操作系统的主机通过网络连接到运行NFS服务的主机上&#xff0c;以实现数据共享。NFS的配置非常简单&#xff0c;经过简单的设置既能快速使用NFS。 使用NFS&#xff0c;首先在服务端运行NFS服…

extjs 学习中

首先找了个js得开发工具&#xff1a;Komodo Edit 但是安装了好多次都没有出现提示&#xff0c;均告失败&#xff01;今天再次鼓起勇气安装试试&#xff1a;首先安装Komodo-Edit-4.3.2-1263; 成功&#xff01;然后&#xff1a; 下载extjs_api_catalogs-2.0.2-ko.xpi &#xff…

火眼睛睛查coredump(stl sort)------永远让比较函数对相同元素返回false

转载&#xff1a;http://blog.csdn.net/stpeace/article/details/51040218#cpp 看看如下代码的一个非常隐晦的错误&#xff0c; 虽然不会每次core dump, 但类似代码迟早会core dump&#xff0c; 好多人遇到过。 此问题极难定位&#xff0c; 看一下吧&#xff1a; [cpp] view pl…

P1648 看守

传送门 以二维的两个点\((x1,y1),(x2,y2)\)为例&#xff0c;那么他们之间的曼哈顿距离肯定为一下四个之一\((x1-x2)(y1-y2)\),\((x2-x1)(y1-y2)\),\((x1-x2)(y2-y1)\),\((x2-x1)(y2-y1)\)&#xff0c;而且为这四个里面最大的 然后搞一搞可以变成下面的样子\((x1y1)-(x2y2)\),\(…

好友让我看这段代码

周末的时候&#xff0c;一个微信好友让我旁边看一段代码在写下面的文章之前&#xff0c;我先简单说下写代码是一件非常有意思的事情&#xff0c;同时也是一件需要我们认真对待的事情&#xff0c;我不认为一定要把代码写的和大神一样看不明白&#xff0c;但是至少要逻辑清晰&…

使用github管理Eclipse分布式项目开发

使用github管理Eclipse分布式项目开发 老关我在前面的博文&#xff08;github管理iOS分布式项目开发&#xff09;中介绍了github管理iOS分布式开发&#xff0c;今天老关将向大家介绍使用github管 理Eclipse分布式项目。事实上我们的516inc团队这在开发一个多移动平台项目&#…

愉快且卓有成效:培养你与人相处的能力

毫无疑问&#xff0c;渊博的学识和不断的创新是事业成功的基础。然而&#xff0c;把一个概念变为成果&#xff0c;离开他人的合作&#xff0c;任何人&#xff0c;无论是伟人还是凡夫&#xff0c;都无法实现。与人合作得是否愉快且卓有成效&#xff0c;完全取决于你与人相处的能…

小玩一个并行多线程MCU—MC3172

大家好转发一篇杂烩君的文章&#xff0c;杂烩君是我同一个高中的老乡&#xff0c;他平时分享的嵌入式知识非常不错。——————大家好&#xff0c;我是杂烩君。最近&#xff0c;朋友送了块小板子&#xff0c;板子上的MCU是个很有意思的东西——并行多线程处理器MC3172 。通俗…

Android 人脸识别签到(一)

因为Android课程设计自己选题&#xff0c;所以作者选了这个相对简单的。本来开始是想做大学课程查签到&#xff0c;拍一张集体照&#xff0c;就可识别哪些人已到&#xff0c;哪些未到。查了一下百度AI开发平台的人脸识别接口&#xff0c;发现V3的接口文档有M:N的人脸识别&#…

[系列文章]上传文件管理控件v2

一、引言&#xff1a; 开发v1的时候&#xff0c;遇到很多困难&#xff08;因为我是新手&#xff09;&#xff0c;于是就上网找了一些资料。 其中&#xff0c;这篇《数据绑定的总结 》文章&#xff08;http://www.cnblogs.com/qingtianyzl/articles/351012.html&#xff09;被我…

[ mongoDB ] - mongoDB的基本操作

mongoDB的基本操作 强烈推荐参考官方用户手册&#xff1a; http://www.mongodb.org/display/DOCS 1)插入&#xff08;insert&#xff09; 插入的value是json对象&#xff0c;以下示例循环添加了10个用户信息&#xff0c;字段可以是字符串、数值、对象、数组等。 通过VUE查看&am…