RISC-V
			- 
					
						  启动第一个应用进程start_kernel ...... arch_call_rest_init() rest_init(); pid = kernel_thread(kernel_init, NULL, CLONE_FS); static int __ref kernel_init(void *unused) { int ret; kernel_init_freeable(); /* need to finish all async __init code before freeing the memory */ async_synchronize_full(); ftrace_free_init_mem(); free_initmem(); mark_readonly(); /* * Kernel mappings are now finalized - update the userspace page-table * to finalize PTI. */ pti_finalize(); system_state = SYSTEM_RUNNING; numa_default_policy(); rcu_end_inkernel_boot(); 创建1号进程的3种方式,如下: 1.方式① if (ramdisk_execute_command) { ret = run_init_process(ramdisk_execute_command); if (!ret) return 0; pr_err("Failed to execute %s (error %d)\\n", ramdisk_execute_command, ret); } /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ 2.方式② if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret); } 3.方式③ if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0; panic("No working init found. Try passing init= option to kernel. " "See Linux Documentation/admin-guide/init.rst for guidance."); } 从上述代码种可知,启动1号进程init一共有3种方式,分别是ramdisk的方式、execute_command的方式、默认方式。第一种是基于ramdisk的方式,将ram作为启动盘挂载根文件系统。第二种通常用于用户指定第一个init进程启动在根文件系统的位置。如果第一个和第二都没有就会使用默认。 ramdisk方式 static int __init rdinit_setup(char *str) { unsigned int i; ramdisk_execute_command = str; /* See "auto" comment in init_setup */ for (i = 1; i < MAX_INIT_ARGS; i++) argv_init[i] = NULL; return 1; } __setup("rdinit=", rdinit_setup); __setup(str,fn)的作用是,在系统开机时解析cmdline中是否有str字段,如果有就会调用fn函数,因此上面的函数意思就是当cmdline中有rdinit=字段时,就会调用rdinit_setup函数,将rdinit=后面的内容赋值给ramdisk_execute_command。 execute_command方式 static int __init init_setup(char *str) { unsigned int i; execute_command = str; /* * In case LILO is going to boot us with default command line, * it prepends "auto" before the whole cmdline which makes * the shell think it should execute a script with such name. * So we ignore all arguments entered _before_ init=... [MJ] */ for (i = 1; i < MAX_INIT_ARGS; i++) argv_init[i] = NULL; return 1; } __setup("init=", init_setup); 上述代码同理,当cmdline中存在\"init=\"时,调用init_setup,将参数赋值给execute_command。这种情况一般是可以让用户指定启动的第一个进程。 默认方式 static int __ref kernel_init(void *unused) { ...... if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0; ...... } 当cmdline中既没有rdinit和init字段时,就是直接使用默认的方式。
- 
					
						  挂载根文件系统根文件系统是控制权从linux内核转移到用户空间的一个桥梁,文件系统的挂载需要提供挂载点,linux内核在初始化时汇初始化一个虚拟的“/”目录用于根文件系统的挂载。 start_kernel vfs_caches_init() mnt_init() init_rootfs() init_mount_tree() vfs_kern_mount(&rootfs_fs_type, 0, "rootfs", NULL); 根文件系统的挂载待日后进一步研究,本小结先简单介绍一下initrd,initramfs,ramdisk。 内核启动执行的第一个应用进程,需要先读取文件系统执行/init或/sbin/init,因此需要先初始化文件系统。而文件系统一般存储在存储介质(flash,IDE,SCSI)上,访问这些存储介质需要有驱动程序。那么是否有什么办法可以解决这个问题了?以下有两种方法可以解决: (1)存储介质驱动编译进内核:是将存储介质的驱动程序编译进内核,内核启动阶段就可以访问存储介质。这种方法对于固定存储介质型号倒也还好,只需要编译少数驱动程序即可。而对于发行版本的linux,如果要支持很多个存储介质,就需要将很多驱动编译进内核,这样就会导致linux内核镜像非常庞大。 (2)基于ramdisk的文件系统:ramdisk不是一个实际的文件系统,而是一种实际文件系统装入内存的机制,并且可以做为根文件系统,换句话说ramdisk实际上是从内存划分一部分空间,当作磁盘来使用,可以往里面存储任何东西。因此当存储介质驱动程序没有准备好时,我们可以把内存模拟成一个存储介质,内核配置上CONFIG_BLK_DEV_RAM,就支持了ramdisk,一般会生成/dev/ramx节点。基于该节点我们可以格式化想要的文件系统。 (3)基于ramdisk的initrd(bootloader initialized RAM Disk):是针对方法(1)的改进,把内存当作存储介质,这方方式我们称为ramdisk,设备节点为/dev/ramX。initrd是一个被压缩过的小型根目录,包含了启动阶段中必须的驱动模块,这个目录中包含了启动阶段中必须的驱动模块,可执行文件和启动脚本。当系统启动的时候,bootloader会把initrd文件读到内存中,然后把initrd文件在内存中的起始地址和大小传递给内核。内核在启动初始化过程中会解压缩initrd文件,然后将解压后的initrd挂载为根目录,然后执行根目录中的/linuxrc脚本(cpio格式的initrd为/init,而image格式的initrd<也称老式块设备的initrd或传统的文件镜像格式的initrd>为/initrc),就可以在这个脚本中加载realfs(真实文件系统)存放设备的驱动程序以及在/dev目录下建立必要的设备节点。这样,就可以mount真正的根目录,并切换到这个根目录中来。initrd并不是一个实际的文件系统,只能说算是一个打包的根目录。 (4)基于内存的initramfs:方式(3)有一个弊端,就是必须要事先支持ramdisk,分配固定的大小,那么这个大小到底分配多少了?分多了浪费,分少了可能不够,针对这种情况,在linux2.5内核之后出现了initramfs。它的作用和initrd类似,只是和内核编译成一个文件。initramfs是经过gzip压缩后的cpio格式和数据文件,该cpio格式文件被链接进了内核中特殊的数据端.init.ramfs上,其中全局变量__initramfs_start和__initramfs_end分别指向这个数据段的起始地址和结束地址。内核启动时会对.init.ramfs段中的数据进行解压,然后使用它作为临时的根文件系统。这种方式依旧会导致内核镜像变大。注意:initramfs本身是一个ramfs文件系统,因此它可以内嵌到内核中,也可以单独存在 Initrd和initramfs的区别? (1) Linux内核只认cpio格式的initramfs文件包(因为unpack_to_rootfs只能解析cpio格式文件),非cpio格式的 initramfs文件包将被系统抛弃,而initrd可以是cpio包也可以是传统的镜像(image)文件,实际使用中initrd都是传统镜像文件。 (2)initramfs在编译内核的同时被编译并与内核连接成一个文件,它被链接到地址__initramfs_start处,与内核同时被 bootloader加载到ram中,而initrd是另外单独编译生成的,是一个独立的文件,它由bootloader单独加载到ram中内核空间外的地址,比如加载的地址为addr(是物理地址而非虚拟地址),大小为8MB,那么只要在命令行加入\"initrd=addr,8M\"命令,系统就可以找到 initrd(当然通过适当修改Linux的目录结构,makefile文件和相关代码,以上两种情况都是可以相通的)。 (3)initramfs被解析处理后原始的cpio包(压缩或非压缩)所占的空间(&__initramfs_start - &__initramfs_end)是作为系统的一部分直接保留在系统中,不会被释放掉,而对于initrd镜像文件,如果没有在命令行中设置\"keepinitd\"命令,那么initrd镜像文件被处理后其原始文件所占的空间(initrd_end - initrd_start)将被释放掉。 (4)initramfs可以独立ram disk单独存在,而要支持initrd必须要先支持ram disk,即要配置CONFIG_BLK_DEV_INITRD选项 -- 支持initrd,必须先要配置CONFIG_BLK_DEV_RAM -- 支持ram disk ,因为initrd image实际就是初始化好了的ramdisk镜像文件,最后都要解析、写入到ram disk设备/dev/ram或/dev/ram0中。注: 使用initramfs,命令行参数将不需要\"initrd=\"和\"root=\"命令 在linux2.5之后,出现了initramfs,initrd有其固有的缺陷(需要ramdisk,固定大小内存等),本章节就重点介绍一下initramfs和基于ramdisk的文件系统方式。 initramfs方式 早期使用的是initrd,但是在linux2.5之后就升级为initramfs,因此本章节重点描述initramfs。 initramfs可以直接集成到kernel里面,也可以单独加载initramfs。 内核配置选择 General setup ----> [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support 以下是要支持压缩的格式 [*] Support initial ramdisk/ramfs compressed using gzip [*] Support initial ramdisk/ramfs compressed using bzip2 [*] Support initial ramdisk/ramfs compressed using LZMA [*] Support initial ramdisk/ramfs compressed using XZ [*] Support initial ramdisk/ramfs compressed using LZO [*] Support initial ramdisk/ramfs compressed using LZ4 制作initramfs cp busybox/_install/ rootfs cd rootfs find . | cpio -o -H newc > rootfs.cpio //将文件复制打包成一个文件 gzip -c rootfs.cpio > rootfs.cpio.gz //将文件进行压缩为gz格式 其中设置-append \"rdinit=/linuxrc\"是Linux内核命令行、启动参数,-initrd initramfs/rootfs/rootfs.cpio.gz 指定文件镜像。Rootfs.cpio.gz是要和内核编译进一个文件的,但是qemu支持通过-initrd指定文件镜像。 qemu-system-riscv64 -M virt -cpu c910v -m 256M -nographic \\ -bios qemu-opensbi/build/platform/generic/firmware/fw_jump.bin \\ -kernel linux-5.4/arch/riscv/boot/Image \\ -initrd initramfs/rootfs/rootfs.cpio.gz \\ -append "rdinit=/linuxrc" 其中设置-append \"rdinit=/linuxrc\"是Linux内核命令行、启动参数,-initrd initramfs/rootfs/rootfs.cpio.gz 指定文件镜像。Rootfs.cpio.gz是要和内核编译进一个文件的,但是qemu支持通过-initrd指定文件镜像。 如果要将根文件系统内嵌如内核 General setup ----> (../initramfs/rootfs/rootfs.cpio.gz) Initramfs sourc ramdisk方式 ramdisk的方式是将内存作为存储盘,生成一个/dev/ramx的节点。作为存储介质不一定使用ramfs文件系统,它可以是任意类型的文件系统,如ext2,ext4等,当然也可以是ramfs。本小结我们演示基于ramdisk挂载ext4的方式启动。 Device Drivers ----> [*] Block devices ----> <*> RAM block device support ----> (65535) Default RAM disk size (kbytes) Ramdisk是在内存中模拟磁盘,使用ramdisk比Initramfs灵活一些,不需要每次都去编译内核。不过在qemu中,都支持通过-initrd指定文件镜像。注意ramdisk大小一定要足够大。 qemu-system-riscv64 -M virt -cpu c910v -m 2G -nographic \\ -bios qemu-opensbi/build/platform/generic/firmware/fw_jump.bin \\ -kernel linux-5.4/arch/riscv/boot/Image \\ -initrd ./rootfs_7_4.img \\ -append "root=/dev/ram0 ramdisk_size=268435456 rw console=ttyS0" 根文件系统制作方法 qemu-img create rootfs.img 256M #---创建一个文件 mkfs.ext4 rootfs.img #---将文件格式为ext4格式 mkdir rootfs sudo mount -o loop rootfs.img rootfs #---挂在文件到rootfs下,将命令等拷贝进去 cd rootfs sudo mkdir dev sudo mkdir proc sudo mkdir sys sudo cp -r ../busyboxsource/_install/* . sudo mkdir proc sys dev etc etc/init.d cd etc/init.d/ sudo touch rcS sudo vi rcS #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys /sbin/mdev -s
- 
					
						  异常初始化异常处理概述 略 异常入口 start_kernel trap_init(); void __init trap_init(void) { /* * Set sup0 scratch register to 0, indicating to exception vector * that we are presently executing in the kernel */ csr_write(CSR_SSCRATCH, 0); /* Set the exception vector address */ csr_write(CSR_STVEC, &handle_exception); /* Enable all interrupts */ csr_write(CSR_SIE, -1); 设置trap的入口函数为handle_exception。 } 异常软件处理流程 ENTRY(handle_exception) SAVE_ALL ① 保存上下文,主要是一些寄存器 /* * Set sscratch register to 0, so that if a recursive exception * occurs, the exception vector knows it came from the kernel */ csrw CSR_SSCRATCH, x0 ②清空临时寄存器scratch /* Load the global pointer */ .option push .option norelax la gp, __global_pointer$ .option pop la ra, ret_from_exception ③ 设置异常返回 /* * MSB of cause differentiates between * interrupts and exceptions */ bge s4, zero, 1f /* Handle interrupts */ move a0, sp /* pt_regs */ tail do_IRQ ④s4里面保存的是scause的值,通过最高位判断是中断还是异常,中断调用do_IRQ,异常的就接着往下运行。关于do_IRQ的处理待后续中断处理流程单独分析。 1: /* Exceptions run with interrupts enabled or disabled depending on the state of sstatus.SR_SPIE */ andi t0, s1, SR_SPIE beqz t0, 1f csrs CSR_SSTATUS, SR_SIE 1: /* Handle syscalls */ li t0, EXC_SYSCALL beq s4, t0, handle_syscall ④ 异常类型是系统调用,则跳转handle_syscall /* Handle other exceptions */ slli t0, s4, RISCV_LGPTR la t1, excp_vect_table la t2, excp_vect_table_end move a0, sp /* pt_regs */ add t0, t1, t0 /* Check if exception code lies within bounds */ bgeu t0, t2, 1f REG_L t0, 0(t0) jr t0 ⑤如果不是系统调用,处理其他类型异常,根据excp_vect_table进行跳转,如do_page_fault 1: tail do_trap_unknown ⑥如果从excp_vect_table都未查询到对应的异常,进行单独处理。 handle_syscall: /* save the initial A0 value (needed in signal handlers) */ REG_S a0, PT_ORIG_A0(sp) /* * Advance SEPC to avoid executing the original * scall instruction on sret */ addi s2, s2, 0x4 REG_S s2, PT_SEPC(sp) /* Trace syscalls, but only if requested by the user. */ REG_L t0, TASK_TI_FLAGS(tp) andi t0, t0, _TIF_SYSCALL_WORK bnez t0, handle_syscall_trace_enter check_syscall_nr: /* Check to make sure we don't jump to a bogus syscall number. */ li t0, __NR_syscalls la s0, sys_ni_syscall /* Syscall number held in a7 */ bgeu a7, t0, 1f la s0, sys_call_table slli t0, a7, RISCV_LGPTR add s0, s0, t0 REG_L s0, 0(s0) 1: jalr s0 ret_from_syscall: /* Set user a0 to kernel a0 */ REG_S a0, PT_A0(sp) /* Trace syscalls, but only if requested by the user. */ REG_L t0, TASK_TI_FLAGS(tp) andi t0, t0, _TIF_SYSCALL_WORK bnez t0, handle_syscall_trace_exit ⑦上面是系统调用的异常处理,后续再单独分析。 ret_from_exception: REG_L s0, PT_SSTATUS(sp) csrc CSR_SSTATUS, SR_SIE andi s0, s0, SR_SPP bnez s0, resume_kernel ⑧从异常中返回,根据sstatus中SPP的位来判断是内核陷入的异常还是用户陷入的异常,0是用户陷入的异常,跳转执行resume_userspace,1是内核陷入的异常,跳转执行resume_kernel。sstatus[8]是SPP位,保存的是陷入异常前是U模式还是S模式,如果是U模式代表是用户态,如果是S模式代表是内核态。 resume_userspace: /* Interrupts must be disabled here so flags are checked atomically */ REG_L s0, TASK_TI_FLAGS(tp) /* current_thread_info->flags */ andi s1, s0, _TIF_WORK_MASK bnez s1, work_pending /* Save unwound kernel stack pointer in thread_info */ addi s0, sp, PT_SIZE_ON_STACK REG_S s0, TASK_TI_KERNEL_SP(tp) /* * Save TP into sscratch, so we can find the kernel data structures * again. */ csrw CSR_SSCRATCH, tp ⑨ 恢复用户态的tp,线程指针,待到后续进程调度时再详细分析。 restore_all: RESTORE_ALL sret ⑩ 恢复上下文,执行sret返回。 arch/riscv/kernel/entry.S是关于risc-v trap入口的实现,trap的包含异常和中断。trap的中总入口为handle_exception,主要处理一下事项。 - 保存上下文,主要是将一些寄存器压入栈中。 - 根据scause寄存器判断是异常还是中断,如果是中断跳转到do_IRQ运行,如果是异常根据异常的cause进分发处理。 - 异常中有一类是系统调用,根据scause查询如果是系统调用根据sys_call_table[]跳转处理。 - 异常如果不是系统调用,就是程序运行中出现的异常,根据scause查询excp_vect_table[]跳转处理。 - 异常和中断都处理完成之后,最终回到ret_from_exception,这里会根据sstatus的SSP位来判断是用户态陷入的还是内核态陷入,根据内核和用户态的更新tp。 - 最后调用RESTORE_ALL恢复上下文,退出trap。
- 
					
						  虚拟地址空间与物理地址空间完整映射setup bootmem 物理内存都添加到系统之后,会调用setup_bootmem对整个物理内存进行整理,主要的工作就是remove掉一些no-map区域(不归内核管理),同时保留一些关键区域,如内核镜像区,dtb中reserved的内存节点。 上图中,浅绿色的就是reserved部分,不能被分配使用,而剩下的部分就可以通过调用上小章节中的函数去使用内存了。 void __init setup_bootmem(void) { struct memblock_region *reg; phys_addr_t mem_size = 0; phys_addr_t vmlinux_end = __pa(&_end); phys_addr_t vmlinux_start = __pa(&_start); /* Find the memory region containing the kernel */ for_each_memblock(memory, reg) { phys_addr_t end = reg->base + reg->size; if (reg->base <= vmlinux_end && vmlinux_end <= end) { mem_size = min(reg->size, (phys_addr_t)-PAGE_OFFSET); /* * Remove memblock from the end of usable area to the * end of region */ if (reg->base + mem_size < end) memblock_remove(reg->base + mem_size, end - reg->base - mem_size); } } BUG_ON(mem_size == 0); /* Reserve from the start of the kernel to the end of the kernel */ memblock_reserve(vmlinux_start, vmlinux_end - vmlinux_start); ①将内核镜像地址空间添加到&memblock.reserved。 set_max_mapnr(PFN_DOWN(mem_size)); max_low_pfn = PFN_DOWN(memblock_end_of_DRAM()); #ifdef CONFIG_BLK_DEV_INITRD setup_initrd(); #endif /* CONFIG_BLK_DEV_INITRD */ /* * Avoid using early_init_fdt_reserve_self() since __pa() does * not work for DTB pointers that are fixmap addresses */ memblock_reserve(dtb_early_pa, fdt_totalsize(dtb_early_va)); ②将dtb的空间添加到&memblock.reserved early_init_fdt_scan_reserved_mem(); ③遍历设备树的节点,将/memreserve/,reserved-memory节点添加到&memblock.reserved memblock_allow_resize(); memblock_dump_all(); for_each_memblock(memory, reg) { unsigned long start_pfn = memblock_region_memory_base_pfn(reg); unsigned long end_pfn = memblock_region_memory_end_pfn(reg); memblock_set_node(PFN_PHYS(start_pfn), PFN_PHYS(end_pfn - start_pfn), &memblock.memory, 0); } } 下面是调用memblock_dump_all的信息: MEMBLOCK configuration: memory size = 0x000000000fe00000 reserved size = 0x000000000091288b memory.cnt = 0x1 memory[0x0] [0x0000000080200000-0x000000008fffffff], 0x000000000fe00000 bytes flags: 0x0 reserved.cnt = 0x3 reserved[0x0] [0x0000000080000000-0x000000008003ffff], 0x0000000000040000 bytes flags: 0x0 reserved[0x1] [0x0000000080200000-0x0000000080ad133b], 0x00000000008d133c bytes flags: 0x0 reserved[0x2] [0x0000000082200000-0x000000008220154e], 0x000000000000154f bytes flags: 0x0 小结: (1)系统通过memblock以数组memory type的方式记录物理内存空间,数组中每一个内存区域描述了一段内存信息,包括base,size,node id等。 (2)在memblock信息中,已经被使用或者被内核定义需要保留的区域,会存储在reserved 数组中。 (3)memory type数组中并不是代表整个内核系统的内存空间,因为部分驱动会保留一段内存区域供自己单独使用,其在dts中具有no-map属性的reserved-memory节点,不会由内核创建地址映射。 (4)可以通过内核调试节点/sys/kernel/debug/memblock进行查询相关信息 完整映射 在前面部分只完成了kernel、dtb,部分临时页表的临时映射,也就是说对于内核来说使能了MMU后也只能访问kernel,dtb,部分临时页表。在前面阶段,内核也只知道kernel,dtb的物理位置,对于其他的内存位置内核是不清楚的。通过memblock机制后,内核系统已经对物理内存布局信息比较清楚了,那么就需要对内存完成最后的映射。主要的工作是,对此前的kernel、dtb等映射重新做一次调整,统一使用swapper_pg_dir根目录页表,同时对于新纳入memblock中的内存建立映射,填充好对应的页表。 static void __init setup_vm_final(void) { uintptr_t va, map_size; phys_addr_t pa, start, end; struct memblock_region *reg; /* Set mmu_enabled flag */ mmu_enabled = true; /* Setup swapper PGD for fixmap */ create_pgd_mapping(swapper_pg_dir, FIXADDR_START, __pa(fixmap_pgd_next), PGDIR_SIZE, PAGE_TABLE); ① 将fixmap的映射的页表从early_pg_dir切换为swapper_pg_dir。 /* Map all memory banks */ for_each_memblock(memory, reg) { start = reg->base; end = start + reg->size; if (start >= end) break; if (memblock_is_nomap(reg)) continue; if (start <= __pa(PAGE_OFFSET) && __pa(PAGE_OFFSET) < end) start = __pa(PAGE_OFFSET); map_size = best_map_size(start, end - start); for (pa = start; pa < end; pa += map_size) { va = (uintptr_t)__va(pa); create_pgd_mapping(swapper_pg_dir, va, pa, map_size, PAGE_KERNEL_EXEC); } } ②遍历memblock中的内存,对所有的内存进行映射,根目录页表为swapper_pg_dir,二级页表和PTE页表会使用memblock_phys_alloc进行分配内存,对已经分配的内存访问会先反向映射到fixmap地址空间,通过fixmap进行访问页表。 /* Clear fixmap PTE and PMD mappings */ clear_fixmap(FIX_PTE); clear_fixmap(FIX_PMD); /* Move to swapper page table */ csr_write(CSR_SATP, PFN_DOWN(__pa(swapper_pg_dir)) | SATP_MODE); local_flush_tlb_all(); ③将swapper_pg_dir的根目录写到satp寄存器,这样就完成了整个物理内存到虚拟地址空间的映射,所有的物理内存空间都可以进行访问了。 } 到此,内核通过memblock知道物理内存的布局信息,虚拟地址翻译到物理内存所需要的页表都填充好了,所有要访问的物理内存都映射虚拟地址空间,所有的物理内存都可以通过虚拟地址正常访问了。内存下一阶段的初始化会涉及到Node、Zone以及伙伴系统等,这里就先不阐述。
- 
					
						  使能MMU① 多核情况使能MMU .Lsecondary_start: #ifdef CONFIG_SMP /* Set trap vector to spin forever to help debug */ la a3, .Lsecondary_park csrw CSR_STVEC, a3 slli a3, a0, LGREG la a1, __cpu_up_stack_pointer la a2, __cpu_up_task_pointer add a1, a3, a1 add a2, a3, a2 /* * This hart didn\'t win the lottery, so we wait for the winning hart to * get far enough along the boot process that it should continue. */ .Lwait_for_cpu_up: /* FIXME: We should WFI to save some energy here. */ REG_L sp, (a1) REG_L tp, (a2) beqz sp, .Lwait_for_cpu_up beqz tp, .Lwait_for_cpu_up fence /* Enable virtual memory and relocate to virtual address */ la a0, swapper_pg_dir call relocate tail smp_callin #endif ②主hart 使能mmu _start_kernel la a0, early_pg_dir call relocate relocate: /* Relocate return address */ li a1, PAGE_OFFSET la a2, _start sub a1, a1, a2 add ra, ra, a1 ③ PAGE_OFFSET(内核的虚拟地址) - _start(内核加载的物理内存) 得到虚拟地址相对物理地址的偏移,ra = ra + (PAGE_OFFSET - _start),相当于计算了ra的虚拟地址。 /* Point stvec to virtual address of intruction after satp write */ la a2, 1f add a2, a2, a1 csrw CSR_STVEC, a2 ④ 将便签1转化为虚拟地址,a1存储的还是va_pa_offset,再将标签1处的虚拟地址填充到stvec寄存器,设置中断的入口函数。 /* Compute satp for kernel page tables, but don\'t load it yet */ srl a2, a0, PAGE_SHIFT a0 = early_pg_dir, a2 = early_pg_dir >> 12 li a1, SATP_MODE a1 = 8,s atp的模式,3级页表 or a2, a2, a1 a2 = early_pg_dir >> 12 | 8 ⑤ 计算satp寄存器的值,mod为8代表3级页表,根目录页表的地址为early_pg_dir,这个页表在set_vm填充的页表,包括内核的镜像的粗粒度映射。这里只是先计算当前的值,但是先不写到satp寄存器。 /* * 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 srl a0, a0, PAGE_SHIFT trampoline_pg_dir >> 12 or a0, a0, a1 trampoline_pg_dir >> 12 | 8 sfence.vma csrw CSR_SATP, a0 satp = trampoline_pg_dir >> 12 | 8 ⑥ satp设置的页表是trampoline_pg_dir,页表trampoline_pg_di/trampoline_pmd在setup_vm中对PAGE_OFFSET~PAGE_OFFSET+2M的空间做了粗粒度映射。a0=trampoline_pg_dir >> 12 | 8,将a0的值写如到satp的那一刻,MMU就使能了,寻址的地址就需要经过MMU来翻译,因此PC下一条指令的取值地址,需要经过MMU翻译,但是下一条指令依旧是物理地址,MMU没有对应的页表就会陷入异常,具体过程如下: 0x802000f4 <_start+244> addi a0,a0,-240 0x802000f8 <_start+248> srli a0,a0,0xc 0x802000fa <_start+250> or a0,a0,a1 => 0x80200100 <_start+256> csrw satp,a0 0x80200104 <_start+260> auipc a0,0x0 0x80200108 <_start+264> addi a0,a0,104 0x8020010c <_start+268> csrw stvec,a0 0x80200110 <_start+272> auipc gp,0x880 0x80200114 <_start+276> addi gp,gp,1976 0x80200118 <_start+280> csrw satp,a2 如上的指令,PC运行到0x80200100,这句的指令就是向satp写入a0的值,执行完这条指令后就使能了MMU,后续的取指令地址都需要经过MMU翻译。当继续运行到下一条时PC=0x80200104地址,0x80200104需要经过MMU翻译,但是MMU查询不到页表就会进入异常,而跳转到异常的入口函数由上述④过程填充的1f(虚拟地址),因此下面的1f是异常处理的入口。 这里使用trampoline_pg_dir页表看起来是多余了,其实可以直接使用early_pg_dir,也做过实验把csrw CSR_SATP, a0 直接改为csrw CSR_SATP, a2,并把set_vm中关于trampoline_pg_dir的映射注释掉,系统也能正常起来,因此暂不清楚linux上游不把这个删除掉。 .align 2 1: ⑦这段代码是异常进入的入口,依旧使用虚拟地址取指运行。 /* Set trap vector to spin forever to help debug */ la a0, .Lsecondary_park csrw CSR_STVEC, a0 ⑧重新更新异常的入口为.Lsecondary_park /* 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 guarnteed to work * because that first superpage is translated the same way. */ csrw CSR_SATP, a2 sfence.vma ⑨ 重新更新satp=early_pg_dir >> 12 | 8 ret 当使能MMU时,下一条指令的取指就需要经过MMU进行翻译,但是下一条指令的地址依旧是物理地址,MMU查询不到对应的页表就会进入异常,而异常的入口实现使用了虚拟地址进行填充,虚拟地址的页表也在set_vm中进行了填充,因此虚拟地址是可以通过MMU查表找到物理地址,这样就从物理地址过渡到了虚拟地址的运行。 针对前面做个小结: 引导linux系统启动,需要解决的是物理地址切换到虚拟地址运行,而分界点就是写satp寄存器,使能MMU的那一刻。虚拟地址转化为物理地址,而关键点就是为MMU分配页表并填充好页表,而此时内存管理未初始化,又分配不了,所以总结下,就遇到了如下的问题。 6.内存管理没准备好。 7.需要分配页表。 8.开了MMU后,分配的页表能够用虚拟地址访问。 9.开了MMU后,要能够用虚拟地址访问内核。 10.开了MMU后,能够用虚拟地址访问设备树。 针对上面的问题对应的解决办法: 1.提前定义全局的静态数组用于做页表。 2.对内核vmlinux做粗粒度映射。 3.固定一段虚拟地址fixmap用于访问设备树、新分配的页表。 4.物理地址到虚拟地址的切换巧妙使用进异常进行转化。
- 
					
						  临时虚拟地址空间映射概述 为什么要做临时虚拟地址空间映射? 一旦开启MMU,PC的下一条指令地址会经过MMU转化,未开启MMU之前地址的翻译是不需要经过MMU转化直接访问。对应开启MMU之后,应该要使用虚拟地址,才能访问到正确的指令内存。 前面描述了虚拟地址转换为物理地址是通过MMU自动转换,但是需要给MMU创建好页表,这样MMU才能自动查询到物理地址。页表也是对应的物理内存,也是需要分配的,在正常系统运行时,页表的分配可以通过系统的内存管理接口获取到,但是在系统刚运行时,内存管理并没有初始化好,无法调用接口分配到页表,这样即使使能MMU但是找不到页表,也无法翻译出物理地址。 而临时虚拟地址空间映射就是要解决这样的问题,对于页表可以提前定义一个静态的全局数组来代替,填充好页表,使能MMU时,可以从物理地址访问过度到虚拟地址访问,等后期的内存初始化好后,可以分配页表了,再重新映射一遍。 在使能MMU后,要访问内核代码,又要访问设备树?要解决这个问题,内核会先做那些临时虚拟地址空间映射? (1)固定虚拟地址映射(fixmap):Linux内核分配了一段固定的虚拟地址范围,这段地址范围称为fixmap,专门用于映射到设备树,early console寄存器等使用,用于在使能MMU时,在未完成内存初始化时可以通过fixmap访问到设备树。 (2) 内核代码的映射(kernel):开启MMU后,内核代码的指令访问都是虚拟地址,所以需要马上进行映射。内核代码的映射了,虚拟地址范围在编译时就确定了,需要处理的是分配到页表进行填充,而各级页表使用的是事先先分配的全局数组,填充好各级页表即可。 (1)内核vmlinux被加载到内存的0x80200000的位置,DTB被加载到0x82200000的位置。 (2)fixmap这段地址编译时由FIXADDR_START和FIXADDR_TOP确定,这段虚拟地址是固定的,如上图例子在0xffffffcefee00000~0xffffffceff000000范围。 (3)kernel地址范围编译的时候确定,从PAGE_OFFSET开始,如上图是0xffffffe000000000开始,与readelf -h工具链查看的入口地址一致。 (4)两段虚拟地址共用了一个根目录页表early_pg_dir[512],因为对应3级页表根目录页表每个表项寻址范围1G,512个表项共计512G范围,是完全足够的。 (5)kernel虚拟地址使用的粗粒度映射,二级页目录为early_pmd[512],每个页表项2M寻址访问,10M的空间占用了5个页表项。 (6)fixmap虚拟地址目前主要用于映射到DTB,本身的映射范围有2M的空间,预留了1M的空间用于DTB,DTB使用的细粒度映射,二级页表使用的是fixmap_pmd[512],三级页表使用的是fixmap_pte[512]。 (7)上述涉及的4个页表都是定义的全局数组,位于vmlinux中被事先加载到物理内存中。 (8)fixmap的地址在PAGE_OFFSET下面,与kernel之间还有vmalloc和pci_io的地址空间。即FIXADDR_TOP= PCI_IO_START,PCI_IO_END=VMEMMAP_START,VMALLOC_END=PAGE_OFFSET - 1。 asmlinkage void __init setup_vm(uintptr_t dtb_pa) { uintptr_t va, end_va; uintptr_t load_pa = (uintptr_t)(&_start); uintptr_t load_sz = (uintptr_t)(&_end) - load_pa; uintptr_t map_size = best_map_size(load_pa, MAX_EARLY_MAPPING_SIZE); ①_start和_end分别为内核加载到的物理地址起始和结束位置。 va_pa_offset = PAGE_OFFSET - load_pa; pfn_base = PFN_DOWN(load_pa); ②va_pa_offset内核虚拟地址相对物理地址的偏移量,pfn_base是内核开始地址对应的pfn, 即叶帧号。 /* * Enforce boot alignment requirements of RV32 and * RV64 by only allowing PMD or PGD mappings. */ BUG_ON(map_size == PAGE_SIZE); /* Sanity check alignment and size */ BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0); BUG_ON((load_pa % map_size) != 0); BUG_ON(load_sz > MAX_EARLY_MAPPING_SIZE); /* Setup early PGD for fixmap */ create_pgd_mapping(early_pg_dir, FIXADDR_START, (uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE); ③填充FIXADDR_START虚拟地址根目录页表项,因为映射大小PGDIR_SIZE,所以只填充根目录页表项。页表项PFN指向下一级页表的物理地址,为fixmap_pgd_next,本章实验是3级页表,所以为fixmap_pmd[512],页表项的属性为PAGE_TABLE(0x1)。最终的填充内容就是上图 early_pg_dir[315]=(0x80a83000 >> 12 ) << 10 | 0x01 = 0x202a0c01,格式见3.3.2章节实际的物理地址需要右移12转化为PPN,然后再左移10 #ifndef __PAGETABLE_PMD_FOLDED /* Setup fixmap PMD */ create_pmd_mapping(fixmap_pmd, FIXADDR_START, (uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABLE); ④填充FIXADDR_START二级页表所在页表项,因为映射的地址大小时PMD_SIZE,所以只填充二级页表。页表项中指向的下一级页表为fixmap_pte[512],页表项属性为PAGE_TABLE。最终的填充内容就是上图 fixmap_pmd[503] = (0x80a85000 >> 12) << 10 | 0x01 = 0x202a1401。 经过③和④步骤,就将FIXADDR_START~FIXADDR_TOP这段虚拟地址空间的PGD/PMD页表填充好了,但是这段虚拟地址空间最终映射到那块物理地址了?最终会映射到DBT存在的那段物理地址,而DBT会在接下来再进行映射,所以了PTE页表项暂时先不填充,待到后续再进行填充。 /* Setup trampoline PGD and PMD */ create_pgd_mapping(trampoline_pg_dir, PAGE_OFFSET, (uintptr_t)trampoline_pmd, PGDIR_SIZE, PAGE_TABLE); create_pmd_mapping(trampoline_pmd, PAGE_OFFSET, load_pa, PMD_SIZE, PAGE_KERNEL_EXEC); ⑤ 这里还对PAGE_OFFSET~PAGE_OFFSET+2M的空间做了一个临时粗粒度映射,主要的目的是用于后续使能MMU时,能够无缝从物理地址切换到虚拟地址,以至于不要让访问的地址查找不到页表而进入异常。用到的根页表是trampoline_pg_dir[512]和PMD页表是trampoline_pmd[512]。trampoline_pg_dir看起来有点多余,因为接下来会对内核做映射,页表使用early_pg_dir和early_pmd_dir。 #else /* Setup trampoline PGD */ create_pgd_mapping(trampoline_pg_dir, PAGE_OFFSET, load_pa, PGDIR_SIZE, PAGE_KERNEL_EXEC); #endif /* * Setup early PGD covering entire kernel which will allows * us to reach paging_init(). We map all memory banks later * in setup_vm_final() below. */ end_va = PAGE_OFFSET + load_sz; for (va = PAGE_OFFSET; va < end_va; va += map_size) create_pgd_mapping(early_pg_dir, va, load_pa + (va - PAGE_OFFSET), map_size, PAGE_KERNEL_EXEC); ⑥对虚拟地址PAGE_OFFSET~end_va范围进行映射,使用early_pg_dir[512]作为根目录页表,由于映射的大小时map_size=0x200000;所以在填充根目录页表后,会继续向下遍历下一级PMD页表,填充到PMD页表时map_size=PMD_SIZE,页表项属性为PAGE_KERNEL_EXEC就不再往下遍历填充PTE页表了,这样就完成一个PGD/PMD页表的填充,达成一个PAGE_OFFSET~end_va地址范围的粗粒度映射,对应上图的10M空间。 /* Create fixed mapping for early FDT parsing */ end_va = __fix_to_virt(FIX_FDT) + FIX_FDT_SIZE; for (va = __fix_to_virt(FIX_FDT); va < end_va; va += PAGE_SIZE) create_pte_mapping(fixmap_pte, va, dtb_pa + (va - __fix_to_virt(FIX_FDT)), PAGE_SIZE, PAGE_KERNEL); ⑦__fix_to_virt(FIX_FDT)~__fix_to_virt(FIX_FDT) + FIX_FDT_SIZE这段虚拟地址空间映射到DBT物理空间,这段虚拟地址空间在FIXMAP范围内,由于FIXMAP的虚拟地址空间已经填充好了PGD/PMD页表项,要映射到DTB只需要填充PTE页表项目即可,所以这里调用的是create_pte_mapping填充PTE页表。 /* Save pointer to DTB for early FDT parsing */ dtb_early_va = (void *)fix_to_virt(FIX_FDT) + (dtb_pa & ~PAGE_MASK); /* Save physical address for memblock reservation */ dtb_early_pa = dtb_pa; ⑧dtb_early_va保存的就是dtb起始的虚拟地址,dtb_early_pa是dtb的其实物理地址。 } 页表填充 所谓映射,实际上分配页表,然后填充页表项。那么页表的填充涉及到3个函数,分别对应的就是PGD/PMD/PTE页表项目的填充。 static void __init create_pgd_mapping(pgd_t *pgdp, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot) -pgd:页表的地址 -va:映射的虚拟地址 -pa:当sz等于PGDIR_SIZE/PMD_SIZE时,pa就是下一级页表的物理地址。否则就是实际的物理地址。PGD页表项的映射地址范围,3级页表是1G,PMD页表项的映射地址范围,3级页表是2M。 -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) static void __init create_pte_mapping(pte_t *ptep, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot) 根目录页表填充 static 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_index = pgd_index(va); ① 将虚拟地址转化为页表的index,((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1) if (sz == PGDIR_SIZE) { if (pgd_val(pgdp[pgd_index]) == 0) pgdp[pgd_index] = pfn_pgd(PFN_DOWN(pa), prot); return; } ② 如果sz为PGDIR_SIZE,pa代表的就是下一级页表基地址,就仅填充PGD页表项,不再向下遍历填充下一级页表。先判断pgd_val(pgdp[pgd_index])对应页表项是否填充过,如果已经填充过就直接返回。未填充过就把(pa >> 12) << 10 | prot写到表项中,见3.3.2章节页表项格式,其中pa>>12是计算ppn。 ③ 如果sz不等于PGDIR_SIZE,那么除了要填充PGD页表项外,还有向下遍历填充下一级页表。 if (pgd_val(pgdp[pgd_index]) == 0) { next_phys = alloc_pgd_next(va); pgdp[pgd_index] = pfn_pgd(PFN_DOWN(next_phys), PAGE_TABLE); nextp = get_pgd_next_virt(next_phys); memset(nextp, 0, PAGE_SIZE); ④ 对应的页表项==0,表示未填充过。先使用alloc_pgd_next分配下一级页表,因为要得到下一级页表的物理地址才能填充。如果是3级页表alloc_pgd_next=alloc_pmd,如果是2级页表alloc_pdg_next=alloc_pte。这里是alloc_pmd,由于memblcok也还未初始化,这里直接就使用的是early_pmd[]数组。 分配到下一级页表后,就将下一级页表的地址转化为PPN再与页表属性相或赋值到pgdp[]中就完成了页表项的填充。这里的页表属性为PAGE_TABLE,其值为V位为1,其他都为0,表示非子叶页表。 } else { next_phys = PFN_PHYS(_pgd_pfn(pgdp[pgd_index])); nextp = get_pgd_next_virt(next_phys); ⑤如果已经填充了页表项,那么从页表项中获取到下一级页表的物理地址。获取到下一级页表的物理地址后,需要调用get_pgd_next_virt将其转化为虚拟地址。因为若是开了MMU之后,使用的是虚拟地址,否则访问不到页表的内存。见下描述。 } create_pgd_next_mapping(nextp, va, pa, sz, prot); ⑥填充完PGD页表项后,需要接着再往下填充下一级页表。其中nextp为下一级页表的地址,可能是物理地址也有可能是虚拟地址。 } static phys_addr_t __init alloc_pmd(uintptr_t va) { uintptr_t pmd_num; if (mmu_enabled) return memblock_phys_alloc(PAGE_SIZE, PAGE_SIZE); ① 如果使能mmu,就调用memblock_phys_alloc分配内存。 pmd_num = (va - PAGE_OFFSET) >> PGDIR_SHIFT; BUG_ON(pmd_num >= NUM_EARLY_PMDS); return (uintptr_t)&early_pmd[pmd_num * PTRS_PER_PMD]; ② 如果未使能mmu,页表就是使用静态的early_pmd[xxx]。 } static pmd_t *__init get_pmd_virt(phys_addr_t pa) { if (mmu_enabled) { clear_fixmap(FIX_PMD); return (pmd_t *)set_fixmap_offset(FIX_PMD, pa); ① 如果使能了mmu,将物理地址反向映射到fixmap的空间,也就是FIX_PMD这个虚拟地址再使能mmu后访问到pa,即FIX_PMD与pa做了虚拟地址到物理地址的映射。 } else { return (pmd_t *)((uintptr_t)pa); ② 如果没有使能mmu,直接返回物理地址即可。 } } PMD页表填充 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_index = pmd_index(va); 如果sz=PMD_SIZE,不再填充PTE页表,可能是粗粒度映射,也有可能是fixmap的填充。 if (sz == PMD_SIZE) { if (pmd_none(pmdp[pmd_index])) pmdp[pmd_index] = pfn_pmd(PFN_DOWN(pa), prot); return; } if (pmd_none(pmdp[pmd_index])) { pte_phys = alloc_pte(va);//分配下一级页表PTE pmdp[pmd_index] = pfn_pmd(PFN_DOWN(pte_phys), PAGE_TABLE);//填充pmd页表项 ptep = get_pte_virt(pte_phys); //将下一级页表的物理地址转化为虚拟地址,当使能MMU时,使用的是虚拟地址。 memset(ptep, 0, PAGE_SIZE); //将下一级页表的PTE清空。 } else { pte_phys = PFN_PHYS(_pmd_pfn(pmdp[pmd_index])); ptep = get_pte_virt(pte_phys); //获取下一级页表的虚拟地址。 } create_pte_mapping(ptep, va, pa, sz, prot); } pmd页表项的填充与PGD页表项的填充逻辑类似,当sz为PMD_SIZE时,传进来的pa就是下一级页表或者就是映射的物理地址(粗粒度映射的时候)。 是否是粗粒度映射主要看PMD页表项的属性值,当属性值XRW权限为可读可写可执行,那么该页表项指向的就是实际的物理地址而不是PTE页表的基地址。 PTE页表填充 static void __init create_pte_mapping(pte_t *ptep, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot) { uintptr_t pte_index = pte_index(va); BUG_ON(sz != PAGE_SIZE); if (pte_none(ptep[pte_index])) ptep[pte_index] = pfn_pte(PFN_DOWN(pa), prot); } PTE页表是最后一级页表,因此填充的逻辑就比较简单了,计算pa所在的PPN,再左移动10位或上prot,赋值到对应的表项即可。 Fixmap映射 Fixmap是一段固定的虚拟地址空间,使能MMU后通过这段虚拟空间先访问到物理空间,具体有那些模块可以看看下面数据结构。 enum fixed_addresses { FIX_HOLE, #define FIX_FDT_SIZE SZ_1M FIX_FDT_END, FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1, FIX_PTE, FIX_PMD, FIX_EARLYCON_MEM_BASE, __end_of_fixed_addresses }; 从上可知,fixmap的地址空间主要分类有4种 - FIX_FDT: 用于映射到DBT的,范围为FIX_FDT~FIX_END,通过这段地址访问到设备树。 - FIX_PTE: 用于映射pte页表,范围为FIX_PTE~FIX_PTE+4K,即一个页大小。场景是当使能了MMU后,对于页表的访问也先需要通过虚拟地址访问,因此FIX_PTE就是对应页表所在物理内存的虚拟地址。 - FIX_PMD: 用于映射pmd页表,范围为FIX_PMD~FIX_PMD+4K,即一个页大小。用于读写pmd页表的物理内存。 - FIX_EARLYCON_MEM_BASE: 用于映射到earlycon。 几个地址如何保证落在FIXADDR_START~FIXADDR_TOP了,先来看看在映射DBT时的实现: /* Create fixed mapping for early FDT parsing */ end_va = __fix_to_virt(FIX_FDT) + FIX_FDT_SIZE; for (va = __fix_to_virt(FIX_FDT); va < end_va; va += PAGE_SIZE) create_pte_mapping(fixmap_pte, va, dtb_pa + (va - __fix_to_virt(FIX_FDT)), PAGE_SIZE, PAGE_KERNEL); 可以看在映射对FIX_FDT~FIX_FDT+ FIX_FDT_SIZE的范围调用__fix_to_virt函数进行了转化,转化的地址是由FIXADDR_TOP向下相减,因此FIX_FDT转化的地址一定会落在FIXADDR_START~FIXADDR_TOP范围。 #define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT)) #define __virt_to_fix(x) ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT) Fixmap映射过程主要调用了create_pgd_mapping/create_pmd_mapping/create_pte_mapping 3个函数填充对应的PGD/PMD/PTE页表,由于内存管理还没有准备好,所以页表使用的是全局的静态数组,如下: pmd_t early_pmd[PTRS_PER_PMD * NUM_EARLY_PMDS] __initdata __aligned(PAGE_SIZE); pmd_t fixmap_pmd[PTRS_PER_PMD] __page_aligned_bss; pte_t fixmap_pte[PTRS_PER_PTE] __page_aligned_bss; 上述除了FIX_FDT的范围由FIX_FDT_SIZE决定外,其他的FIX_PTE、FIX_PMD、EARLYCON都是4K范围。 fixmap初始化 asmlinkage void __init setup_vm(uintptr_t dtb_pa) { /* Setup early PGD for fixmap */ create_pgd_mapping(early_pg_dir, FIXADDR_START, (uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE); ③填充FIXADDR_START虚拟地址根目录页表项,因为映射大小PGDIR_SIZE,所以只填充根目录页表项。页表项PFN指向下一级页表的物理地址,为fixmap_pgd_next,本章实验是3级页表,所以为fixmap_pmd[512],页表项的属性为PAGE_TABLE(0x1)。最终的填充内容就是上图 early_pg_dir[315]=(0x80a83000 >> 12 ) << 10 | 0x01 = 0x202a0c01,格式见3.3.2章节实际的物理地址需要右移12转化为PPN,然后再左移10 /* Setup fixmap PMD */ create_pmd_mapping(fixmap_pmd, FIXADDR_START, (uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABLE); ④填充FIXADDR_START二级页表所在页表项,因为映射的地址大小时PMD_SIZE,所以只填充二级页表。页表项中指向的下一级页表为fixmap_pte[512],页表项属性为PAGE_TABLE。最终的填充内容就是上图 fixmap_pmd[503] = (0x80a85000 >> 12) << 10 | 0x01 = 0x202a1401。 经过③和④步骤,就将FIXADDR_START~FIXADDR_TOP这段虚拟地址空间的PGD/PMD页表填充好了,但是这段虚拟地址空间最终映射到那块物理地址了?最终会映射到DBT存在的那段物理地址,而DBT会在接下来再进行映射,所以了PTE页表项暂时先不填充,待到后续再进行填充。 } fixmap的映射是对3个全局数组的填充,事先会填充好PGD/PMD,而PTE页表根据是那个模块使用再进行填充,具体的填充过程在上一章节已经描述,这里就不再赘述。 DTB的映射 asmlinkage void __init setup_vm(uintptr_t dtb_pa) { ........ /* Create fixed mapping for early FDT parsing */ end_va = __fix_to_virt(FIX_FDT) + FIX_FDT_SIZE; for (va = __fix_to_virt(FIX_FDT); va < end_va; va += PAGE_SIZE) create_pte_mapping(fixmap_pte, va, dtb_pa + (va - __fix_to_virt(FIX_FDT)), PAGE_SIZE, PAGE_KERNEL); ⑦__fix_to_virt(FIX_FDT)~__fix_to_virt(FIX_FDT) + FIX_FDT_SIZE这段虚拟地址空间映射到DBT物理空间,这段虚拟地址空间在FIXMAP范围内,由于FIXMAP的虚拟地址空间已经填充好了PGD/PMD页表项,要映射到DTB只需要填充PTE页表项目即可,所以这里调用的是create_pte_mapping填充PTE页表。 /* Save pointer to DTB for early FDT parsing */ dtb_early_va = (void *)fix_to_virt(FIX_FDT) + (dtb_pa & ~PAGE_MASK); /* Save physical address for memblock reservation */ dtb_early_pa = dtb_pa; ⑧dtb_early_va保存的就是dtb起始的虚拟地址,dtb_early_pa是dtb的其实物理地址。 ........ } PMD与PTE页表映射 页表也是存储在物理内存中的,分配页表可以是静态的全局数组也也可使用内存分配接口获取到。分配到页表的物理内存后需要进行填充,而使能mmu后访问物理内存必现要使用虚拟地址,填充页表是为了做虚拟地址转化,访问页表又要虚拟地址,那如何解决这个问题了?Fixmap的fix_pte和fix_pmd就排上用场了,在fixmap固定的虚拟地址空间访问,前面已经填充了PGD/PMD,剩下再填充好PTE就可以转化到相应物理地址,因此当新分配的页表要访问时,先将其物理地址填充到fixmap对应的pte表项中,这样就可以通过fixmap访问到页表了。 如上图所示,其中fix_pte和fix_pmd就是专门用来访问页表的,fix_pte专门用于访问pte类型页表,fix_pmd专门用于访问pmd类型页表,页表分配到空间后。 下面来看看实际的应用场景,以create_pmd_mapping示例说明。 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_index = pmd_index(va); if (sz == PMD_SIZE) { if (pmd_none(pmdp[pmd_index])) pmdp[pmd_index] = pfn_pmd(PFN_DOWN(pa), prot); return; } if (pmd_none(pmdp[pmd_index])) { pte_phys = alloc_pte(va); ① 分配pte页表,这时候使用能mmu,分配pte页表调用memblock_phys_alloc进行分配,返回的是页表的物理地址。 pmdp[pmd_index] = pfn_pmd(PFN_DOWN(pte_phys), PAGE_TABLE); ptep = get_pte_virt(pte_phys); ②因为接下来要调用create_pte_mapping填充pte页表,即①分配的页表,但是此时已经使能了mmu,如果传递的页表基地址是物理内存,将无法访问到PTE页表,所以要对其转化给虚拟地址。 memset(ptep, 0, PAGE_SIZE); } else { pte_phys = PFN_PHYS(_pmd_pfn(pmdp[pmd_index])); ptep = get_pte_virt(pte_phys); } create_pte_mapping(ptep, va, pa, sz, prot); } 接下来就看看get_pte_virt的实现。 static pte_t *__init get_pte_virt(phys_addr_t pa) { if (mmu_enabled) { clear_fixmap(FIX_PTE); return (pte_t *)set_fixmap_offset(FIX_PTE, pa); ①使能了MMU,先调用clear_fixmap将对应的FIX_PTE的页表项都清除掉。接着调用set_fixmap_offset获取虚拟地址。 } else { return (pte_t *)((uintptr_t)pa); } } /* Return a pointer with offset calculated */ #define __set_fixmap_offset(idx, phys, flags) \\\\ ({ \\\\ unsigned long ________addr; \\\\ __set_fixmap(idx, phys, flags); \\\\ ________addr = fix_to_virt(idx) + ((phys) & (PAGE_SIZE - 1)); \\\\ ________addr; \\\\ }) #define set_fixmap_offset(idx, phys) \\\\ __set_fixmap_offset(idx, phys, FIXMAP_PAGE_NORMAL) ② 调用__set_fixmap将物理地址填充到FIX_PTE对应的PTE页表项目,最终的目的是要让FIX_PTE虚拟地址与当前新分配的页表pa映射起来。对于本章实验示例填充的4.3节图中PTE表项的fixmap_pte[0xff]。 void __set_fixmap(enum fixed_addresses idx, phys_addr_t phys, pgprot_t prot) { unsigned long addr = __fix_to_virt(idx); ③ __fix_to_virt 是获取FIX_PTE的虚拟地址,使其落在fixmap的范围内。 pte_t *ptep; BUG_ON(idx <= FIX_HOLE || idx >= __end_of_fixed_addresses); ptep = &fixmap_pte[pte_index(addr)]; ④ pte_index(addr)计算的是FIX_PTE在fixmap_pte[]数组的偏移,也就是页表项的位置。 if (pgprot_val(prot)) { set_pte(ptep, pfn_pte(phys >> PAGE_SHIFT, prot)); ⑤ 将新分配的页表物理地址phys与prot计算设置到表项中。这样访问FIX_PTE就可以访问到新分配的页表了,就可以进行填充操作了。 } else { pte_clear(&init_mm, addr, ptep); local_flush_tlb_page(addr); } } 粗粒度内核映射 asmlinkage void __init setup_vm(uintptr_t dtb_pa) { uintptr_t va, end_va; uintptr_t load_pa = (uintptr_t)(&_start); uintptr_t load_sz = (uintptr_t)(&_end) - load_pa; uintptr_t map_size = best_map_size(load_pa, MAX_EARLY_MAPPING_SIZE); ①_start和_end分别为内核加载到的物理地址起始和结束位置。 va_pa_offset = PAGE_OFFSET - load_pa; pfn_base = PFN_DOWN(load_pa); ②va_pa_offset内核虚拟地址相对物理地址的偏移量,pfn_base是内核开始地址对应的pfn, 即叶帧号。 /* * Enforce boot alignment requirements of RV32 and * RV64 by only allowing PMD or PGD mappings. */ BUG_ON(map_size == PAGE_SIZE); /* Sanity check alignment and size */ BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0); BUG_ON((load_pa % map_size) != 0); BUG_ON(load_sz > MAX_EARLY_MAPPING_SIZE); ...... /* * Setup early PGD covering entire kernel which will allows * us to reach paging_init(). We map all memory banks later * in setup_vm_final() below. */ end_va = PAGE_OFFSET + load_sz; for (va = PAGE_OFFSET; va < end_va; va += map_size) create_pgd_mapping(early_pg_dir, va, load_pa + (va - PAGE_OFFSET), map_size, PAGE_KERNEL_EXEC); ⑥对虚拟地址PAGE_OFFSET~end_va范围进行映射,使用early_pg_dir[512]作为根目录页表,由于映射的大小时map_size=0x200000;所以在填充根目录页表后,会继续向下遍历下一级PMD页表,填充到PMD页表时map_size=PMD_SIZE,页表项属性为PAGE_KERNEL_EXEC就不再往下遍历填充PTE页表了,这样就完成一个PGD/PMD页表的填充,达成一个PAGE_OFFSET~end_va地址范围的粗粒度映射,对应上图的10M空间。 }
- 
					
						  虚拟地址与物理地址概念地址空间 虚拟地址:程序使用的内存地址;物理地址:硬件的地址空间。虚拟地址通过MMU转化为物理地址,虚拟地址的长度与实际的物理内存容量没有关系,从系统中每个进程的角度看,地址空间的进程无法感知其他进程的存在。 32位cpu处理的地址空间为2^32=4G,所以虚拟地址空间为4G,分为用户空间和内核空间。用户空间的范围0~TASK_SIZE(可配置)通常为0x00000000~0xBFFFFFFF,内核空间的地址范围0xC0000000~0xFFFFFFF。由于地址空间虚拟化的结果,每个用户进程都认为自身有3G内存,而内核空间总是同样的,无论当前执行的是那个进程。 64位的cpu寻址空间为2^64,但实际只使用了47位,即寻址范围位2^48=256TB,多出的高16位就形成了地址空洞。内核空间的高16位全为1,用户空间的高16位全为0,可以直接方便判断是用户空间地址还是内核空间地址。 虚拟地址空间和物理内存被划分为很多等长的部分,称之为页,物理内存页被称为页帧。 进程1的页3和进程2的页1同时指向了物理页帧3,这种情况是可能的,因为两个虚拟地址空间的页可以映射到同一物理内存页,内核负责将虚拟地址空间映射到物理地址空间,可以决定哪些内存区域在进程之间共享,那些不共享。 进程的虚拟地址空间不是所有的页都映射到物理内存页帧上,因为没有这么大的一个物理内存供所有进程都一一映射完(一个进程3G,10个进程就是30G),因此只需要将当前使用的页进行映射(加载到内存中)。主要分为一些地址空间完全没有使用,另外就是有一些页被暂时换出到磁盘上,需要的时候再换回。当程序执行虚拟地址在经过MMU转化时,找不到页表无法查询不到物理地址,将运行缺页异常处理。 页表 实现虚拟地址和物理地址两个地址空间的映射管理最容易的方法是使用数组,对虚拟地址空间的每一页,都分配一个数组项,该数组项指向与之关联的页帧。虚拟地址空间经过查询获得物理地址的数据结构称为页表。 虚拟地址经过MMU查询页表,页表中存储了映射的物理地址,查询的过程由硬件MMU自动执行,但是需要事先分配一个页表并填充好页表项目,把页表的地址给到MMU。 二级页表 先解释下什么是PFN:是page frame number的缩写,所谓page frame,就是针对物理内存而言的,把物理内存分成一个个的page size的区域,并且给每一个page 编号,这个编号就是PFN。 假设物理内存从0地址开始,那么PFN等于0的那个页帧就是0地址(物理地址)开始的那个page。 寻址转化过程 把虚拟地址分为3部分,PGD+PT+Offset组成,而最终的物理地址为PFN+Offset。虚拟地址转化为物理地址是硬件MMU自动行为,查询的最终目的是要找到虚拟地址对应的PFN。这里先不讨论页表的填充过程,默认页表已经填充好,转化的具体过程如下: (1)一级页表的地址会被存储在satp寄存器中,因此通过获取satp寄存器就可以获取到一级页表,取虚拟地址的[31:20]的值计算出二级页表物理地址所在的表项偏移,这里为0x600(1536),即二级页表的物理地址存储在一级页表的第1536表项中,进而查询到二级页表的物理地址。 (2)取虚拟地址的[19:12]的值计算出PFN所在表项的位置,这里是0x3,所以PFN的值存储在二级页表的第3表项中,这样就获取到了PFN。 (3)最终的物理地址就是 PFN << 12 | Offset。 页表项中存储的地址是虚拟地址还是物理地址?是物理地址。 为什么要使用多级页表? (1)一级页表 假设32位系统,以4KB物理页为映射单位,一个进程4GB的虚拟地址空间需要4GB/4KB=1MB个页表项来做映射。每个页表项占用4字节,那么一级页表目录就需要4 * 1MB = 4MB的存储空间,那么对应该进程就需要4MB的内存来存储页表,那100个进程需要400MB。 (2)二级页表 一级页表PGD: 有4096个页表项(PGD [31:20],2^12),每个页表项4字节,一共16KB。 二级页表PTE:一共4096个PTE页表,每个二级页表包含256个页表项(PTE[19:12],2^8),大小为1KB(2564字节),所有的二级页表大小一共是4K1KB=4MB。 一个进程需要4MB+16KB的内存来存储页表。一个页表中有多少个页表项,根据当前PGD/PTE等的位数来决定。 根据linux内核局部性原理,不是所有的地址空间都需要映射到物理内存,所以二级页表实际的页表占用内存大小为(16KB+1KBn)m,其中n表示PTE页表的个数,m表示进程的数目。 因此对比一级页表,一个进程最少需要4MB的空间,而二级页表最少需要17KB的空间。 五级页表 PGD: page global directory,页全局目录,用于索引L2的表项,512个表项。 P4D: page four directory,页四级目录,用于索引L3的表项,512个表项,一个表项512G寻址范围。 PUD: page upper directory,页上级目录,用于索引L4的表项,512个表项,一个表项1G寻址范围。 PMD:page middle directory,页中间目录,用于索引L5的表现,512个表项,一个表项2M寻址范围。 PT:page table,直接页表,用于获取PFN的表项,512个表项,1个表项4K寻址范围。 linux 4.11之后,页表拓展到5级,在页全局目录和页上级目录之间增加了页四级目录。各个处理器架构可以选择5级、4级、3级、2级。页表级数越多,存储空间越小,主要是利用了内存管理的局部性原理,但是相应的硬件结构越复杂,访问的速度也会降低。针对访问速度的问题,CPU试图通过以下两种方法加速其转换过程。 (1)CPU中有一个专门的部分MMU(Memory Management Unit,内存管理单元),该单元优化了内存访问的操作。(通过硬件来负责转化,所有的页表都是存储在物理内存中,由软件按照要求先填写好,然后给一级页表的起始地址给TTBRx,后续虚拟地址到物理地址的转换就由MMU自动完成) (2)地址转换中出现最频繁的那些地址,保存到地址转换缓冲器中(TLB,Translation Lookaside Buffer),下次访问直接从缓存器中获得地址数据,不在进行地址转换。 块页表 上两小结描述都是页表类型的页表项,映射的页面大小一般为4KB。还有一种块类型的页表项,映射的范围大块内存,块类型的页表项包含的是大块物理内存的基地址,如1GB,2MB等大块内存。 如上图,PMD已经为子页表项,没有PT页表,PMD页表项中存储的就是PFN,只不过这种类型的PFN页面大小是2M,相对于4KB的页面大小,这种页面粒度较大。在系统刚初始化时,会使用这种粗粒度的方式,而最终正常运行时会使用小粒度,毕竟如果时粗粒度,每次分配的最小单元时2M,内存就会浪费。 RISC-V MMU RISC-V的MMU,功能主要有:地址转换、页面保护、页面属性管理。 - 地址转换:将虚拟地址转换位物理地址 - 页面保护:通过对页面的访问者读写执行权限检查来进行保护 - 页面属性管理:扩展地址属性位,根据访问地址,获取原页面对应的属性,提供系统进一步使用。 在RISC-V体系结构中,根据处理器虚拟地址的位宽,提供了多种地址转换机制。 SATP地址转化寄存器 在RISC-V中有一个重要的SATP( Supervisor Address Translation and Protection Register)寄存器,称为MMU地址转换寄存器,一级页表的地址从该寄存器中获取得到,寄存器如下图: PPN:存储了第一级页表基地址的页帧号,页表物理地址=PPN*PAGE SIZE。 ASID:进程地址空间标识符,用于优化TLB的。 MODE:用来选择地址转换的模式,多少级页表,如下图所示,当mode=9时,位Sv48模式。 Sv39类型页表 页表的大小为4KB,Sv39页表包含了512个(2^9)页表项(PTEs),每个页表项8个字节(64 bit)。因为页表大小正好是4KB,所以要获取页表的地址只有获取到PPN就行,而根页表的PPN( physical page number)存储在satp寄存器的PPN域。 页表项的格式如上图4.21所示,[9:0]用于描述页表项的属性。 - V:指示页表项是否有效,0表示无效,1表示有效。 - R:指示页表项对应页面可读权限,0表示不可读,1表示可读。 - W:页面的可写权限 - X:页面的可执行权限 - U:用户模式下的访问权限 - G:全局映射,用于TLB。 - A:处理器是否访问过页面 - D:脏页,是否被修改过。 - RSW:预留给管理员使用。 - PPN[2]:PPN[0]:下一级页表或物理地址的页帧号。 - PBMT:映射页面的内存属性,0-无;1-普通内存;2-I/O内存;3-保留。 页表项类型分为非子叶页表类型和子叶页表类型,非子叶页表类型表示指向的是下一级页表,对应页表属性值[3:1]XWR为0;子叶页表类型表示指向的是实际的物理内存,对应页表属性值[3:1]XWR不为0; Sv39地址转化过程 (1)根据SATP.PPN获取到一级页表的基地址。 (2)根据PGD[38:30]的值定位到表项index,计算出目标表项的物理地址。 (3)获取一级页表中的目标表项的值,判断[9:0]相关权限属性,若都合法取出PPN[2]:PPN[0]即为二级页表的物理基地址。 (4)根据PMD[29:21]获取到第三级页表的物理基地址。 (5)根据PT[20:12]查询在第三级页表项中获取到最终的PFN。 (6)最终的物理地址PFN<<12 | Offset。 上面的整个过程软件只需要填充好各级页表,并将根目录页表写到satp中。整个翻译过程由硬件MMU自动完成,下图是硬件实现结构图。
- 
					
						  arch初始化_start_kernel: /* Mask all interrupts */ csrw CSR_SIE, zero csrw CSR_SIP, zero ① 将sie,sip寄存器设置为0,关闭所有中断和清除中断的pending(不是异常)。 /* Load the global pointer */ .option push .option norelax la gp, __global_pointer$ .option pop /* * Disable FPU to detect illegal usage of * floating point in kernel space */ li t0, SR_FS csrc CSR_SSTATUS, t0 ②关闭掉浮点,浮点至少在内核空间才会使用 #ifdef CONFIG_SMP li t0, CONFIG_NR_CPUS bgeu a0, t0, .Lsecondary_park ③如果使能了SMP,硬件的实际hart数量超过配置的数量部分直接进入 第二阶段,直接进入WFI模式。 #endif /* Pick one hart to run the main boot sequence */ la a3, hart_lottery //hart_lottery是一个全局变量,刚开始为0 li a2, 1 amoadd.w a3, a2, (a3)//将a3地址的值赋值为a3,将a2的值加到a3地址处 bnez a3, .Lsecondary_start //判断a3的值是否为0,如果不为0,跳转 ④彩票机制,挑选一个hart作为主hart运行,其他hart跳转到.Lsecondary_start 彩票机制是最先运行到该代码的hart对全局变量hart_lottery+1,后续hart运行时 检测到全局变量hart_lottery不为0时都需要跳转。 /* Clear BSS for flat non-ELF images */ la a3, __bss_start la a4, __bss_stop ble a4, a3, clear_bss_done clear_bss: REG_S zero, (a3) add a3, a3, RISCV_SZPTR blt a3, a4, clear_bss clear_bss_done: ⑤ 向BSS段写0 /* Save hart ID and DTB physical address */ mv s0, a0 //hart ID mv s1, a1 //DTB phy address la a2, boot_cpu_hartid REG_S a0, (a2) ⑥ 将hart id 保存到变量boot_cpu_hartid /* Initialize page tables and relocate to virtual addresses */ la sp, init_thread_union + THREAD_SIZE //初始化一个临时栈空间,接下来会调用c函数 mv a0, s1 call setup_vm //调用setup_vm,传递参数为DTB的物理地址 la a0, early_pg_dir call relocate //调用relocate,传递参数为early_pg_dir
- 
					
						  Linux系统RISC-V架构启动流程分析之概述典型的linux系统启动流程如上,但本文主要探讨的是OS的启动流程,opensbi,uboot暂不涉及。主要围绕arch/riscv/kernel/head.S进行分析。 _start j _start_kernel _start_kernel arch init //关中断,关浮点检测,挑选一个主hart启动初始化序列 clear bss//清除BSS setup_vm//为打开MMU做准备,fixmap映射,内核粗粒度映射,fdt映射 fixmap mapping trampoline mapping early Kernel mapping 映射的页表:粗粒度映射,只使用到PMD,不会用到PTE early_pg_dir[PTRS_PER_PGD] trampoline_pmd[PTRS_PER_PMD] early_pmd[PTRS_PER_PMD * NUM_EARLY_PMDS] fixed mapping for fdt 虚拟地址是FIX_FDT, 将DBT的物理地址到填充fixmap_pte[PTRS_PER_PTE] Relocate 使能MMU,切换到虚拟地址运行 parse_dtb //解析设备树 early_init_dt_scan start_kernel start_kernel setup_arch setup_bootmem paging_init() trap_init //异常初始化 mm_init //kernel 内存初始化 sched_init //调度初始化 early_irq_init //irq初始化 init_IQR tick_init init_times //timer初始化 hrtimer_init softirq_init time_init arch_call_rest_init rest_init pid = kernel_thread(kernel_init, NULL, CLONE_FS); kernel_init kernel_init_freeable(); prepare_namespace mount_root() //挂载根文件系统 run_init_process(execute_command) //启动第一个应用进程 系统初始化的时候,有一个关键环节是使能MMU,而使能MMU就需要使用虚拟地址,那么就会遇到以下问题,如何解决了? 1.内存管理没准备好。 2.需要分配页表。 3.开了MMU后,分配的页表能够用虚拟地址访问,否则访问不了页表无法填充。 4.开了MMU后,要能够用虚拟地址访问内核代码,无法无法运行。 5.开了MMU后,能够用虚拟地址访问设备树,无法读取内核内存的相关信息。
- 
					
						  Linux risc-v head.S调试记录调试准备 gdb调试 0x0000000080200000为opensbi跳转执行的物理地址,linux内核的img会加载到这个地址运行,如果要在这个地方断点,就b *(0x0000000080200000),没法对head.S中进行断点,因为linux加载后会变成虚拟地址。调试的符号需要把CONFIG_DEBUG_INFO=y打开。 li s4,-13 j _start_kernel //j 0x802010cc _start_kernel: //关闭所有的中断 csrw sie,zero csrw sip,zero //加载gp指针 la gp, __global_pointer$ //关掉浮点 li t0, SR_FS csrc CSR_STATUS, t0 //挑选一个主核来运行初始化的代码,其他核跳转到Lsecondary_park等待(进入wfi) li t0, CONFIG_NR_CPUS blt a0, t0, .Lgood_cores tail .Lsecondary_park .Lgood_cores: //低电量检查? la a3, hart_lottery li a2,1 amoadd.w a3,a2, (a3) bnez a3, .Lsecondary_start 直接使用物理地址断点 调试linux CONFIG_DEBUG_INFO=y b *(0x0000000080200000) file linux-5.18/vmlinux linux内核在没使能mmu前的代码如何打断点? 只能打代码所在的物理地址如b *(0x0000000080200000) 怎么确定代码的符号的物理地址? info address setup_vm 获得符号的虚拟地址 riscv64-unknown-linux-gnu-readelf -h vmlinux 获取内核镜像的入口虚拟地址 获取内核镜像的加载所在物理地址 目标符号物理地址 = 符号虚拟地址-镜像入口虚拟地址 + 镜像加载的物理地址 info address setup_vm Symbol "setup_vm" is a function at address 0xffffffff80804a82. 目标物理地址=0xffffffff80804a82-0xffffffff80000000+0x0000000080200000 b *(0x80A04A82) 符号断点 riscv64-unknown-linux-gnu-readelf -h vmlinux 获取内核镜像的入口虚拟地址,然后使用riscv64-unknown-linux-gnu-readelf -S vmlinux获取各个段相对起始段的偏移,各个段的偏移+镜像加载的物理地址即是符号的地址,最后使用add-symbol-file添加符号。 riscv64-unknown-linux-gnu-readelf -S vmlinux There are 38 section headers, starting at offset 0xb3cc860: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .head.text PROGBITS ffffffff80000000 00001000 0000000000001e90 0000000000000000 AX 0 0 4096 [ 2] .text PROGBITS ffffffff80002000 00003000 000000000065bdb0 0000000000000000 AX 0 0 4 [ 3] .init.text PROGBITS ffffffff80800000 00800000 0000000000035132 0000000000000000 AX 0 0 2097152 [ 4] .exit.text PROGBITS ffffffff80835138 00835138 0000000000001996 0000000000000000 AX 0 0 2 [ 5] .init.data PROGBITS ffffffff80a00000 00837000 0000000000014e38 0000000000000000 WA 0 0 4096 [ 6] .data..percpu PROGBITS ffffffff80a15000 0084c000 00000000000084f8 0000000000000000 WA 0 0 64 [ 7] .alternative PROGBITS ffffffff80a1d4f8 008544f8 0000000000000288 0000000000000000 A 0 0 1 [ 8] .rodata PROGBITS ffffffff80c00000 00855000 0000000000195710 0000000000000000 WA 0 0 64 如.text段相对偏移为ffffffff80002000-ffffffff80000000=0x2000,因此符号添加位置为 0x80200000 +0x2000=0x80202000 (gdb) add-symbol-file linux-5.18/vmlinux -s .head.text 0x80200000 -s .text 0x80202000 -s .rodata 0x80e00000 -s .init.text 0x80a00000 -s .init.data 0x80c00000 add symbol table from file "linux-5.18/vmlinux" at .head.text_addr = 0x80200000 .text_addr = 0x80202000 .rodata_addr = 0x80e00000 .init.text_addr = 0x80a00000 .init.data_addr = 0x80c00000 (y or n) y Reading symbols from linux-5.18/vmlinux... struct kernel_mapping { unsigned long page_offset; unsigned long virt_addr; uintptr_t phys_addr; uintptr_t size; /* Offset between linear mapping virtual address and kernel load address */ unsigned long va_pa_offset; /* Offset between kernel mapping virtual address and kernel load address */ unsigned long va_kernel_pa_offset; unsigned long va_kernel_xip_pa_offset; #ifdef CONFIG_XIP_KERNEL uintptr_t xiprom; uintptr_t xiprom_sz; #endif }; printk调试 未使能MMU如何使用printf? 想要在setup_vm里面调用打印,发现这个阶段是不能使用printk的,因为mmu还没开启。可以调用到opensbi进行打印。 +static void _sbi_ecall(int ext, int fid, unsigned long arg0, + unsigned long arg1, unsigned long arg2, + unsigned long arg3, unsigned long arg4, + unsigned long arg5) +{ + + register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0); + register uintptr_t a1 asm ("a1") = (uintptr_t)(arg1); + register uintptr_t a2 asm ("a2") = (uintptr_t)(arg2); + register uintptr_t a3 asm ("a3") = (uintptr_t)(arg3); + register uintptr_t a4 asm ("a4") = (uintptr_t)(arg4); + register uintptr_t a5 asm ("a5") = (uintptr_t)(arg5); + register uintptr_t a6 asm ("a6") = (uintptr_t)(fid); + register uintptr_t a7 asm ("a7") = (uintptr_t)(ext); + asm volatile ("ecall" + : "+r" (a0), "+r" (a1) + : "r" (a2), "r" (a3), "r" (a4), "r" (a5), "r" (a6), "r" (a7) + : "memory"); +} + +static void _sbi_console_putchar(int ch) +{ + _sbi_ecall(0x1, 0, ch, 0, 0, 0, 0, 0); +} + +static void uart_puts(char *s) +{ + while(*s) { + _sbi_console_putchar(*s++); + } +} +static int _vsnprintf(char * out, size_t n, const char* s, va_list vl) +{ + int format = 0; + int longarg = 0; + size_t pos = 0; + for (; *s; s++) { + if (format) { + switch(*s) { + case 'l': { + longarg = 1; + break; + } + case 'p': { + longarg = 1; + if (out && pos < n) { + out[pos] = '0'; + } + pos++; + if (out && pos < n) { + out[pos] = 'x'; + } + pos++; + } + case 'x': { + long num = longarg ? va_arg(vl, long) : va_arg(vl, int); + int hexdigits = 2*(longarg ? sizeof(long) : sizeof(int))-1; + for(int i = hexdigits; i >= 0; i--) { + int d = (num >> (4*i)) & 0xF; + if (out && pos < n) { + out[pos] = (d < 10 ? '0'+d : 'a'+d-10); + } + pos++; + } + longarg = 0; + format = 0; + break; + } + case 'd': { + long num = longarg ? va_arg(vl, long) : va_arg(vl, int); + if (num < 0) { + num = -num; + if (out && pos < n) { + out[pos] = '-'; + } + pos++; + } + long digits = 1; + for (long nn = num; nn /= 10; digits++); + for (int i = digits-1; i >= 0; i--) { + if (out && pos + i < n) { + out[pos + i] = '0' + (num % 10); + } + num /= 10; + } + pos += digits; + longarg = 0; + format = 0; + break; + } + case 's': { + const char* s2 = va_arg(vl, const char*); + while (*s2) { + if (out && pos < n) { + out[pos] = *s2; + } + pos++; + s2++; + } + longarg = 0; + format = 0; + break; + } + case 'c': { + if (out && pos < n) { + out[pos] = (char)va_arg(vl,int); + } + pos++; + longarg = 0; + format = 0; + break; + } + default: + break; + } + } else if (*s == '%') { + format = 1; + } else { + if (out && pos < n) { + out[pos] = *s; + } + pos++; + } + } + if (out && pos < n) { + out[pos] = 0; + } else if (out && n) { + out[n-1] = 0; + } + return pos; +} + + +static char out_buf[1000]; // buffer for _vprintf() +static int _vprintf(const char* s, va_list vl) +{ + int res = _vsnprintf(NULL, -1, s, vl); + if (res+1 >= sizeof(out_buf)) { + uart_puts("error: output string size overflow\\n"); + while(1) {} + } + _vsnprintf(out_buf, res + 1, s, vl); + uart_puts(out_buf); + return res; +} + +int _printf(const char* s, ...) +{ + int res = 0; + va_list vl; + va_start(vl, s); + res = _vprintf(s, vl); + va_end(vl); + return res; +} + pgd_t early_pg_dir[PTRS_PER_PGD] __initdata __aligned(PAGE_SIZE); /* Number of entries in the page global directory */ #define PTRS_PER_PGD (PAGE_SIZE / sizeof(pgd_t)) /* Number of entries in the page table */ #define PTRS_PER_PTE (PAGE_SIZE / sizeof(pte_t)) PAGE_SIZE = 4096 sizeof(pgd_t) = 8 PTRS_PER_PGD = 512 PGDIR_SIZE = 0x8000000000 ---512G try satp模式 static __init void set_satp_mode(void) { u64 identity_satp, hw_satp; uintptr_t set_satp_mode_pmd = ((unsigned long)set_satp_mode) & PMD_MASK; bool check_l4 = false; //下面是对set_satp_mode到set_satp_mode+4M代码空间做虚拟地址到物理地址映射,即填充 //这段空间的页表,P4D/PUD/PMD create_p4d_mapping(early_p4d, set_satp_mode_pmd, (uintptr_t)early_pud, P4D_SIZE, PAGE_TABLE); create_pud_mapping(early_pud, set_satp_mode_pmd, (uintptr_t)early_pmd, PUD_SIZE, PAGE_TABLE); /* Handle the case where set_satp_mode straddles 2 PMDs */ // 处理 `set_satp_mode` 跨越两个PMD的情况,即set_satp_mode开始+4M的空间恒等映射 //PMD_SIZE = 2MB,0x200000,其实也不用这么大,1个表项也够了。 create_pmd_mapping(early_pmd, set_satp_mode_pmd, set_satp_mode_pmd, PMD_SIZE, PAGE_KERNEL_EXEC); create_pmd_mapping(early_pmd, set_satp_mode_pmd + PMD_SIZE, set_satp_mode_pmd + PMD_SIZE, PMD_SIZE, PAGE_KERNEL_EXEC); retry: //默认先按照5级页表来设置 create_pgd_mapping(early_pg_dir, set_satp_mode_pmd, check_l4 ? (uintptr_t)early_pud : (uintptr_t)early_p4d, PGDIR_SIZE, PAGE_TABLE); identity_satp = PFN_DOWN((uintptr_t)&early_pg_dir) | satp_mode; local_flush_tlb_all(); csr_write(CSR_SATP, identity_satp); //设置satp, satp_mode = a/9, early_pg_dir[512]为L0页表项目 //执行该代码后,就使能了MMU,后续取指令的地址,就需要经过MMU转化才是实际的物理地址。 //这也是为什么前面要做set_satp_mode的恒等映射(虚拟地址=物理地址)。 //如果前面不进行恒等映射,MMU在转化过程中查询不到页表,就会进入异常 hw_satp = csr_swap(CSR_SATP, 0ULL); //这条代码是虚拟地址运行。 //将 0ULL 写入 CSR_SATP 寄存器,这样就相当又关闭了MMU。 //将 CSR_SATP 寄存器的旧值返回,并赋给变量 hw_satp。 local_flush_tlb_all(); //satp先默认设置为10,即Sv57 5级页表,如果硬件支持5级页表SATP写入时返回的值hw_satp //即为设置的值identity_satp,如果不等,则是4级页表。 if (hw_satp != identity_satp) { if (!check_l4) { disable_pgtable_l5(); check_l4 = true; memset(early_pg_dir, 0, PAGE_SIZE); goto retry; } disable_pgtable_l4(); } memset(early_pg_dir, 0, PAGE_SIZE); memset(early_p4d, 0, PAGE_SIZE); memset(early_pud, 0, PAGE_SIZE); memset(early_pmd, 0, PAGE_SIZE); } static void __init disable_pgtable_l5(void) { pgtable_l5_enabled = false; kernel_map.page_offset = PAGE_OFFSET_L4; satp_mode = SATP_MODE_48; } //说明不支持Sv57模式,5级页表,更新page offset和satp_mode static void __init disable_pgtable_l4(void) { pgtable_l4_enabled = false; kernel_map.page_offset = PAGE_OFFSET_L3; satp_mode = SATP_MODE_39; } //说明不支持Sv48,4级页表,更新page offset和satp_mode,本章的实验是支持4级页表。 set_satp_mode作用是从5级/4级页表开始进行尝试硬件是否支持,判断的方法是如果当前的等级支持,那么satp寄存器是可以写入的,如果不支持寄存器将无法写入。 前面create_p4d_mapping/create_pud_mapping/create_pmd_mapping三个函数的作用是先将set_satp_mode这段代码开始的4M(2个PMD页表项)范围进行恒等映射(虚拟地址=物理地址),之所以要这么做是因为一旦写satp寄存使能mmu后,当前运行的地址就会变成虚拟地址,该地址会经过mmu转化为实际的加载地址,而如果mmu在转化过程中查询不到该地址对应的页表,就会发生异常。因此先将set_satp_mode这段代码的页表填充好,做好虚拟地址到物理地址的映射,在使能MMU后,代码可以继续接着运行。 Sv64:6级页表 Sv57: 5级页表 Sv48:4级页表 Sv39:3级页表 5级页表结构 使能MMU relocate_enable_mmu: /* Relocate return address */ la a1, kernel_map //将符号kernel_map的地址加载到寄存器a1 XIP_FIXUP_OFFSET a1 //修正a1的偏移,这通常是用于处理执行在位置无关代码(XIP)的情况。 REG_L a1, KERNEL_MAP_VIRT_ADDR(a1) //从虚拟地址中加载值到寄存器a1,这样我们就有了kernel_map的虚拟地址。 la a2, _start //将符号_start的地址加载到寄存器a2。 sub a1, a1, a2 //计算kernel_map - _start的偏移量,并存储在a1中。 add ra, ra, a1 //将返回地址ra加上前面计算的偏移量,重新定位返回地址。 //将返回地址从物理地址转化为虚拟地址 /* Point stvec to virtual address of intruction after satp write */ la a2, 1f //将标签1f(即稍后定义的标签1)的地址加载到a2中。 add a2, a2, a1 //将标签的地址加上偏移量,得到它的虚拟地址。 csrw CSR_TVEC, a2 //将a2的值写入CSR_TVEC寄存器,设置异常向量表基地址。 //异常后将跳转到1f的位置,1f是虚拟地址。 /* Compute satp for kernel page tables, but don\'t load it yet */ srl a2, a0, PAGE_SHIFT //将a0右移PAGE_SHIFT位,计算页号(Page Number) la a1, satp_mode //将satp_mode的地址加载到寄存器a1。 REG_L a1, 0(a1) //从satp_mode地址处加载一个值到寄存器a1,即或stap_mode的值 or a2, a2, a1 //将页号和模式组合起来,生成最终的 satp 值,但还不加载它。 /* * 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 //将trampoline_pg_dir的地址加载到寄存器a0。 XIP_FIXUP_OFFSET a0 //修正a0的偏移 srl a0, a0, PAGE_SHIFT //将a0右移PAGE_SHIFT位,计算页号。 or a0, a0, a1 //将页号和模式组合起来,生成最终的 satp 值。 sfence.vma //执行虚地址翻译的同步指令,确保所有之前的变更生效。 csrw CSR_SATP, a0 //将a0的值写入CSR_SATP寄存器,加载 trampoline 页目录。 //这里使能了MMU,那么下一条指令 地址PC+8需要经过MMU转化后地址进行 //访问,而此时PC+8的地址是没有映射页表的,那么地址就会变成非法而 //进入异常 .align 2 1: //以该物理地址来取指\"标号1的指令\",异常 -----这里非常巧妙 //PC跳转到\"CSR_TVEC的值\",以标号1的虚拟地址来取指\"标号1的指令\",正常,往下执行 /* Set trap vector to spin forever to help debug */ la a0, .Lsecondary_park csrw CSR_TVEC, a0 //将 trap 向量设置为.Lsecondary_park。 /* 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 sfence.vma ret asmlinkage void __init setup_vm(uintptr_t dtb_pa) { pmd_t __maybe_unused fix_bmap_spmd, fix_bmap_epmd; kernel_map.virt_addr = KERNEL_LINK_ADDR; kernel_map.page_offset = _AC(CONFIG_PAGE_OFFSET, UL); _printf(\"%s,%d,virt_addr:%lx, offset:%lx\\n\",__func__,__LINE__, KERNEL_LINK_ADDR, kernel_map.page_offset); #ifdef CONFIG_XIP_KERNEL kernel_map.xiprom = (uintptr_t)CONFIG_XIP_PHYS_ADDR; kernel_map.xiprom_sz = (uintptr_t)(&_exiprom) - (uintptr_t)(&_xiprom); phys_ram_base = CONFIG_PHYS_RAM_BASE; kernel_map.phys_addr = (uintptr_t)CONFIG_PHYS_RAM_BASE; kernel_map.size = (uintptr_t)(&_end) - (uintptr_t)(&_sdata); kernel_map.va_kernel_xip_pa_offset = kernel_map.virt_addr - kernel_map.xiprom; #else kernel_map.phys_addr = (uintptr_t)(&_start); kernel_map.size = (uintptr_t)(&_end) - kernel_map.phys_addr; #endif _printf(\"%s,%d,_start:%lx, _end:%lx\\n\",__func__,__LINE__,&_start, &_end); _printf(\"%s,%d,phys_addr:%lx, size:%lx\\n\",__func__,__LINE__, kernel_map.phys_addr, kernel_map.size); #if defined(CONFIG_64BIT) && !defined(CONFIG_XIP_KERNEL) set_satp_mode(); //设置satp寄存器,确定几级页表 #endif _printf(\"%s,%d,_start:%lx, _end:%lx\\n\",__func__,__LINE__,&_start, &_end); _printf(\"%s,%d,phys_addr:%lx, size:%lx\\n\",__func__,__LINE__, kernel_map.phys_addr, kernel_map.size); #if defined(CONFIG_64BIT) && !defined(CONFIG_XIP_KERNEL) set_satp_mode(); #endif _printf(\"%s,%d,page offset%lx, kernel_map.page_offset:%lx\\n\", __func__,__LINE__,PAGE_OFFSET, kernel_map.page_offset); kernel_map.va_pa_offset = PAGE_OFFSET - kernel_map.phys_addr; kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr; riscv_pfn_base = PFN_DOWN(kernel_map.phys_addr); _printf(\"%s,%d,va_pa_offset:%lx, va_kernel_pa_offset:%lx, pfn_base:%lx\\n\",__func__,__LINE__, kernel_map.va_pa_offset, kernel_map.va_kernel_pa_offset, riscv_pfn_base); /* * The default maximal physical memory size is KERN_VIRT_SIZE for 32-bit * kernel, whereas for 64-bit kernel, the end of the virtual address * space is occupied by the modules/BPF/kernel mappings which reduces * the available size of the linear mapping. */ memory_limit = KERN_VIRT_SIZE - (IS_ENABLED(CONFIG_64BIT) ? SZ_4G : 0); _printf(\"%s,%d,memory_limit:%lx\\n\",__func__,__LINE__, memory_limit); /* Sanity check alignment and size */ BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0); BUG_ON((kernel_map.phys_addr % PMD_SIZE) != 0); #ifdef CONFIG_64BIT /* * The last 4K bytes of the addressable memory can not be mapped because * of IS_ERR_VALUE macro. */ BUG_ON((kernel_map.virt_addr + kernel_map.size) > ADDRESS_SPACE_END - SZ_4K); #endif pt_ops_set_early(); /* Setup early PGD for fixmap */ _printf(\"%s,%d FIXADDR_START:%lx,fixmap_pgd_next :%lx, PGDIR_SIZE:%lx\\n\",__func__,__LINE__, FIXADDR_START, fixmap_pgd_next, PGDIR_SIZE); //因为一旦使能MMU,就是取指令的地址不是实际的运行地址,而是需要经过MMU转化才能访问 //如果要访问DTB、访问IO就不能访问,因为查询不到对应的页表,因为内存管理系统还没有初始化, //也不能动态分配内存来作为页表填充,那么就因此内核引入fixmap,事先 //分配一段虚拟地址空间,然后给其虚拟地址创建号PGD/PUD/PMD的页表,PTE的页表等那个模块 //(如DTB)使用了再进行填充,这样通过fixmap这段虚拟地址就可以查询页表访问到物理内存。 //当前fixmap主要是用于访问设备树dtb的。 //FIXADDR_START~FIXADDR_TOP这段虚拟地址范围是固定的,用于映射到FDT/EARLYCON/IO //填充fixmap的PGD页表,fixmap虚拟地址对应的页表项中存储的是fixmap_pgd_next,下一级 //页表的地址,如果是5级页表就是fixmap_p4d[PTRS_PER_P4D],如果是4级页表就是 //fixmap_pud[PTRS_PER_PUD],本章实验是4级页表。 create_pgd_mapping(early_pg_dir, FIXADDR_START, fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE); #ifndef __PAGETABLE_PMD_FOLDED _printf(\"%s,%d,pgtable_l5_enabled: %lx,pgtable_l5_enabled:%lx \\n\", __func__,__LINE__,pgtable_l5_enabled,pgtable_l4_enabled); /* Setup fixmap P4D and PUD */ if (pgtable_l5_enabled) create_p4d_mapping(fixmap_p4d, FIXADDR_START, (uintptr_t)fixmap_pud, P4D_SIZE, PAGE_TABLE); /* Setup fixmap PUD and PMD */ if (pgtable_l4_enabled) create_pud_mapping(fixmap_pud, FIXADDR_START, (uintptr_t)fixmap_pmd, PUD_SIZE, PAGE_TABLE); create_pmd_mapping(fixmap_pmd, FIXADDR_START, (uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABLE); /* Setup trampoline PGD and PMD */ create_pgd_mapping(trampoline_pg_dir, kernel_map.virt_addr, trampoline_pgd_next, PGDIR_SIZE, PAGE_TABLE); if (pgtable_l5_enabled) create_p4d_mapping(trampoline_p4d, kernel_map.virt_addr, (uintptr_t)trampoline_pud, P4D_SIZE, PAGE_TABLE); if (pgtable_l4_enabled) create_pud_mapping(trampoline_pud, kernel_map.virt_addr, (uintptr_t)trampoline_pmd, PUD_SIZE, PAGE_TABLE); #ifdef CONFIG_XIP_KERNEL create_pmd_mapping(trampoline_pmd, kernel_map.virt_addr, kernel_map.xiprom, PMD_SIZE, PAGE_KERNEL_EXEC); #else create_pmd_mapping(trampoline_pmd, kernel_map.virt_addr, kernel_map.phys_addr, PMD_SIZE, PAGE_KERNEL_EXEC); #endif #else /* Setup trampoline PGD */ create_pgd_mapping(trampoline_pg_dir, kernel_map.virt_addr, kernel_map.phys_addr, PGDIR_SIZE, PAGE_KERNEL_EXEC); #endif /* * Setup early PGD covering entire kernel which will allow * us to reach paging_init(). We map all memory banks later * in setup_vm_final() below. */ create_kernel_page_table(early_pg_dir, true); /* Setup early mapping for FDT early scan */ create_fdt_early_page_table(early_pg_dir, dtb_pa); /* * Bootime fixmap only can handle PMD_SIZE mapping. Thus, boot-ioremap * range can not span multiple pmds. */ BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT) != (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT)); #ifndef __PAGETABLE_PMD_FOLDED /* * Early ioremap fixmap is already created as it lies within first 2MB * of fixmap region. We always map PMD_SIZE. Thus, both FIX_BTMAP_END * FIX_BTMAP_BEGIN should lie in the same pmd. Verify that and warn * the user if not. */ fix_bmap_spmd = fixmap_pmd[pmd_index(__fix_to_virt(FIX_BTMAP_BEGIN))]; fix_bmap_epmd = fixmap_pmd[pmd_index(__fix_to_virt(FIX_BTMAP_END))]; if (pmd_val(fix_bmap_spmd) != pmd_val(fix_bmap_epmd)) { WARN_ON(1); pr_warn(\"fixmap btmap start [%08lx] != end [%08lx]\\n\", pmd_val(fix_bmap_spmd), pmd_val(fix_bmap_epmd)); pr_warn(\"fix_to_virt(FIX_BTMAP_BEGIN): %08lx\\n\", fix_to_virt(FIX_BTMAP_BEGIN)); pr_warn(\"fix_to_virt(FIX_BTMAP_END): %08lx\\n\", fix_to_virt(FIX_BTMAP_END)); pr_warn(\"FIX_BTMAP_END: %d\\n\", FIX_BTMAP_END); pr_warn(\"FIX_BTMAP_BEGIN: %d\\n\", FIX_BTMAP_BEGIN); } #endif _printf(\"%s,%d start pt_ops_set_fixmap\\n\",__func__,__LINE__); pt_ops_set_fixmap(); _printf(\"%s,%d end pt_ops_set_fixmap\\n\",__func__,__LINE__); }
- 
					
						  RISC-V backtrace实现原理start_kernel addi sp,sp,-16 ---① 分配栈帧sp=sp-16,sp指向栈顶 sw ra,12(sp) ---② 将ra存储到sp+12的位置 sw s0,8(sp) xxxxxx jal ra, 6000dba2 <backtrace> xxxxxx lw s0,8(sp) lw ra,12(sp) addi sp,sp,16 _backtrace for(level = 1; level < BT_LEVEL_LIMIT; level++) backtrace_from_stack(&SP, &PC, &LR, print_func, 1); for(i = 2; i < BT_SCAN_MAX_LIMIT; i += 2) riscv_ins16_get_push_lr_framesize(ins16, &offset); //获取到分配栈帧的位置,地址为parse_addr,该位置也是函数的起始位置,如上的① //同时也获取到ra的在栈中的偏移位置offset,通过类似sw ra,12(sp)计算得出。 for(i = 0; parse_addr + i < PC; i += 2) riscv_ins16_backtrace_stask_push(ins16); //计算函数的起始位置到当前运行的PC位置,使用了多少栈空间。存储到framesize中。 LR = (char *) * (SP + offset); //计算LR的值,如上②,RA是存储到SP+offset的位置。 *pSP = SP + framesize; //*pSP即为栈底位置,即为上一个函数的栈顶,为下一次遍历做准备 offset = find_lr_offset(LR, print_func); print_backtrace(print_func, (unsigned long)LR_fixed - offset);//打印位置 *pPC = LR - offset; //因为RA存储的是跳转指令的下一条指令,所以需要减去一条指令就得到跳转的位置。 //到这里*pSP、*pPC就更新到上一个函数的值,再接着继续遍历即可。
- 
					
						  RISC-V动态链接实验准备 实验环境 qemu linux启动环境: http://www.laumy.tech/1186.html 代码 动态库 cat swap.c #include <stdio.h> int shared = 1; int z; void swap(int *a, int *b) { printf("%s,%d\\n",__func__,__LINE__); *a ^= *b ^= *a ^= *b; } 主程序 #include <stdio.h> extern int shared; int s; int main(void) { int a = 100; int b = 200; int c = 300; swap(&a, &shared); printf("a:%d,b:%d,c:%d\\n",a,b,c); sleep(10); swap(&a, &b); printf("a:%d,b:%d,c:%d\\n",a,b,c); return 0; } 编译 生成动态库:riscv64-unknown-linux-gnu-gcc -g -fPIC -shared swap.c -o libswap.so 生成可执行程序:riscv64-unknown-linux-gnu-gcc -g main.c ./libswap.so 将生成的可执行程序通过通过mount方式挂载到qemu linux中。 qemu gdb 启动gdb: riscv64-unknown-linux-gnu-gdb 连接: target remote:1234 加载符号:file xxx/a.out 打断点:b main 执行显示信息:(可实现在gdbinit中写好) display/z $ra display/z $sp display/z $s0 display/z $s1 display/z $s2 display/z $s3 display/z $s4 display/z $s5 display/z $a0 display/z $a1 display/z $a2 display/z $a3 display/z $a4 display/z $a5 display/z $a6 display/z $a7 display/z $t0 display/z $t1 display/z $t2 display/z $t3 display/z $t4 display/z $t5 set disassemble-next-line on 在qemu linux中:./a.out 第一次跳转到PLT表 准备跳转到swap@plt表 0x00000000000105d0 7 swap(&a, &shared); 0x00000000000105c6 <main+16>: fec40713 addi a4,s0,-20 0x00000000000105ca <main+20>: 83818593 addi a1,gp,-1992 0x00000000000105ce <main+24>: 853a mv a0,a4 => 0x00000000000105d0 <main+26>: f11ff0ef jal ra,0x104e0 <swap@plt> 寄存器信息如下: 10: /z $ra = 0x00000000000105d4 11: /z $sp = 0x0000003fd23d4bf0 12: /z $s0 = 0x0000003fd23d4c10 13: /z $s1 = 0x0000000000000000 14: /z $s2 = 0x0000000000145f60 15: /z $s3 = 0x0000000000000000 16: /z $s4 = 0x00000000001a2828 17: /z $s5 = 0x0000000000000000 18: /z $a0 = 0x0000003fd23d4bfc 19: /z $a1 = 0x0000000000012038 20: /z $a2 = 0x0000003fd23d4d98 21: /z $a3 = 0x0000000000000000 22: /z $a4 = 0x0000003fd23d4bfc 23: /z $a5 = 0x0000000000000064 24: /z $a6 = 0x0000003f81c60d90 25: /z $a7 = 0x7a2f5b5a40014e00 26: /z $t0 = 0x0000003f81b61e18 27: /z $t1 = 0x0000003f81b7e6d2 28: /z $t2 = 0xffffffffffffffff 29: /z $t3 = 0x00000000000206d2 30: /z $t4 = 0x0000003f81c86a8c 31: /z $t5 = 0x0000000000000004 下面是.plt的代码,先关注swap@plt Disassembly of section .plt: 00000000000104a0 <_PROCEDURE_LINKAGE_TABLE_>: 104a0: 00002397 auipc t2,0x2 104a4: 41c30333 sub t1,t1,t3 104a8: b683be03 ld t3,-1176(t2) # 12008 <__TMC_END__> 104ac: fd430313 addi t1,t1,-44 104b0: b6838293 addi t0,t2,-1176 104b4: 00135313 srli t1,t1,0x1 104b8: 0082b283 ld t0,8(t0) 104bc: 000e0067 jr t3 # 1a000 <__global_pointer$+0x7800> 00000000000104c0 <__libc_start_main@plt>: 104c0: 00002e17 auipc t3,0x2 104c4: b58e3e03 ld t3,-1192(t3) # 12018 <__libc_start_main@GLIBC_2.27> 104c8: 000e0367 jalr t1,t3 104cc: 00000013 nop 00000000000104d0 <sleep@plt>: 104d0: 00002e17 auipc t3,0x2 104d4: b50e3e03 ld t3,-1200(t3) # 12020 <sleep@GLIBC_2.27> 104d8: 000e0367 jalr t1,t3 104dc: 00000013 nop 00000000000104e0 <swap@plt>: 104e0: 00002e17 auipc t3,0x2 104e4: b48e3e03 ld t3,-1208(t3) # 12028 <swap> 104e8: 000e0367 jalr t1,t3 104ec: 00000013 nop 从上面发现,swap@plt,是从12028处加载一个地址,然后跳转运行。下面则是12028的地址,发现是.got表。注意12028: 04a0 addi s0,sp,584,不要被这个误导,正好反汇编翻译2字节对应是addi s0,sp,584。实际上0x12028处存储的地址是:0x104a0,因此上面加载的t3值就是0x104a0,即jalr将跳转到该值运行。而0x104a0正好是PROCEDURE_LINKAGE_TABLE。 Disassembly of section .got: 0000000000012008 <__TMC_END__>: 12008: ffffffff 0xffffffff 1200c: ffffffff 0xffffffff ... 12018: 04a0 addi s0,sp,584 1201a: 0001 nop 1201c: 0000 unimp 1201e: 0000 unimp 12020: 04a0 addi s0,sp,584 12022: 0001 nop 12024: 0000 unimp 12026: 0000 unimp 12028: 04a0 addi s0,sp,584 1202a: 0001 nop 1202c: 0000 unimp ... 从上面分析可知,swap函数地址存储在.got表0x12028处,而该值初值是PROCEDURE_LINKAGE_TABLE,这是因为在编译完还没有运行前,swap在动态库中地址并没有确定,实际的地址需要程序运行加载才会确定,所以了默认先填PROCEDURE_LINKAGE_TABLE,而该标签中的代码,正好就是处理寻址swap地址的功能。 下面是gdb单步调试的信息,下面的跳转跟我们分析的一致。 也可以通过x命令查询.got表的信息 小结:跳转到.plt表,第一次执行对应的.got表中没有存储swap的地址,将先跳转到PROCEDURE_LINKAGE_TABLE,即.plt的第一表项。 procedure linkage table .plt的地址一表项存储的是链接器表。 Disassembly of section .plt: 00000000000104a0 <_PROCEDURE_LINKAGE_TABLE_>: 104a0: 00002397 auipc t2,0x2 104a4: 41c30333 sub t1,t1,t3 104a8: b683be03 ld t3,-1176(t2) # 12008 <__TMC_END__> 104ac: fd430313 addi t1,t1,-44 104b0: b6838293 addi t0,t2,-1176 104b4: 00135313 srli t1,t1,0x1 104b8: 0082b283 ld t0,8(t0) 104bc: 000e0067 jr t3 # 1a000 <__global_pointer$+0x7800> 上面的汇编最终会跳转到链接器里面,所做的工作就是通过链接器查询swap的地址,然后填充到.got中,然后进行跳转swap,这里就不再分析,直接看最后的结果。当执行完链接器的处理后,会更新.got表。 从上面可知swap函数在got表中,地址被更新为0x3f9a69f4c2。 再次调用swap函数 当再次调用swap函数的时候,ld t3,-1208(t3),这里的t3就变成了0x3f9a69f4c2,这样就不会再执行PROCEDURE_LINKAGE_TABLE,直接跳转到swap的地址。 还可以使用cat /proc/xx/maps确定so落在的范围,下面的地址不一定匹配了, 重新运行了一次a.out,只是给个示例。
- 
					
						  opensbi分析(二)待补充...... 参考: 玄铁处理器Linux移植
- 
					
						  opensbi分析(一)引导Hart启动 _start: /* Find preferred boot HART id */ MOV_3R s0, a0, s1, a1, s2, a2 # 将a0,a1,a2的参数分别赋值为s0,s1,s2,这3个参数是前一阶段传入的参数。 # a0: hart id # a1: device tree # a2: struct fw_dynamic_info地址 call fw_boot_hart # fw_boot_hart会根据配置为fw_dynamic、fw_jump、fw_payload三种方式进行跳转, # 后两种方式直接返回-1,即hart id使用随机的方式,而fw_dynamic根据传进来的参数 # 进行解析出boot hart id,a2指向struct fw_dynamic_info # 我们这里分析使用的是fw_jump的方式。 add a6, a0, zero # 将经过fw_boot_hart选择的hart id赋值为a6,这样a6保存的就是首先要启动的hart id MOV_3R a0, s0, a1, s1, a2, s2 # 恢复a0,a1,a2的值,之前是暂存到s0,s1,s2中,因为前面调用了fw_boot_hart会修改 li a7, -1 beq a6, a7, _try_lottery # 如果a6为-1,即上一阶段没有选定boot hard id,则使用彩票机制随机选择一个 # hard id进行先启动。 /* Jump to relocation wait loop if we are not boot hart */ bne a0, a6, _wait_relocate_copy_done # 如果选择的boot hard id与当前运行的hart id不等,则进行等待boot hart id先运行。 _try_lottery:(彩票机制选择一个hart id作为启动hart) /* Jump to relocation wait loop if we don't get relocation lottery */ lla a6, _relocate_lottery # 加载_relocate_lottery(是一段bss空间)到a6寄存 li a7, 1 amoadd.w a6, a7, (a6) # a6指向的地址上的值(_relocate_lottery 的地址)做原子加 1, # _relocate_lottery 的老值写入a6(最原始的值为0)。 bnez a6, _wait_relocate_copy_done # 如果a6不等于0,表示不是最先执行的hart,则跳转到_wait_relocate_copy_done # 等待,如果等于0,表示最快运行的hart,则为boot hart往下走 # 与其说是彩票机制,看起来是赛跑机制,最先跑到这段代码的作为boot hart 上面的代码,主要是获取一个hart 作为boot hart,选定了boot hart后,其他的hart就需要等待boot hart先运行完成相关初始化工作再运行。如果opensbi使用的是fw_jump、fw_payload,选择boot hart的方式就是彩票机制。 重定位和初始化 预备信息 _fw_start _fw_start的值,在链接脚本 build/platform/generic/firmware/fw_jump.elf.ld中指定 从通过工具链读取到_fw_start为0x80000000,如下: .rela.dyn .rela.dyn是重定位段,重定位是连接符号引用与符号定义的过程。例如,程序调用函数时,关联的调用指令必须在执行时将控制权转移到正确的目标地址。可重定位文件必须包含说明如何修改其节内容的信息。通过此信息,可执行文件和共享目标文件可包含进程的程序映像的正确信息。重定位项即是这些数据。 typedef struct elf32_rela{ Elf32_Addr r_offset; Elf32_Word r_info; Elf32_Sword r_addend; } Elf32_Rela; typedef struct elf64_rela { Elf64_Addr r_offset; /* Location at which to apply the action */ Elf64_Xword r_info; /* index and type of relocation */ Elf64_Sxword r_addend; /* Constant addend used to compute value */ } Elf64_Rela; 重定位表的每个条目的内容如上数据结构所示,有3个变量。 r_offset: 需要重定位地址的偏移,外部符号在got表的偏移 r_r_info: 重定位的方法 r_addend:重定位计算的值 使用 riscv64-unknown-linux-gnu-readelf -S build/platform/generic/firmware/fw_jump.elf命令可以看到重定位表的地址。 如上图.rela.dyn的地址为0x8001c3a8,长度为0x2418。 执行riscv64-unknown-linux-gnu-readelf -rW build/platform/generic/firmware/fw_jump.elf可以查看重定位信息表。 上图所示,fw_jump.elf的重定位表中有385个条目,即有385个地址需要重定位,上图中r_info是0x3,即类型是R_RISCV_RELATIVE,则重定位方式是按照B+A,B表示执行过程中将共享目标文件装入内存的基本地址,通常生成的共享目标文件的基本虚拟地址为0,在本节中B为_fw_start - FW_TEXT_START;A表示常量加数,用于计算存储在可重定位字段中的值,即r_addend的值,所以0x80020018的地址重定位到0x80000000。 怎么理解_fw_start 和 FW_TEXT_START地址? _fw_start表示加载地址 FW_TEXT_START表示链接地址 在opensbi 1.2版本,_fw_start和FW_TEXT_START是一样的。 OpenSBI在什么时候引入的PIE支持? 在1.0版本引入的,https://github.com/riscv-software-src/opensbi/commit/0f20e8a?diff=split&w=0 链接脚本包含.rela.dyn节,该节中的所有重定位表项均是R_RISCV_RELATIVE类型。 Self-relocataion发生在非常早期的汇编代码阶段firmware/fw_base.S 遍历.rela.dyn节里的所有重定位表项,进行地址修正 在重定位完成之前,所有对全局变量的地址引用,需要显式使用lla汇编伪指令(load local address)而不是la la在PIE模式下会编译成对GOT表的引用。 rela.dyn和.rel.dyn有什么区别? .rel.dyn 和 .rela.dyn 都是 ELF 文件中用于存储重定位条目的节(section),它们主要用于动态链接过程中修正全局偏移表(GOT, Global Offset Table)和程序中的某些地址。不过,它们之间存在一些关键差异: (1)数据结构不同:.rel.dyn 使用的是 REL 类型的重定位条目,每个条目包含两个字段:一个表示需要被重定位的位置的偏移量,另一个表示重定位类型。 .rela.dyn 使用的是 RELA 类型的重定位条目,在 REL 类型的基础上增加了一个 additive constant 字段,使得重定位操作可以直接应用一个立即数到目标位置,而不需要像 REL 那样进行间接计算。这意味着 RELA 类型提供了更直接的重定位能力。 (2)内存占用和效率:RELA 类型由于包含额外的 additive constant 字段,所以每个条目相比 REL 类型会占用更多的空间。然而,RELA 类型在处理时可能因为直接性而稍微提高效率,因为它减少了处理器需要执行的计算步骤。 (3)使用场景:在不同的系统或编译器配置下,可能会优先选择使用其中一种类型。通常,现代系统和编译器更倾向于使用 .rela.dyn,因为它虽然占用更多空间,但提供的直接重定位能力简化了链接过程。 总的来说,.rel.dyn 和 .rela.dyn 都服务于动态链接时的地址修正,主要区别在于重定位条目的格式及其对空间和处理效率的影响。在具体应用中,根据编译器选项和目标系统的偏好,会选择使用其中之一来完成动态链接所需的重定位工作。 /* Save load address */ lla t0, _load_start # 加载地址存储到t0寄存器:0x0000000080020018 lla t1, _fw_start # _fw_start存储到t1寄存器:t1片=0x0000000080000000 # _fw_start为链接脚本中设定的起始地址 REG_S t1, 0(t0) # REG_S是sd,即t1的值存储到t0的地址 # 可以理解为_load_start是一个全局指针(指向事先分配的空间),将fw_jump.elf # 的起始地址即,在链接脚本中设置的赋值_load_start中存储。 #ifdef FW_PIC /* relocate the global table content */ lla t0, _link_start # _link_start存储到t0寄存器:t0 = 0x0000000080020020 REG_L t0, 0(t0) # 将t0地址的值加载到t0,t0=0x0000000080000000, # _link_start指向一个dword全局变量的指针,指向的是FW_TEXT_START # 在firmware/fw_base.S中如下定义 # _link_start:gg # RISCV_PTR FW_TEXT_START # 所以上面代码的意思就是将FW_TEXT_START赋值为t0,FW_TEXT_START在 # platform/generic/objects.mk中定义,表示FW TEXT段的开始地址 # 正好也是FW_TEXT_START=0x80000000 # FW_TEXT_START设置的是固件要运行的起始地址,而_fw_start是链接地址 /* t1 shall has the address of _fw_start */ sub t2, t1, t0 # t2 = _fw_start - FW_TEXT_START,计算要运行地址和链接地址的差值 lla t3, _runtime_offset # t3 = 0x0000000080020060,申请的一个全局变量(bss空间) REG_S t2, (t3) # 将运行的地址与链接地址的偏移存储到_runtime_offset指向的地址中 lla t0, __rel_dyn_start # 重定位表开始地址:0x000000008001c3a8 lla t1, __rel_dyn_end # 重定位表的结束地址:0x000000008001e7c0 # 结束地址减去起始地址正好size为0x2418,跟前面章节对应。 beq t0, t1, _relocate_done j 5f # 如果重定位表的开始地址等于结束地址,说明不需要重定位,直接跳转到 # _relocate_done结束重定位,否则跳转到5标签进行重定位。 2: REG_L t5, -(REGBYTES*2)(t0) /* t5 <-- relocation info:type */ # REG_L为ld,t0-16地址指向条目的第二个成员变量即info type li t3, R_RISCV_RELATIVE /* reloc type R_RISCV_RELATIVE */ bne t5, t3, 3f # 如果info type不是R_RISCV_RELATIVE类型,则跳转到标签 # 接下来就是处理info type为R_RISCV_RELATIVE的重定位 REG_L t3, -(REGBYTES*3)(t0) # 获取重定位表一个条目中offset的值存储到t3 REG_L t5, -(REGBYTES)(t0) /* t5 <-- addend */ # 获取重定位表一个条目中addend值 add t5, t5, t2 # 即重定位地址为B+A,A为addend值,B=_fw_start - FW_TEXT_START add t3, t3, t2 REG_S t5, 0(t3) /* store runtime address to the GOT entry */ # 将重定位的地址更新到GOT表中去。 j 5f # 跳转到标签5继续循环,将所有的重定位条目更新到GOT表中去。 # 标签3和4是针对info type不是R_RISCV_RELATIVE的处理,这里就不再分析了。 3: lla t4, __dyn_sym_start 4: REG_L t5, -(REGBYTES*2)(t0) /* t5 <-- relocation info:type */ srli t6, t5, SYM_INDEX /* t6 <--- sym table index */ andi t5, t5, 0xFF /* t5 <--- relocation type */ li t3, RELOC_TYPE bne t5, t3, 5f /* address R_RISCV_64 or R_RISCV_32 cases*/ REG_L t3, -(REGBYTES*3)(t0) li t5, SYM_SIZE mul t6, t6, t5 add s5, t4, t6 REG_L t6, -(REGBYTES)(t0) /* t0 <-- addend */ REG_L t5, REGBYTES(s5) add t5, t5, t6 add t5, t5, t2 /* t5 <-- location to fix up in RAM */ add t3, t3, t2 /* t3 <-- location to fix up in RAM */ REG_S t5, 0(t3) /* store runtime address to the variable */ 5: addi t0, t0, (REGBYTES*3) # REGBYTES在64位系统为8字节大小,__rel_dyn_start偏移3个变量,即24字节大小 # 正好就是一个条目的大小(Elf64_Rela数据结构)。 ble t0, t1, 2b # 如果t0加了一个条目的大小还小于__rel_dyn_end则跳转到2标签。 j _relocate_done _wait_relocate_copy_done: j _wait_for_boot_hart _relocate_done: /* * Mark relocate copy done * Use _boot_status copy relative to the load address */ lla t0, _boot_status # t0 = 0x0000000080020010 #ifndef FW_PIC lla t1, _link_start REG_L t1, 0(t1) lla t2, _load_start REG_L t2, 0(t2) sub t0, t0, t1 add t0, t0, t2 #endif li t1, BOOT_STATUS_RELOCATE_DONE # t1 = 1 REG_S t1, 0(t0) # 将1 写到_boot_status指向的地址 fence rw, rw # 建立一个全局内存屏障,确保该指令执行前后,所有写操作都完成完成。 # _relocate_done的作用就是写一个标志位到_boot_status表示重定位 # 已经完成。 状态初始化 /* At this point we are running from link address */ /* Reset all registers for boot HART */ li ra, 0 call _reset_regs # 清除所有的寄存器 /* Zero-out BSS */ lla s4, _bss_start lla s5, _bss_end _bss_zero: REG_S zero, (s4) add s4, s4, __SIZEOF_POINTER__ blt s4, s5, _bss_zero # 清除BSS段,BSS段写0 /* Setup temporary trap handler */ lla s4, _start_hang csrw CSR_MTVEC, s4 # 设置临时的trap handle,里面直接进入WFI /* Setup temporary stack */ lla s4, _fw_end li s5, (SBI_SCRATCH_SIZE * 2) add sp, s4, s5 # 设置临时栈 /* Allow main firmware to save info */ MOV_5R s0, a0, s1, a1, s2, a2, s3, a3, s4, a4 call fw_save_info MOV_5R a0, s0, a1, s1, a2, s2, a3, s3, a4, s4 初始化平台 /* * Initialize platform * Note: The a0 to a4 registers passed to the * firmware are parameters to this function. */ MOV_5R s0, a0, s1, a1, s2, a2, s3, a3, s4, a4 call fw_platform_init add t0, a0, zero MOV_5R a0, s0, a1, s1, a2, s2, a3, s3, a4, s4 add a1, t0, zero struct sbi_platform { /** * OpenSBI version this sbi_platform is based on. * It's a 32-bit value where upper 16-bits are major number * and lower 16-bits are minor number */ u32 opensbi_version; /** * OpenSBI platform version released by vendor. * It's a 32-bit value where upper 16-bits are major number * and lower 16-bits are minor number */ u32 platform_version; /** Name of the platform */ char name[64]; /** Supported features */ u64 features; /** Total number of HARTs */ u32 hart_count; /** Per-HART stack size for exception/interrupt handling */ u32 hart_stack_size; /** Pointer to sbi platform operations */ unsigned long platform_ops_addr; /** Pointer to system firmware specific context */ unsigned long firmware_context; /** * HART index to HART id table * * For used HART index <abc>: * hart_index2id[<abc>] = some HART id * For unused HART index <abc>: * hart_index2id[<abc>] = -1U * * If hart_index2id == NULL then we assume identity mapping * hart_index2id[<abc>] = <abc> * * We have only two restrictions: * 1. HART index < sbi_platform hart_count * 2. HART id < SBI_HARTMASK_MAX_BITS */ const u32 *hart_index2id; }; 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_scratch /** Representation of per-HART scratch space */ struct sbi_scratch { /** Start (or base) address of firmware linked to OpenSBI library */ unsigned long fw_start; /** Size (in bytes) of firmware linked to OpenSBI library */ unsigned long fw_size; /** Offset (in bytes) of the R/W section */ unsigned long fw_rw_offset; /** Arg1 (or 'a1' register) of next booting stage for this HART */ unsigned long next_arg1; /** Address of next booting stage for this HART */ unsigned long next_addr; /** Privilege mode of next booting stage for this HART */ unsigned long next_mode; /** Warm boot entry point address for this HART */ unsigned long warmboot_addr; /** Address of sbi_platform */ unsigned long platform_addr; /** Address of HART ID to sbi_scratch conversion function */ unsigned long hartid_to_scratch; /** Address of trap exit function */ unsigned long trap_exit; /** Temporary storage */ unsigned long tmp0; /** Options for OpenSBI library */ unsigned long options; }; 接下来的代码就是创建一段空间,给sbi_scratch,然后将里面的值填充。 /* Preload HART details * s7 -> HART Count * s8 -> HART Stack Size */ lla a4, platform # 加载platform的地址到a4寄存器 #if __riscv_xlen > 32 lwu s7, SBI_PLATFORM_HART_COUNT_OFFSET(a4) lwu s8, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(a4) #else lw s7, SBI_PLATFORM_HART_COUNT_OFFSET(a4) # 基于a4地址偏移80字节,即指向platform->hart_count:1?为啥不是128? lw s8, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(a4) # 基于a4地址偏移84字节,即指向platform->hart_stack_size:8192 #endif /* Setup scratch space for all the HARTs*/ lla tp, _fw_end # 加载_fw_end: 0x80038000 mul a5, s7, s8 # a5 = s7 *s8 = 1 * 0x2000 = 0x2000 add tp, tp, a5 # tp = tp +a5 = 0x80038000 + 0x2000 = 0x8003a000 /* Keep a copy of tp */ add t3, tp, zero # t3 = tp = 0x8003a000 /* Counter */ li t2, 1 /* hartid 0 is mandated by ISA */ li t1, 0 # tp 是 RISC-V 中的一个特殊寄存器,用于指向临时工作区域(scratch space)。 # 将 _fw_end 地址加载进 tp, 在用 s7,s8 计算出 scratch space, 再加上 tp, # 这样就相当于分配了一段hart的暂存空间。 _scratch_init: /* * The following registers hold values that are computed before * entering this block, and should remain unchanged. * * t3 -> the firmware end address * s7 -> HART count * s8 -> HART stack size */ add tp, t3, zero # tp = t3 = 0x8003a000 mul a5, s8, t1 # a5 = s8 *s1 = 0 sub tp, tp, a5 # tp = tp - a5 = 0x8003a000 li a5, SBI_SCRATCH_SIZE # 加载SBI_SRATCH_SIZE空间到a5,这里是0x1000 sub tp, tp, a5 # tp = tp - a5 = 0x80039000 /* Initialize scratch space */ /* Store fw_start and fw_size in scratch space */ lla a4, _fw_start sub a5, t3, a4 REG_S a4, SBI_SCRATCH_FW_START_OFFSET(tp) REG_S a5, SBI_SCRATCH_FW_SIZE_OFFSET(tp) # 将fw_start和fw_size写到scratch空间 /* Store R/W section's offset in scratch space */ lla a4, __fw_rw_offset REG_L a5, 0(a4) REG_S a5, SBI_SCRATCH_FW_RW_OFFSET(tp) # 将__fw_rw_offset= _fw_rw_start - _fw_start,写到scratch空间。 # 即data段的偏移位置。 /* Store next arg1 in scratch space */ MOV_3R s0, a0, s1, a1, s2, a2 call fw_next_arg1 REG_S a0, SBI_SCRATCH_NEXT_ARG1_OFFSET(tp) MOV_3R a0, s0, a1, s1, a2, s2 # 存储下一阶段启动的传递的第一个参数,这里是FW_JUMP_FDT_ADDR # 在platform/generic/objects.mk中配置,= ($(FW_TEXT_START) + 0x2200000)) # FW_JUMP_FDT_ADDR = 0x82200000 /* Store next address in scratch space */ MOV_3R s0, a0, s1, a1, s2, a2 call fw_next_addr REG_S a0, SBI_SCRATCH_NEXT_ADDR_OFFSET(tp) MOV_3R a0, s0, a1, s1, a2, s2 # 存储下一阶段启动地址,即opensbi运行阶段要跳转的地址 # 跳转的地址为FW_JUMP_ADDR,object.mk中设置 # = ($(FW_TEXT_START) + 0x200000)) # 所以FW_JUMP_ADDR = 0x80200000 /* Store next mode in scratch space */ MOV_3R s0, a0, s1, a1, s2, a2 call fw_next_mode REG_S a0, SBI_SCRATCH_NEXT_MODE_OFFSET(tp) MOV_3R a0, s0, a1, s1, a2, s2 # 将下一阶段启动的模式存储next_mode,这里是S模式:1 /* Store warm_boot address in scratch space */ lla a4, _start_warm REG_S a4, SBI_SCRATCH_WARMBOOT_ADDR_OFFSET(tp) # 存储warm_boot启动地址:0x80000354 # 即:firmware/fw_base.S::426行 /* Store platform address in scratch space */ lla a4, platform REG_S a4, SBI_SCRATCH_PLATFORM_ADDR_OFFSET(tp) # 存储platform的地址 /* Store hartid-to-scratch function address in scratch space */ lla a4, _hartid_to_scratch REG_S a4, SBI_SCRATCH_HARTID_TO_SCRATCH_OFFSET(tp) # 存储_hartid_to_scratch地址,这段是其他hard调用该函数用于分配scratch空间 /* Store trap-exit function address in scratch space */ lla a4, _trap_exit REG_S a4, SBI_SCRATCH_TRAP_EXIT_OFFSET(tp) # 存储的是trap退出函数 /* Clear tmp0 in scratch space */ REG_S zero, SBI_SCRATCH_TMP0_OFFSET(tp) /* Store firmware options in scratch space */ MOV_3R s0, a0, s1, a1, s2, a2 #ifdef FW_OPTIONS li a0, FW_OPTIONS #else call fw_options #endif REG_S a0, SBI_SCRATCH_OPTIONS_OFFSET(tp) MOV_3R a0, s0, a1, s1, a2, s2 /* Move to next scratch space */ add t1, t1, t2 blt t1, s7, _scratch_init # 完成一个hart的scrach初始化,接着为下一个hart分配一个scratch并初始化。 FDT重定位 /* * Relocate Flatened Device Tree (FDT) * source FDT address = previous arg1 * destination FDT address = next arg1 * * Note: We will preserve a0 and a1 passed by * previous booting stage. */ beqz a1, _fdt_reloc_done /* Mask values in a4 */ li a4, 0xff /* t1 = destination FDT start address */ MOV_3R s0, a0, s1, a1, s2, a2 call fw_next_arg1 add t1, a0, zero MOV_3R a0, s0, a1, s1, a2, s2 beqz t1, _fdt_reloc_done beq t1, a1, _fdt_reloc_done /* t0 = source FDT start address */ add t0, a1, zero /* t2 = source FDT size in big-endian */ #if __riscv_xlen == 64 lwu t2, 4(t0) #else lw t2, 4(t0) #endif /* t3 = bit[15:8] of FDT size */ add t3, t2, zero srli t3, t3, 16 and t3, t3, a4 slli t3, t3, 8 /* t4 = bit[23:16] of FDT size */ add t4, t2, zero srli t4, t4, 8 and t4, t4, a4 slli t4, t4, 16 /* t5 = bit[31:24] of FDT size */ add t5, t2, zero and t5, t5, a4 slli t5, t5, 24 /* t2 = bit[7:0] of FDT size */ srli t2, t2, 24 and t2, t2, a4 /* t2 = FDT size in little-endian */ or t2, t2, t3 or t2, t2, t4 or t2, t2, t5 /* t2 = destination FDT end address */ add t2, t1, t2 /* FDT copy loop */ ble t2, t1, _fdt_reloc_done _fdt_reloc_again: REG_L t3, 0(t0) REG_S t3, 0(t1) add t0, t0, __SIZEOF_POINTER__ add t1, t1, __SIZEOF_POINTER__ blt t1, t2, _fdt_reloc_again boot hard启动完成 _fdt_reloc_done: /* mark boot hart done */ li t0, BOOT_STATUS_BOOT_HART_DONE lla t1, _boot_status REG_S t0, 0(t1) fence rw, rw j _start_warm # boot hart启动完成,写入一个标志位,以便通知其他hart,跳转到热启动 非boot hart等待运行 /* waiting for boot hart to be done (_boot_status == 2) */ _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 hart是否运行初始化完,如果初始化 # 即可进入_start_warm,否则需要一直等待。 hart 热启动 _start_warm: /* Reset all registers for non-boot HARTs */ li ra, 0 call _reset_regs # 对于non-boot HARTS复位寄存器 /* Disable all interrupts */ csrw CSR_MIE, zero # 关闭所有的中断 /* Find HART count and HART stack size */ lla a4, platform #if __riscv_xlen == 64 lwu s7, SBI_PLATFORM_HART_COUNT_OFFSET(a4) lwu s8, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(a4) #else lw s7, SBI_PLATFORM_HART_COUNT_OFFSET(a4) lw s8, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(a4) #endif REG_L s9, SBI_PLATFORM_HART_INDEX2ID_OFFSET(a4) # 加载platform,获取到Hart count和hart stack size /* Find HART id */ csrr s6, CSR_MHARTID # 获取当前的hart id /* Find HART index */ beqz s9, 3f li a4, 0 1: #if __riscv_xlen == 64 lwu a5, (s9) #else lw a5, (s9) #endif beq a5, s6, 2f add s9, s9, 4 add a4, a4, 1 blt a4, s7, 1b li a4, -1 2: add s6, a4, zero 3: bge s6, s7, _start_hang /* Find the scratch space based on HART index */ lla tp, _fw_end mul a5, s7, s8 add tp, tp, a5 mul a5, s8, s6 sub tp, tp, a5 li a5, SBI_SCRATCH_SIZE sub tp, tp, a5 # 根据hart index找到对应的scratch space /* update the mscratch */ csrw CSR_MSCRATCH, tp # 将scratch地址更新到mscratch寄存器中 /* Setup stack */ add sp, tp, zero # 设置hart的栈空间 /* Setup trap handler */ lla a4, _trap_handler #if __riscv_xlen == 32 csrr a5, CSR_MISA srli a5, a5, ('H' - 'A') andi a5, a5, 0x1 beq a5, zero, _skip_trap_handler_rv32_hyp lla a4, _trap_handler_rv32_hyp _skip_trap_handler_rv32_hyp: #endif csrw CSR_MTVEC, a4 # 设置trap处理入口函数_trap_handler #if __riscv_xlen == 32 /* Override trap exit for H-extension */ csrr a5, CSR_MISA srli a5, a5, ('H' - 'A') andi a5, a5, 0x1 beq a5, zero, _skip_trap_exit_rv32_hyp lla a4, _trap_exit_rv32_hyp csrr a5, CSR_MSCRATCH REG_S a4, SBI_SCRATCH_TRAP_EXIT_OFFSET(a5) _skip_trap_exit_rv32_hyp: #endif /* Initialize SBI runtime */ csrr a0, CSR_MSCRATCH call sbi_init # scratch的地址作为参数,跳转到sbi_init执行。 小结 本文参考: [泰晓科技OpenSBI分析1](https://tinylab.org/sbi-firmware-analyze-41/ \"泰晓科技OpenSBI分析1\") [泰晓科技OpenSBI分析2](https://tinylab.org/sbi-firmware-analyze-2/ \"泰晓科技OpenSBI分析2\") [泰晓科技OpenSBI分析3](https://tinylab.org/sbi-firmware-analyze-3/ \"泰晓科技OpenSBI分析3\") [泰晓科技OpenSBI分析4](https://tinylab.org/sbi-firmware-analyze-4/ \"泰晓科技OpenSBI分析4\") [passenger12234的博客 OpenSBI分析](https://blog.csdn.net/passenger12234/category_11411139.html \"passenger12234的博客 OpenSBI分析\") [OpenSBI ELF rela.dyn和.dynsym动态链接过程](https://blog.csdn.net/dai_xiangjun/article/details/123629743 \"OpenSBI ELF rela.dyn和.dynsym动态链接过程\")
- 
					
						  qemu+opensbi+uboot+linux+busybox启动环境搭建前置条件 qemu+工具链安装 安装好交叉编译工具链和qemu环境,参考:http://www.laumy.tech/1127.html#risc-v64_Xuantie 代码下载 opensbi:[version: v1.2](https://gitee.com/tinylab/qemu-opensbi \"version: v1.2\") uboot: [version: v2022.04](https://gitee.com/mirrors/u-boot \"v2022.04\") kernel: [version: v5.18](https://gitee.com/mirrors/linux_old1 \"version: v5.18\") busybox:[ version: v1.37.0.git](https://gitee.com/mirrors/busyboxsource \" version: v1.37.0.git\") 编译opensbi export CROSS_COMPILE=riscv64-unknown-linux-gnu- make all PLATFORM=generic PLATFORM_RISCV_XLEN=64 bin路径:build/platform/generic/firmware/fw_jump.bin 编译uboot export CROSS_COMPILE=riscv64-unknown-linux-gnu- make qemu-riscv64_smode_defconfig make -j 2 适用默认qemu自带的opensbi,引导uboot镜像 cd uboot qemu-system-riscv64 -M virt -smp 4 -m 2G \\ -display none -serial stdio -kernel u-boot.bin 使用自行编译的opensbi进行引导uboot,需要在OpenSBI 编译时指定 U-Boot的路径,如下: cd ../qemu-opensbi export CROSS_COMPILE=riscv64-unknown-linux-gnu- make PLATFORM=generic FW_PAYLOAD_PATH=../u-boot-v2022.04/u-boot.bin 编译完成后运行 qemu-system-riscv64 -M virt -m 256M -nographic \\ -bios qemu-opensbi/build/platform/generic/firmware/fw_payload.elf 编译linux cd linux-5.18/ make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- defconfig make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- -j 48 固件生成:arch/riscv/boot/Image 编译busybox cd busyboxsource export CROSS_COMPILE=riscv64-unknown-linux-gnu- make defconfig make menuconfig Settings-->Build Options [*] Build static binary (no shared libs) ---使能,这样就不用拷贝lib make -j 48 make install 编译生成的命令:_install 制作根文件系统 cd ../ qemu-img create rootfs.img 1g ---创建一个文件 mkfs.ext4 rootfs.img ---将文件格式为ext4格式 mkdir rootfs sudo mount -o loop rootfs.img rootfs ---挂在文件到rootfs下,将命令等拷贝进去 cd rootfs sudo cp -r ../busyboxsource/_install/* . sudo mkdir proc sys dev etc etc/init.d cd etc/init.d/ sudo touch rcS sudo vi rcS 如果要执行要进行交叉编译把库文件拷贝进去 cp -rf Xuantie-900-gcc-linux-5.10.4-glibc-x86_64-V2.8.1/sysroot/lib* . rcS文件内容如下: #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys /sbin/mdev -s sudo chmod +x rcS cd ~ sudo umount rootfs 启动内核 #!/bin/sh qemu-system-riscv64 -M virt -cpu c910v -m 256M -nographic \\ -bios qemu-opensbi/build/platform/generic/firmware/fw_jump.bin \\ -kernel linux-5.18/arch/riscv/boot/Image \\ -drive file=rootfs.img,format=raw,id=hd0 \\ -device virtio-blk-device,drive=hd0 \\ -append \"root=/dev/vda rw console=ttyS0\" qemu虚拟机和宿主机传输文件 qemu启动参数加上下面命令 -virtfs local,path=/mnt/shared,mount_tag=host0,security_model=passthrough,id=host0 -virtfs选项指定了共享文件夹的参数 local表示共享文件夹是本地文件夹 path指定了共享文件夹的路径 mount_tag指定了共享文件夹在虚拟机中的挂载点 security_model”指定了安全模型 id是共享文件夹的标识符。 宿主机执行 mkdir -p /mnt/shared mount -t 9p -o trans=virtio,version=9p2000.L host0 /mnt/shared 本文参考:[tinylab:YJMSTR](https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220816-introduction-to-qemu-and-riscv-upstream-boot-flow.md#https://gitee.com/link?target=https%3A%2F%2Ftinylab.org%2Friscv-uefi-part1%2F \"tinylab:YJMSTR\")
- 
					
						  RISC-V架构与FreeRTOS任务栈变化栈的基本概念 在FreeRTOS中,每个任务有一个全局的tskTCB实例,pxCurrentTCB指针指向的是正在运行的任务实例,有三个和栈相关的变量pxTopOfStack和pxStack,pxEndOfStack。 pxTopOfStack指向当前堆栈栈顶,随着进栈出栈,pxTopOfStack指向的位置是会变化的; pxStack指向当前堆栈的起始位置,一经分配后,堆栈起始位置就固定了,不会被改变了。 pxEndOfStack 标记着栈结束位置,任务创建完成后就固定了 adress:0+ulStackDepth ------------------- pxEndOfStack(栈底) |XXXXXXXXXXXXXXXXXX| adress:0+x --------------------pxTopOfStack(栈顶) | | | | | | adress:0 ------------------- pxStack 任务栈空间开辟 xTaskCreate 1. 分配一个栈空间 /* Allocate space for the stack used by the task being created. */ pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); pxNewTCB->pxStack = pxStack; 2. 初始化新任务 prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL ); 2.1 更新pxTopOfStack pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] ); pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) ); 2.2 初始化栈 pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); 初始化栈后,pxNewTCB->pxTopOfStack = pxTopOfStack - Context Size 初始化栈 pxPortInitialiseStack: #if defined(CONFIG_ARCH_RISCV_FPU) && !defined(CONFIG_SAVE_RISCV_FPU_CONTEXT_IN_TCB) addi a0, a0, -(FPU_CONTEXT_SIZE) /* set fpu rounding mode field to 0(Round to Nearest, ties to Even) when create new thread, * otherwise it\'s possible that the rounding mode filed is invalid and * cause the illegal instruction exception when restore the fcsr. */ store_x x0, FPU_CTX_FCSR_OFFSET(a0) #endif addi a0, a0, -portWORD_SIZE addi a0, a0, -portWORD_SIZE addi a0, a0, -portWORD_SIZE addi a0, a0, -portWORD_SIZE csrr t0, mstatus /* Obtain current mstatus value. */ andi t0, t0, ~0x8 /* Ensure interrupts are disabled when the stack is restored within an ISR. Required when a task is created after the schedulre has been started, otherwise interrupts would be disabled anyway. */ addi t1, x0, 0x188 /* Generate the value 0x1880, which are the MPIE and MPP bits to set in mstatus. */ slli t1, t1, 4 or t0, t0, t1 /* Set MPIE and MPP bits in mstatus value. */ #ifdef CONFIG_SAVE_RISCV_FPU_CONTEXT_IN_TCB /* set FS filed(in thread stack) to initial state for skip save/restore FPU context operation */ li t1, SR_FS li t2, SR_FS_INITIAL not t3, t1 and t0, t0, t3 or t0, t0, t2 #endif addi a0, a0, -portWORD_SIZE store_x t0, 0(a0) /* mstatus onto the stack. */ addi a0, a0, -(22 * portWORD_SIZE) /* Space for registers x11-x31. */ store_x a2, 0(a0) /* Task parameters (pvParameters parameter) goes into register X10/a0 on the stack. */ addi a0, a0, -(6 * portWORD_SIZE) /* Space for registers x5-x9. */ la t0, portTASK_RETURN_ADDRESS store_x t0, 0(a0) /* Return address onto the stack, could be portTASK_RETURN_ADDRESS */ addi t0, x0, portasmADDITIONAL_CONTEXT_SIZE /* The number of chip specific additional registers. */ chip_specific_stack_frame: /* First add any chip specific registers to the stack frame being created. */ beq t0, x0, 1f /* No more chip specific registers to save. */ addi a0, a0, -portWORD_SIZE /* Make space for chip specific register. */ store_x x0, 0(a0) /* Give the chip specific register an initial value of zero. */ addi t0, t0, -1 /* Decrement the count of chip specific registers remaining. */ j chip_specific_stack_frame /* Until no more chip specific registers. */ 1: addi a0, a0, -portWORD_SIZE store_x a1, 0(a0) /* mret value (pxCode parameter) onto the stack. */ ret .endfunc 经过pxPortInitialiseStack初始化后存储布局如下: 第一个任务栈恢复 xPortStartFirstTask: #if( portasmHAS_SIFIVE_CLINT != 0 ) /* If there is a clint then interrupts can branch directly to the FreeRTOS trap handler. Otherwise the interrupt controller will need to be configured outside of this file. */ la t0, freertos_risc_v_trap_handler csrw mtvec, t0 #endif /* portasmHAS_CLILNT */ load_x t0, pxCurrentTCB /* 加载当前TCB */ load_x sp, 0 * portWORD_SIZE( t0 ) /* 获取当前TCB的栈空间*/ load_x x1, 0 * portWORD_SIZE( sp ) /* 加载返回地址,即任务入口函数 */ load_x x6, 3 * portWORD_SIZE( sp ) /* t1 */ load_x x7, 4 * portWORD_SIZE( sp ) /* t2 */ load_x x8, 5 * portWORD_SIZE( sp ) /* s0/fp */ load_x x9, 6 * portWORD_SIZE( sp ) /* s1 */ load_x x10, 7 * portWORD_SIZE( sp ) /* a0 */ load_x x11, 8 * portWORD_SIZE( sp ) /* a1 */ load_x x12, 9 * portWORD_SIZE( sp ) /* a2 */ load_x x13, 10 * portWORD_SIZE( sp ) /* a3 */ load_x x14, 11 * portWORD_SIZE( sp ) /* a4 */ load_x x15, 12 * portWORD_SIZE( sp ) /* a5 */ load_x x16, 13 * portWORD_SIZE( sp ) /* a6 */ load_x x17, 14 * portWORD_SIZE( sp ) /* a7 */ load_x x18, 15 * portWORD_SIZE( sp ) /* s2 */ load_x x19, 16 * portWORD_SIZE( sp ) /* s3 */ load_x x20, 17 * portWORD_SIZE( sp ) /* s4 */ load_x x21, 18 * portWORD_SIZE( sp ) /* s5 */ load_x x22, 19 * portWORD_SIZE( sp ) /* s6 */ load_x x23, 20 * portWORD_SIZE( sp ) /* s7 */ load_x x24, 21 * portWORD_SIZE( sp ) /* s8 */ load_x x25, 22 * portWORD_SIZE( sp ) /* s9 */ load_x x26, 23 * portWORD_SIZE( sp ) /* s10 */ load_x x27, 24 * portWORD_SIZE( sp ) /* s11 */ load_x x28, 25 * portWORD_SIZE( sp ) /* t3 */ load_x x29, 26 * portWORD_SIZE( sp ) /* t4 */ load_x x30, 27 * portWORD_SIZE( sp ) /* t5 */ load_x x31, 28 * portWORD_SIZE( sp ) /* t6 */ load_x x5, 29 * portWORD_SIZE( sp ) /* Initial mstatus into x5 (t0) */ addi x5, x5, 0x08 /* Set MIE bit so the first task starts with interrupts enabled - required as returns with ret not eret. */ csrrw x0, mstatus, x5 /* Interrupts enabled from here! */ load_x x5, 2 * portWORD_SIZE( sp ) /* Initial x5 (t0) value. */ addi sp, sp, portCONTEXT_SIZE /*销毁栈空间,SP已经更新*/ #if 0 load_x t0, pxCurrentTCB /* Load pxCurrentTCB. */ store_x sp, 0( t0 ) /* Write sp to first TCB member. */ #endif 更新栈顶指针,上面两句不执行也行,因为任务执行时,sp的值已经更新了,任务用的是SP, 而不是pxCurrentTCB->pxTopOfStack。 ret .endfunc /*-----------------------------------------------------------*/ 任务切换栈变化 clic_interrupt_handler: 1. 任务在运行过程中,被中断打断,先分配一块栈帧,保存现场。 addi sp, sp, -portCONTEXT_SIZE store_x x1, 1 * portWORD_SIZE( sp ) store_x x5, 2 * portWORD_SIZE( sp ) store_x x6, 3 * portWORD_SIZE( sp ) store_x x7, 4 * portWORD_SIZE( sp ) store_x x8, 5 * portWORD_SIZE( sp ) store_x x9, 6 * portWORD_SIZE( sp ) store_x x10, 7 * portWORD_SIZE( sp ) store_x x11, 8 * portWORD_SIZE( sp ) store_x x12, 9 * portWORD_SIZE( sp ) store_x x13, 10 * portWORD_SIZE( sp ) store_x x14, 11 * portWORD_SIZE( sp ) store_x x15, 12 * portWORD_SIZE( sp ) store_x x16, 13 * portWORD_SIZE( sp ) store_x x17, 14 * portWORD_SIZE( sp ) store_x x18, 15 * portWORD_SIZE( sp ) store_x x19, 16 * portWORD_SIZE( sp ) store_x x20, 17 * portWORD_SIZE( sp ) store_x x21, 18 * portWORD_SIZE( sp ) store_x x22, 19 * portWORD_SIZE( sp ) store_x x23, 20 * portWORD_SIZE( sp ) store_x x24, 21 * portWORD_SIZE( sp ) store_x x25, 22 * portWORD_SIZE( sp ) store_x x26, 23 * portWORD_SIZE( sp ) store_x x27, 24 * portWORD_SIZE( sp ) store_x x28, 25 * portWORD_SIZE( sp ) store_x x29, 26 * portWORD_SIZE( sp ) store_x x30, 27 * portWORD_SIZE( sp ) store_x x31, 28 * portWORD_SIZE( sp ) store_x x3, 31 * portWORD_SIZE( sp ) store_x x4, 32 * portWORD_SIZE( sp ) addi a0, sp, portCONTEXT_SIZE store_x a0, 30 * portWORD_SIZE( sp ) csrr a0, mepc store_x a0, 0 * portWORD_SIZE( sp ) csrr t0, mscratch store_x t0, 33 * portWORD_SIZE( sp ) csrr t0, mstatus store_x t0, 29 * portWORD_SIZE( sp ) 2. 因为分配了一段栈空间,所以需要将栈顶指针更新到pxCurrentTCB->pxTopOfStack 到这里,这样任务的上下文就保存好了,可以运行中断函数,或切换到其他任务。 lw t0, pxCurrentTCB /* Load pxCurrentTCB. */ sw sp, 0( t0 ) /* Write sp to first TCB member. */ 3. 加载中断的栈空间,执行中断处理函数。 load_x sp, xISRStackTop call enter_interrupt_handler csrr a0, mcause andi a0, a0, 0x7FF /* If the CLIC support more than 2048 interrupts, we need to use the mask value saved in register */ call irq_core_handle_root_ic_irq call exit_interrupt_handler 4. 获取任务的控制块,这里的任务可能是原来的任务,也可能是新的任务 在中断处理函数中,会进行调度,更新pxCurrentTCB,之后获取pxCurrentTCB->pxTopOfStack 这样就找到了新任务的栈空间 lw t1, pxCurrentTCB /* Load pxCurrentTCB. */ lw sp, 0( t1 ) /* Read sp from first TCB member. */ 5. 栈空间的第一个保存的开始要运行的地址,加载出来写入到mepc中 load_x t0, 0 * portWORD_SIZE( sp ) csrw mepc, t0 /* Load mstatus with the interrupt enable bits used by the task. */ load_x t0, 29 * portWORD_SIZE( sp ) csrw mstatus, t0 load_x t0, 33 * portWORD_SIZE( sp ) csrw mscratch, t0 load_x x1 , 1 * portWORD_SIZE( sp ) load_x x5 , 2 * portWORD_SIZE( sp ) /* t0 */ load_x x6 , 3 * portWORD_SIZE( sp ) /* t1 */ load_x x7 , 4 * portWORD_SIZE( sp ) /* t2 */ load_x x8 , 5 * portWORD_SIZE( sp ) /* s0/fp */ load_x x9 , 6 * portWORD_SIZE( sp ) /* s1 */ load_x x10, 7 * portWORD_SIZE( sp ) /* a0 */ load_x x11, 8 * portWORD_SIZE( sp ) /* a1 */ load_x x12, 9 * portWORD_SIZE( sp ) /* a2 */ load_x x13, 10 * portWORD_SIZE( sp ) /* a3 */ load_x x14, 11 * portWORD_SIZE( sp ) /* a4 */ load_x x15, 12 * portWORD_SIZE( sp ) /* a5 */ load_x x16, 13 * portWORD_SIZE( sp ) /* a6 */ load_x x17, 14 * portWORD_SIZE( sp ) /* a7 */ load_x x18, 15 * portWORD_SIZE( sp ) /* s2 */ load_x x19, 16 * portWORD_SIZE( sp ) /* s3 */ load_x x20, 17 * portWORD_SIZE( sp ) /* s4 */ load_x x21, 18 * portWORD_SIZE( sp ) /* s5 */ load_x x22, 19 * portWORD_SIZE( sp ) /* s6 */ load_x x23, 20 * portWORD_SIZE( sp ) /* s7 */ load_x x24, 21 * portWORD_SIZE( sp ) /* s8 */ load_x x25, 22 * portWORD_SIZE( sp ) /* s9 */ load_x x26, 23 * portWORD_SIZE( sp ) /* s10 */ load_x x27, 24 * portWORD_SIZE( sp ) /* s11 */ load_x x28, 25 * portWORD_SIZE( sp ) /* t3 */ load_x x29, 26 * portWORD_SIZE( sp ) /* t4 */ load_x x30, 27 * portWORD_SIZE( sp ) /* t5 */ load_x x31, 28 * portWORD_SIZE( sp ) /* t6 */ 6.上下文已经恢复,销毁掉栈空间 addi sp, sp, portCONTEXT_SIZE 7. 下面两句,可以不执行,因为mret之后就切到任务中运行了,sp已经更新。 #if 0 load_x t0, pxCurrentTCB /* Load pxCurrentTCB. */ store_x sp, 0( t0 ) /* Write sp to first TCB member. */ #endif 8. 返回到任务中运行, mret pxCurrentTCB->pxTopOfStack的变化 pxPortInitialiseStack初始化栈,分配一段栈空间,设置pxCurrentTCB->pxTopOfStack初值。这段栈空间在任务得到运行时会将pxCurrentTCB->pxTopOfStack赋值为sp,接着恢复上下文运行。任务得到运行有两种场景 xPortStartFirstTask:这种任务是优先级比较高,系统初始化完成后会挑选第一个任务进行运行,在该函数中会恢复上下文,接着更新sp的值进而销毁栈空间,但是没有更新pxCurrentTCB->pxTopOfStack的值(该值是否更新不影响,sp已经更新了)。 其他任务让出得到调度:这种场景是得到调度机会,在中断中进行恢复上下文(上面代码的第4点点开始), 运行过程中被中断打断(可能是中断,也有可能是时间片用完),分配一段栈空间用于保存上下文信息。上面代码第1点就是开辟一段栈空间,进行保存上下文,保存上下文后,因为可能会让出调度,因此需要更新pxCurrentTCB->pxTopOfStack,上述代码的第二点。 为什么只看到第1点中SP开辟了,更新到pxCurrentTCB->pxTopOfStack后,没有见哪里归还,是否有栈泄露? 答:在任务再次得到运行的时候,会进行归还,上面第6点就是,SP进行的增加,释放的栈空间,只是没有写到pxCurrentTCB->pxTopOfStack,而当任务在运行过程中被打断进入中断后,sp是已经释放后的sp,而即使在第1点中开辟空间,也是归还后,再开辟的,所以不存在泄露。 跟踪一个任务运行栈的变化实例 初始化栈 初始化时开辟了一个栈空间,栈帧指向0x6007af78位置。 调度得到运行 恢复上下文
- 
					
						  平头哥E90X中断处理汇编部分任务上下文保存 addi sp, sp, -portCONTEXT_SIZE store_x x1, 1 * portWORD_SIZE( sp ) store_x x5, 2 * portWORD_SIZE( sp ) store_x x6, 3 * portWORD_SIZE( sp ) store_x x7, 4 * portWORD_SIZE( sp ) store_x x8, 5 * portWORD_SIZE( sp ) store_x x9, 6 * portWORD_SIZE( sp ) store_x x10, 7 * portWORD_SIZE( sp ) store_x x11, 8 * portWORD_SIZE( sp ) store_x x12, 9 * portWORD_SIZE( sp ) store_x x13, 10 * portWORD_SIZE( sp ) store_x x14, 11 * portWORD_SIZE( sp ) store_x x15, 12 * portWORD_SIZE( sp ) store_x x16, 13 * portWORD_SIZE( sp ) store_x x17, 14 * portWORD_SIZE( sp ) store_x x18, 15 * portWORD_SIZE( sp ) store_x x19, 16 * portWORD_SIZE( sp ) store_x x20, 17 * portWORD_SIZE( sp ) store_x x21, 18 * portWORD_SIZE( sp ) store_x x22, 19 * portWORD_SIZE( sp ) store_x x23, 20 * portWORD_SIZE( sp ) store_x x24, 21 * portWORD_SIZE( sp ) store_x x25, 22 * portWORD_SIZE( sp ) store_x x26, 23 * portWORD_SIZE( sp ) store_x x27, 24 * portWORD_SIZE( sp ) store_x x28, 25 * portWORD_SIZE( sp ) store_x x29, 26 * portWORD_SIZE( sp ) store_x x30, 27 * portWORD_SIZE( sp ) store_x x31, 28 * portWORD_SIZE( sp ) store_x x3, 31 * portWORD_SIZE( sp ) store_x x4, 32 * portWORD_SIZE( sp ) addi a0, sp, portCONTEXT_SIZE store_x a0, 30 * portWORD_SIZE( sp ) csrr a0, mepc store_x a0, 0 * portWORD_SIZE( sp ) ----中断返回执行的地址指令存储到栈中 csrr t0, mscratch store_x t0, 33 * portWORD_SIZE( sp ) csrr t0, mstatus store_x t0, 29 * portWORD_SIZE( sp ) lw t0, pxCurrentTCB /* Load pxCurrentTCB. */ sw sp, 0( t0 ) /* Write sp to first TCB member. */ 检查是否为中断并跳转 load_x sp, xISRStackTop ----设置中断栈,中断与用户是不同栈 call enter_interrupt_handler csrr a0, mcause andi a0, a0, 0x7FF ---按位与,只取低11位,低11位也有2048个中断了 call irq_core_handle_root_ic_irq --跳转到中断处理,参数为a0 call exit_interrupt_handler 任务上下文恢复 lw t1, pxCurrentTCB /* Load pxCurrentTCB. */ lw sp, 0( t1 ) /* Read sp from first TCB member. */ 中断函数处理完成后,准备恢复任务的上下文,需要先获取sp 中断用的是中断的栈 load_x t0, 0 * portWORD_SIZE( sp ) csrw mepc, t0 ----中断返回后,跳转执行的地址指令 /* Load mstatus with the interrupt enable bits used by the task. */ load_x t0, 29 * portWORD_SIZE( sp ) csrw mstatus, t0 ----恢复mstatus,使能中断 load_x t0, 33 * portWORD_SIZE( sp ) csrw mscratch, t0 load_x x1 , 1 * portWORD_SIZE( sp ) load_x x5 , 2 * portWORD_SIZE( sp ) /* t0 */ load_x x6 , 3 * portWORD_SIZE( sp ) /* t1 */ load_x x7 , 4 * portWORD_SIZE( sp ) /* t2 */ load_x x8 , 5 * portWORD_SIZE( sp ) /* s0/fp */ load_x x9 , 6 * portWORD_SIZE( sp ) /* s1 */ load_x x10, 7 * portWORD_SIZE( sp ) /* a0 */ load_x x11, 8 * portWORD_SIZE( sp ) /* a1 */ load_x x12, 9 * portWORD_SIZE( sp ) /* a2 */ load_x x13, 10 * portWORD_SIZE( sp ) /* a3 */ load_x x14, 11 * portWORD_SIZE( sp ) /* a4 */ load_x x15, 12 * portWORD_SIZE( sp ) /* a5 */ load_x x16, 13 * portWORD_SIZE( sp ) /* a6 */ load_x x17, 14 * portWORD_SIZE( sp ) /* a7 */ load_x x18, 15 * portWORD_SIZE( sp ) /* s2 */ load_x x19, 16 * portWORD_SIZE( sp ) /* s3 */ load_x x20, 17 * portWORD_SIZE( sp ) /* s4 */ load_x x21, 18 * portWORD_SIZE( sp ) /* s5 */ load_x x22, 19 * portWORD_SIZE( sp ) /* s6 */ load_x x23, 20 * portWORD_SIZE( sp ) /* s7 */ load_x x24, 21 * portWORD_SIZE( sp ) /* s8 */ load_x x25, 22 * portWORD_SIZE( sp ) /* s9 */ load_x x26, 23 * portWORD_SIZE( sp ) /* s10 */ load_x x27, 24 * portWORD_SIZE( sp ) /* s11 */ load_x x28, 25 * portWORD_SIZE( sp ) /* t3 */ load_x x29, 26 * portWORD_SIZE( sp ) /* t4 */ load_x x30, 27 * portWORD_SIZE( sp ) /* t5 */ load_x x31, 28 * portWORD_SIZE( sp ) /* t6 */ addi sp, sp, portCONTEXT_SIZE ----销毁栈空间 mret ---中断返回
- 
					
						  平头哥E90X异常处理上下文入栈保存 addi sp, sp, -portCONTEXT_SIZE ----开辟一段栈空间,这里的sp可能是主栈或任务栈 store_x x1, 1 * portWORD_SIZE( sp ) ----依次存储相关寄存器 store_x x5, 2 * portWORD_SIZE( sp ) store_x x6, 3 * portWORD_SIZE( sp ) store_x x7, 4 * portWORD_SIZE( sp ) store_x x8, 5 * portWORD_SIZE( sp ) store_x x9, 6 * portWORD_SIZE( sp ) store_x x10, 7 * portWORD_SIZE( sp ) store_x x11, 8 * portWORD_SIZE( sp ) store_x x12, 9 * portWORD_SIZE( sp ) store_x x13, 10 * portWORD_SIZE( sp ) store_x x14, 11 * portWORD_SIZE( sp ) store_x x15, 12 * portWORD_SIZE( sp ) store_x x16, 13 * portWORD_SIZE( sp ) store_x x17, 14 * portWORD_SIZE( sp ) store_x x18, 15 * portWORD_SIZE( sp ) store_x x19, 16 * portWORD_SIZE( sp ) store_x x20, 17 * portWORD_SIZE( sp ) store_x x21, 18 * portWORD_SIZE( sp ) store_x x22, 19 * portWORD_SIZE( sp ) store_x x23, 20 * portWORD_SIZE( sp ) store_x x24, 21 * portWORD_SIZE( sp ) store_x x25, 22 * portWORD_SIZE( sp ) store_x x26, 23 * portWORD_SIZE( sp ) store_x x27, 24 * portWORD_SIZE( sp ) store_x x28, 25 * portWORD_SIZE( sp ) store_x x29, 26 * portWORD_SIZE( sp ) store_x x30, 27 * portWORD_SIZE( sp ) store_x x31, 28 * portWORD_SIZE( sp ) store_x x3, 31 * portWORD_SIZE( sp ) store_x x4, 32 * portWORD_SIZE( sp ) mv a0, sp addi a0, a0, portCONTEXT_SIZE store_x a0, 30 * portWORD_SIZE( sp ) ---将栈起始地址保存到栈空间中 csrr a0, mepc store_x a0, 0 * portWORD_SIZE( sp ) ---将mepc保存到栈空间中 csrr t0, mscratch store_x t0, 33 * portWORD_SIZE( sp ) ---将mscratch保存到栈空间中 csrr t0, mstatus /* Required for MPIE bit. */ store_x t0, 29 * portWORD_SIZE( sp ) 将mstatus保存到栈空间中 load_x t0, pxCurrentTCB /* Load pxCurrentTCB. */ store_x sp, 0( t0 ) /* Write sp to first TCB member. */ 将sp写入pxCurrentTCB->pxTopOfStack 即将sp保存到任务的控制块中,每个任务都有一个全局的控制块 当恢复的时候,也是通过pxCurrentTCB先获取到sp。 检查是同步异常 csrr a0, mcause ---a0=mcause csrr a1, mepc ---a1=mepc test_if_asynchronous: srli a2, a0, __riscv_xlen - 1 --将a0右移63位(64位系统),即获取mcause最高位,赋值为a2 beq a2, x0, handle_synchronous --判断a2是否为0,如果是0,则是同步异常,跳转处理,否则是中断。 异常处理 handle_synchronous: csrr t0, mcause --获取mcasue到t0中 andi t0, t0, 0x7ff mv a0, t0 addi a1, a1, 4 store_x a1, 0( sp ) test_if_environment_call: li t0, 11 bne a0, t0, is_exception load_x sp, xISRStackTop call vTaskSwitchContext j processed_source is_exception: csrr t0, mcause csrr t1, mepc csrr t2, mtval mv a0, t0 mv a1, t1 mv a2, t2 mv a3, sp call enter_interrupt_handler call riscv_cpu_handle_exception ----异常处理函数 call exit_interrupt_handler j is_exception 上下文出栈 load_x t1, pxCurrentTCB /* Load pxCurrentTCB. */ load_x sp, 0( t1 ) /* Read sp from first TCB member. */ load_x t0, 0( sp ) --获取中断返回后,要执行的PC,其保存在栈的第一个位置 csrw mepc, t0 /* Load mstatus with the interrupt enable bits used by the task. */ load_x t0, 29 * portWORD_SIZE( sp ) csrw mstatus, t0 恢复mstatus(打开中断) load_x x1, 1 * portWORD_SIZE( sp ) load_x x5, 2 * portWORD_SIZE( sp ) /* t0 */ load_x x6, 3 * portWORD_SIZE( sp ) /* t1 */ load_x x7, 4 * portWORD_SIZE( sp ) /* t2 */ load_x x8, 5 * portWORD_SIZE( sp ) /* s0/fp */ load_x x9, 6 * portWORD_SIZE( sp ) /* s1 */ load_x x10, 7 * portWORD_SIZE( sp ) /* a0 */ load_x x11, 8 * portWORD_SIZE( sp ) /* a1 */ load_x x12, 9 * portWORD_SIZE( sp ) /* a2 */ load_x x13, 10 * portWORD_SIZE( sp ) /* a3 */ load_x x14, 11 * portWORD_SIZE( sp ) /* a4 */ load_x x15, 12 * portWORD_SIZE( sp ) /* a5 */ load_x x16, 13 * portWORD_SIZE( sp ) /* a6 */ load_x x17, 14 * portWORD_SIZE( sp ) /* a7 */ load_x x18, 15 * portWORD_SIZE( sp ) /* s2 */ load_x x19, 16 * portWORD_SIZE( sp ) /* s3 */ load_x x20, 17 * portWORD_SIZE( sp ) /* s4 */ load_x x21, 18 * portWORD_SIZE( sp ) /* s5 */ load_x x22, 19 * portWORD_SIZE( sp ) /* s6 */ load_x x23, 20 * portWORD_SIZE( sp ) /* s7 */ load_x x24, 21 * portWORD_SIZE( sp ) /* s8 */ load_x x25, 22 * portWORD_SIZE( sp ) /* s9 */ load_x x26, 23 * portWORD_SIZE( sp ) /* s10 */ load_x x27, 24 * portWORD_SIZE( sp ) /* s11 */ load_x x28, 25 * portWORD_SIZE( sp ) /* t3 */ load_x x29, 26 * portWORD_SIZE( sp ) /* t4 */ load_x x30, 27 * portWORD_SIZE( sp ) /* t5 */ load_x x31, 28 * portWORD_SIZE( sp ) /* t6 */ addi sp, sp, portCONTEXT_SIZE -----栈空间释放 mret ---中断返回
- 
					
						  平头哥E90X的head.S分析关闭中断 /* disable interrupt */ li t0, MR_MIE --- t0=0x08 csrc mstatus, t0 --- mstatus = mstatus | ~t0 csrw mie, zero --- mie = zero mstatus第4bit清0,mie清0。 设置异常处理入口 /* setup the address of exception handler */ la a0, risc_v_trap_handler ori a0, a0, 3 csrw mtvec, a0 E90X异常和中断使用了CLINT和CLIC两个控制器,对于异常的服务程序入口函数有MTVEC寄存器指定,这里的异常指的是同步异常,这类异常没法屏蔽,通常是执行指令、取指令、非法内存访问等的异常。MTVEC寄存器的低2位,设置为3,表示使用模式3,该模式下CPU使用MTVEC[31:6]<<6作为异常(同步异常)的服务程序入口地址并跳转执行。 设置中断处理入口 /* setup the address of vector interrupt handler */ la a0, interrupt_vectors csrw mtvt, a0 当MTVEC的低两位设置MODE[1:0]=3时,CPU使用MTVEC[31:6]<<6作为异常(同步异常)的服务程序入口地址并跳转执行,而对于中断服务程序入口分两种情况,当使能了CLIC.CLICINTATTR中的shv域设置是否为硬件矢量中断,如果是硬件矢量中断,CPU首先使用MTVT+4*中断ID为地址,去取中断服务程序入口地址,并跳转到该入口地址执行,如果是非矢量中断,CPU使用MTVEC[31:6]<<作为中断服务程序入口地址并跳转执行。MTVT为矢量中断基址寄存器,在CLIC模式存在。在E907中MODE[1:0]硬件固定设置为3,软件不可设置。 中断向量表 .section .vectors, \"xx\", @progbits .balign 64 .globl clic_interrupt_vectors .type clic_interrupt_vectors, @object clic_interrupt_vectors: .rept (PLAT_CLIC_IRQ_CNT) .long clic_interrupt_handler .endr .text 这里默认设置为中断硬件矢量模式,所以需要注册向量表,但是这里向量表统一入口为clic_interrupt_handler。 清除BSS段 # clear mscratch register. csrw mscratch,zero /* Clear bss section */ la a0, __bss_start__ la a1, __bss_end__ bgeu a0, a1, 2f --- 2f标签后面跟着一个字符(如f)是一种常见的命名约定,帮助区分不同类型的标签,这里暗示这是一个向前跳转的目标(forward jump target),如果a0>=a1,向前跳转到标签2的地方。 1: sw zero, (a0) addi a0, a0, 4 bltu a0, a1, 1b ---1bb也是一个标签,但与之前的2f不同,它指示了一个向后跳转(backward jump)的目标。这里的b后缀表明这是一个向后的跳转,意味着如果条件满足(即a0 < a1) 2: xxxx 上面的汇编代码的作用就是获取到bss段的起始地址,然后写入0; 设置栈SP /* setup stack pointer for C runtime environment */ la sp, __init_process_stack_end__ init_process_stack_end是在链接脚本中栈空间结束地址,这里直接复制为sp即可。
- 
					
						  平头哥RISC-V+gdb+vscode调试工具准备 T-HeadDebugServer工具:https://www.xrvm.cn/community/download?id=4238019891233361920 T-HeadDebugServer使用说明:https://www.xrvm.cn/community/download?id=4170589434888130560 vscode下载:https://code.visualstudio.com/ cklink:硬件调试器(网上购买) 安装完成工具后,硬件平台要使能调试模式,一般是GPIO的复用要打开为JTAG模式。 T-headDebugServer工具 安装好T-headDebugServer以及对应的cklink驱动后,并且使能了硬件平台的gpio复用为jtag,打开T-headDebugServer即可链接上硬件平台。如下图所示,表示连接成功。 如果连接不成功,可以修改下配置,再进行尝试。 mobarX远程服务器代码与本地T-HeadDebuger GDB调试 设置T-HeadDebugServer监听端口 设置好监听端口后启动 设置mobaXterm 启动代理连接,有可能要输入ssh访问远程服务的账号和密码 gdb连接 如果架构相关不正确,可使用set architecture riscv:rv64来设置。 为了便捷快速连接,也可以将常用的指令设置到.gdbinit脚本中。 vscode+gdb调试 如果不习惯mobarXiterm上gdb命令行方式调试,可以使用vscode搭建gdb的调试环境,下面介绍vscode的gdb调试方法,前置条件为设置好mobarXiterm的代理。 安装Remote-SSH ssh连接服务器 错误解决 Could not establish connetion to xxxx 如果报下面的错误 则进行配置remote-SSH,右键->Extension Setting 参考:https://blog.csdn.net/qq_41854763/article/details/103317116 XHR failedscode 参考:https://blog.csdn.net/m0_38040006/article/details/126752751 gdb连接 创建一个lauch.json { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "(gdb) Launch", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/xxx/img/rt_system.elf", "args": [], "stopAtEntry": false, "cwd": "${fileDirname}", "environment": [], "externalConsole": false, "MIMode": "gdb", "miDebuggerPath": "${workspaceFolder}/xxx/riscv64-elf-x86_64-20201104/bin/riscv64-unknown-elf-gdb", "miDebuggerServerAddress": "localhost:8193", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "set remotetimeout 20", "ignoreFailures": true } ] } ] } 修改好elf、gdb的位置,然后再设置好mobarXiterm的代理端口,这样就可以启动连接了。 常用技巧 在汇编代码中支持打断点 ctrl+鼠标左键生效 先安装拓展插件 进入VScode的首选项,选择设置(快捷键 Ctrl + , ),输入Go to definition,找到如下两个设置。 Editor: Multi Cursor Modifier 设置成 alt “editor.gotoLocation.multipleDefinitions” 设置成 “goto”。 重启vscode 在vscode中实时显示汇编 在代码处右键->Open Disassembly View。