RISC-V
  • RISC-V指令集架构

    RISC-V指令集架构

    简介 RISC-V是一个通用的指令集架构(ISA),ISA是底层硬件电路面向上层软件程序提供的一层接口规范,ISA定义了: 基本数据类型,BYTE/HALFWORD/WORD 寄存器 指令 寻址模式 异常或中断处理方式 之所以需要ISA,是为上层软件提供一层抽象,制定规则和约束,让编程者不用关心具体的电路结构,目前的指令集架构有两种类别: - CISC复杂指令集(Complex Instruction Set Computing),这类指令针对特定功能的实现需要新增实现特定的指令,导致指令数目比较多,但生成的程序长度相对较短; - RISC精简指令集(Reduced Instruction Set Computing),只定义常用指令,对复杂的功能采用常用指令组合实现,这导致指令数目比较精简,但生成的程序长度相对较长。 ISA的宽度指定是CPU中通用寄存器的宽度,决定了寻址的范围大小以及数据运算的能力。当前ISA有以下 X86 SPARC POWER ARM MIPS RISC-V RISC-V ISA命名格式为RV[###][abc...xyz],如RV64IMA; RV:标识RISC-V体系结构前缀 [###]:{32,64,128}用于标识处理器的子宽,即处理器寄存器的宽度,单位为bit [abc...xyz]:标识处理器支持的指令集模块集合,见下描述 RISCV-ISA指令集按照模块来划分,=1个基本整数指令集+多个可选拓展指令集 整数指令集(interger):强制要求实现的基础指令集,RV32I,RV64I 扩展指令集:M为整数乘与除法指令集,A存储器原子指令集,F单精度浮点指令集,D双精度浮点指令,兼容F,C压缩指令集。而GC代表IMAFDC的组合。 根据以上RV32IMAC表示32位实现,支持interger,multiply,atomic,compressed;RV64GC,就是表示支持RV64位IMAFDC。 指令集 RISC-V的指令集可分为6中,每种的编码格式如下: 整形指令集 乘除指令集 原子扩展指令集 浮点扩展指令集 伪指令 为了方便编程,对指令进一步封装,形成伪指令 寄存器 通用寄存器 特权寄存器 RISC-V指令运行,根据访问权限,分为M/S/U模式,每个模式下有自己对应的寄存器,这里重点列出M模式相关的寄存器。 mstatus寄存器 mie寄存器 mip寄存器 mtvec寄存器 mepc寄存器 mscratch寄存器 mcause寄存器 对于mcause exception code如下: 异常处理流程
  • qemu-system-riscv64 virt平台ROM代码启动分析

    qemu-system-riscv64 virt平台ROM代码启动分析

    为什么下面qemu启动elf时,text地址要从0x80000000开始? qemu-system-riscv64 -machine virt -cpu c910v -nographic -smp 1 -bios none -kernel xxx.elf 从memory mapping角度 下面是qemu virt平台的memory mapping https://github.com/qemu/qemu/blob/master/hw/riscv/virt.c static const MemMapEntry virt_memmap[] = { [VIRT_DEBUG] = { 0x0, 0x100 }, [VIRT_MROM] = { 0x1000, 0xf000 }, [VIRT_TEST] = { 0x100000, 0x1000 }, [VIRT_RTC] = { 0x101000, 0x1000 }, [VIRT_CLINT] = { 0x2000000, 0x10000 }, [VIRT_ACLINT_SSWI] = { 0x2F00000, 0x4000 }, [VIRT_PCIE_PIO] = { 0x3000000, 0x10000 }, [VIRT_PLATFORM_BUS] = { 0x4000000, 0x2000000 }, [VIRT_PLIC] = { 0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) }, [VIRT_APLIC_M] = { 0xc000000, APLIC_SIZE(VIRT_CPUS_MAX) }, [VIRT_APLIC_S] = { 0xd000000, APLIC_SIZE(VIRT_CPUS_MAX) }, [VIRT_UART0] = { 0x10000000, 0x100 }, [VIRT_VIRTIO] = { 0x10001000, 0x1000 }, [VIRT_FW_CFG] = { 0x10100000, 0x18 }, [VIRT_FLASH] = { 0x20000000, 0x4000000 }, [VIRT_IMSIC_M] = { 0x24000000, VIRT_IMSIC_MAX_SIZE }, [VIRT_IMSIC_S] = { 0x28000000, VIRT_IMSIC_MAX_SIZE }, [VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 }, [VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 }, [VIRT_DRAM] = { 0x80000000, 0x0 }, }; MROM: 是virt平台的rom化启动代码,所以qemu启动时会默认从该地址开始运行。 DRAM:DRAM的映射地址为0x80000000,从这里大概能猜出来elf的地址为什么要那么设置了。 qemu启动时,使用-kernel参数带上elf,与实际硬件平台的差异相比,就会默认根据elf的程序入口地址将elf加载到对应DRAM内存上,如下图所示。 跟踪启动流程 上面是启动qemu后的初始化代码,连接上gdb后,可以使用layout asm查看汇编指令,并且使用si可进行单步调试。可见qemu启动就会跳转到0x1000进行执行,即对应上面memory mapping的VIRT_MROM。上面代码主要逻辑是,跳转到t0,传递参数a0/a1/a2。下面来看看具体的值。 0x0000000000001000: 97 02 00 00 auipc t0,0x0 //t0=PC+0x0<<12,即t0=0x1000 0x0000000000001004: 13 86 82 02 addi a2,t0,40//a2=t0+40,即0x1028 0x0000000000001008: 73 25 40 f1 csrr a0,mhartid//a0=0 0x000000000000100c: 83 b5 02 02 ld a1,32(t0) //a1=t0+0x20地址的值即0x1020地址处 ld是加载8字节,由于是小端,所以a1=0x87e00000 0x0000000000001010: 83 b2 82 01 ld t0,24(t0) //t0=t0+0x18地址的值即0x1018地址处 同理ld是加载8字节且小端,所有t0=80000000 0x0000000000001014: 67 80 02 00 jr t0 //跳转到0x80000000执行。
  • RISC-V汇编指令

    RISC-V汇编指令

    C源代码 unsigned int arithmetic(unsigned int a, unsigned int b) { unsigned int sum, diff, upper; sum = a + b; sum = sum + 2; diff = a - b; diff = diff -1; upper = 8192; return sum + diff + upper; } unsigned int shits(unsigned int *a, unsigned *b) { unsigned int shift_left,shift_right; shift_left = 16 << 2; shift_right = 8 >> 2; shift_left = shift_left >> *a; shift_right = shift_right << *b; return shift_left + shift_right; } unsigned int logical(unsigned int a, unsigned int b) { unsigned and_op, or_op, xor_op; and_op = a & b; and_op = and_op & 3; or_op = a | b; or_op = or_op | 3; xor_op = a ^ b; xor_op = xor_op ^ 3; return and_op + or_op + xor_op; } unsigned int compare(unsigned int a, unsigned int b) { return a > b ? 1 : 0; } void branch(unsigned int a, unsigned int b) { int c; int i; if (a <= b) { c = a + b; } else { c = a - b; } for (i = 0; i < c; i ++) { c = c + i; } } unsigned int mul_div_rem(unsigned int a, unsigned int b) { unsigned int prod, quo, rem; prod = a * b; quo = a / b; rem = b % a; return prod + quo + rem; } float fp_add(float a, float b) { return a + b; } double fp_mul(double a, double b) { return a * b; } int start_kernel(void) { unsigned int a = 8, b = 2, ret; ret = arithmetic(a,b); ret = shits(&ret, &b); ret = logical(ret , a); ret = compare(a, b); branch(a, b); float fa = 1.0f, fb = 2.0f; fp_add(fa, fb); double da = 1.0, db = 2.0; fp_mul(da, db); return 0; } 汇编代码 baremetal.elf: file format elf64-littleriscv Disassembly of section .text: 0000000080000000 <_start>: .global _start .text _start: la sp, __init_stack_end__ 80000000: 00004117 auipc sp,0x4 //sp = pc + 0x4 << 12(0x400) 80000004: 3c010113 addi sp,sp,960 # 800043c0 //sp = sp + 960 <__init_stack_end__> li t0, 0x6000 80000008: 6299 lui t0,0x6//t0 = 0x6 << 12 csrc mstatus, t0 8000000a: 3002b073 csrc mstatus,t0 //mstatus = mstatus & ~t0 li t0, 0x2000 8000000e: 6289 lui t0,0x2 //t0 = 0x2 << 12 csrs mstatus, t0 80000010: 3002a073 csrs mstatus,t0 //mstatus=mstatus | t0 使能mstauts[14:13]bit,使能浮点数单元。 j start_kernel 80000014: 00e0006f j 80000022 <start_kernel> 0000000080000018 <loop>: loop: nop 80000018: 0001 nop nop 8000001a: 0001 nop nop 8000001c: 0001 nop nop 8000001e: 0001 nop j loop 80000020: bfe5 j 80000018 <loop> 0000000080000022 <start_kernel>: #include <test.h> int start_kernel(void) { 80000022: 7139 addi sp,sp,-64 //sp=sp-64,相当于分配64字节大小的栈空间 80000024: fc06 sd ra,56(sp)//ra存储到sp+56位置,ra是返回函数地址,64位系统占用8个字节。由于该函数有子函数,所以要存储ra。 80000026: f822 sd s0,48(sp)//s0存储到sp+48位置 80000028: 0080 addi s0,sp,64//s0=sp +64,s0指向当前栈帧位置,所以s0也常用于栈帧指针。更新当前函数的栈帧。 unsigned int a = 8, b = 2, ret; 8000002a: 47a1 li a5,8//a5=8 8000002c: fef42623 sw a5,-20(s0)//将a5存储到s0-20的位置,存储到栈帧里面去 80000030: 4789 li a5,2//a5=2, 80000032: fcf42623 sw a5,-52(s0)//将a5存储到s0-52的位置,存储栈帧空间去。(sw,store word,存储4字节) ret = arithmetic(a,b); 80000036: fcc42703 lw a4,-52(s0)//加载s0-52地址的数据到a4中,就是b=2。 8000003a: fec42783 lw a5,-20(s0)//加载s0-20地址的数据到a5,就是a=8 8000003e: 85ba mv a1,a4//a1=a4,参数b 80000040: 853e mv a0,a5//a0=a5,参数a 80000042: 0c4000ef jal ra,80000106 <arithmetic>//ra=PC+4,存储函数的返回的地址,PC=80000106,跳转到arithmetic函数 80000046: 87aa mv a5,a0 //保存函数返回值 80000048: 2781 sext.w a5,a5 //将a5拓展为32位有符号数字,因为a0是64为寄存器。 8000004a: fcf42423 sw a5,-56(s0) //将a5存储到栈帧中(s0-56) ret = shits(&ret, &b); 8000004e: fcc40713 addi a4,s0,-52 //a4=s0-52,获取b的地址 80000052: fc840793 addi a5,s0,-56//a5=s0-56,获取ret的地址 80000056: 85ba mv a1,a4//a1=a4,将b的地址赋值为a1,函数参数传递准备 80000058: 853e mv a0,a5//a0=a5,将ret的地址赋值为a0,函数参数传递准备 8000005a: 114000ef jal ra,8000016e <shits>//ra=PC+4,PC=8000016e 8000005e: 87aa mv a5,a0//保存函数的返回地址 80000060: 2781 sext.w a5,a5//将a5拓展为32位有符号数字 80000062: fcf42423 sw a5,-56(s0)//将a5存储到栈帧中(更新ret的值) ret = logical(ret , a); 80000066: fc842783 lw a5,-56(s0) 8000006a: fec42703 lw a4,-20(s0) 8000006e: 85ba mv a1,a4 80000070: 853e mv a0,a5 80000072: 154000ef jal ra,800001c6 <logical> 80000076: 87aa mv a5,a0 80000078: 2781 sext.w a5,a5 8000007a: fcf42423 sw a5,-56(s0) ret = compare(a, b); 8000007e: fcc42703 lw a4,-52(s0) 80000082: fec42783 lw a5,-20(s0) 80000086: 85ba mv a1,a4 80000088: 853e mv a0,a5 8000008a: 1b8000ef jal ra,80000242 <compare> 8000008e: 87aa mv a5,a0 80000090: 2781 sext.w a5,a5 80000092: fcf42423 sw a5,-56(s0) branch(a, b); branch(a, b); 80000096: fcc42703 lw a4,-52(s0) 8000009a: fec42783 lw a5,-20(s0) 8000009e: 85ba mv a1,a4 800000a0: 853e mv a0,a5 800000a2: 1d2000ef jal ra,80000274 <branch> float fa = 1.0f, fb = 2.0f; 800000a6: 00000797 auipc a5,0x0 800000aa: 2fa78793 addi a5,a5,762 # 800003a0 <.LC0> 800000ae: 0007a787 flw fa5,0(a5) 800000b2: fef42427 fsw fa5,-24(s0) 800000b6: 00000797 auipc a5,0x0 800000ba: 2ee78793 addi a5,a5,750 # 800003a4 <.LC1> 800000be: 0007a787 flw fa5,0(a5) 800000c2: fef42227 fsw fa5,-28(s0) fp_add(fa, fb); 800000c6: fe442587 flw fa1,-28(s0) 800000ca: fe842507 flw fa0,-24(s0) 800000ce: 286000ef jal ra,80000354 <fp_add> 对于浮点值,先存储到符号表上,去加载load到寄存器中去。 在标准RISC-V指令集中,浮点数确实不支持直接的立即数操作。大部分RISC-V的浮点指令涉及到的操作数通常是寄存器到寄存器的,也就是说,浮点运算的数据来源于浮点寄存器文件(FPU registers),而不是像整数指令那样可以从指令中直接获取一个立即数。 如果需要在程序中使用常量浮点数,通常会在程序初始化或相关代码段中先将这些常量加载到浮点寄存器中,然后再参与后续的浮点运算。例如,可以通过加载字节(LB, LBU, etc.)或加载半字(LH, LHU, etc.)指令从内存中读取浮点数的二进制表示(按照IEEE 754标准编码),然后利用适当的浮点装载指令(FLW或FLD)将这些数据载入浮点寄存器。 当然,在一些高级编程语言环境下,编译器会自动处理这些细节,程序员无需关心如何将常量放入寄存器的过程。而对于硬件设计者或底层软件开发者来说,理解这一机制是非常重要的。随着RISC-V生态的发展,未来有可能会出现扩展指令集来支持更丰富的浮点数立即数操作,但这不是当前标准RISC-V ISA的一部分。 double da = 1.0, db = 2.0; 800000d2: 00000797 auipc a5,0x0 //a5=PC+0x00<<12 800000d6: 2d678793 addi a5,a5,726 # 800003a8 <.LC2>//a5=a5+726 800000da: 239c fld fa5,0(a5)//fa5=*a5,获取a5的地址数据 800000dc: fcf43c27 fsd fa5,-40(s0)//将fa5存储到s0-40栈上。 800000e0: 00000797 auipc a5,0x0//a5=PC+0x00<<12 800000e4: 2d078793 addi a5,a5,720 # 800003b0 <.LC3>//a5=a5+720 800000e8: 239c fld fa5,0(a5)//fa5=*a5,获取a5地址的数据 800000ea: fcf43827 fsd fa5,-48(s0)//将fa5存储到s0-48栈上。 fp_mul(da, db); 800000ee: fd043587 fld fa1,-48(s0)//加载s0-48的数据到fa1,(da) 800000f2: fd843507 fld fa0,-40(s0)//加载s0-40的数据到fa0 800000f6: 282000ef jal ra,80000378 <fp_mul>//跳转到fp_mul return 0; 800000fa: 4781 li a5,0 } 800000fc: 853e mv a0,a5 800000fe: 70e2 ld ra,56(sp) 80000100: 7442 ld s0,48(sp) 80000102: 6121 addi sp,sp,64 80000104: 8082 ret 0000000080000106 <arithmetic>: unsigned int arithmetic(unsigned int a, unsigned int b) { 80000106: 7179 addi sp,sp,-48 80000108: f422 sd s0,40(sp) 8000010a: 1800 addi s0,sp,48 8000010c: 87aa mv a5,a0 8000010e: 872e mv a4,a1 80000110: fcf42e23 sw a5,-36(s0) 80000114: 87ba mv a5,a4 80000116: fcf42c23 sw a5,-40(s0) unsigned int sum, diff, upper; sum = a + b; 8000011a: fdc42703 lw a4,-36(s0) 8000011e: fd842783 lw a5,-40(s0) 80000122: 9fb9 addw a5,a5,a4 80000124: fef42623 sw a5,-20(s0) sum = sum + 2; 80000128: fec42783 lw a5,-20(s0) 8000012c: 2789 addiw a5,a5,2 8000012e: fef42623 sw a5,-20(s0) diff = a - b; 80000132: fdc42703 lw a4,-36(s0) 80000136: fd842783 lw a5,-40(s0) 8000013a: 40f707bb subw a5,a4,a5 8000013e: fef42423 sw a5,-24(s0) diff = diff -1; 80000142: fe842783 lw a5,-24(s0) 80000146: 37fd addiw a5,a5,-1 80000148: fef42423 sw a5,-24(s0) upper = 8192; 8000014c: 6789 lui a5,0x2 8000014e: fef42223 sw a5,-28(s0) return sum + diff + upper; 80000152: fec42703 lw a4,-20(s0) 80000156: fe842783 lw a5,-24(s0) 8000015a: 9fb9 addw a5,a5,a4 8000015c: 2781 sext.w a5,a5 8000015e: fe442703 lw a4,-28(s0) 80000162: 9fb9 addw a5,a5,a4 80000164: 2781 sext.w a5,a5 } 80000166: 853e mv a0,a5 80000168: 7422 ld s0,40(sp) 8000016a: 6145 addi sp,sp,48 8000016c: 8082 ret 000000008000016e <shits>: unsigned int shits(unsigned int *a, unsigned *b) { 8000016e: 7179 addi sp,sp,-48 80000170: f422 sd s0,40(sp) 80000172: 1800 addi s0,sp,48 80000174: fca43c23 sd a0,-40(s0) 80000178: fcb43823 sd a1,-48(s0) unsigned int shift_left,shift_right; shift_left = 16 << 2; 8000017c: 04000793 li a5,64 80000180: fef42623 sw a5,-20(s0) shift_right = 8 >> 2; 80000184: 4789 li a5,2 80000186: fef42423 sw a5,-24(s0) shift_left = shift_left >> *a; 8000018a: fd843783 ld a5,-40(s0) 8000018e: 439c lw a5,0(a5) 80000190: 873e mv a4,a5 80000192: fec42783 lw a5,-20(s0) 80000196: 00e7d7bb srlw a5,a5,a4 8000019a: fef42623 sw a5,-20(s0) shift_right = shift_right << *b; 8000019e: fd043783 ld a5,-48(s0) 800001a2: 439c lw a5,0(a5) 800001a4: 873e mv a4,a5 800001a6: fe842783 lw a5,-24(s0) 800001aa: 00e797bb sllw a5,a5,a4 800001ae: fef42423 sw a5,-24(s0) return shift_left + shift_right; 800001b2: fec42703 lw a4,-20(s0) 800001b6: fe842783 lw a5,-24(s0) 800001ba: 9fb9 addw a5,a5,a4 800001bc: 2781 sext.w a5,a5 } 800001be: 853e mv a0,a5 800001c0: 7422 ld s0,40(sp) 800001c2: 6145 addi sp,sp,48 800001c4: 8082 ret 00000000800001c6 <logical>: unsigned int logical(unsigned int a, unsigned int b) 800001c6: 7179 addi sp,sp,-48 800001c8: f422 sd s0,40(sp) 800001ca: 1800 addi s0,sp,48 800001cc: 87aa mv a5,a0 800001ce: 872e mv a4,a1 800001d0: fcf42e23 sw a5,-36(s0) 800001d4: 87ba mv a5,a4 800001d6: fcf42c23 sw a5,-40(s0) unsigned and_op, or_op, xor_op; and_op = a & b; 800001da: fdc42703 lw a4,-36(s0) 800001de: fd842783 lw a5,-40(s0) 800001e2: 8ff9 and a5,a5,a4 800001e4: fef42623 sw a5,-20(s0) and_op = and_op & 3; 800001e8: fec42783 lw a5,-20(s0) 800001ec: 8b8d andi a5,a5,3 800001ee: fef42623 sw a5,-20(s0) or_op = a | b; 800001f2: fdc42703 lw a4,-36(s0) 800001f6: fd842783 lw a5,-40(s0) 800001fa: 8fd9 or a5,a5,a4 800001fc: fef42423 sw a5,-24(s0) or_op = or_op | 3; 80000200: fe842783 lw a5,-24(s0) 80000204: 0037e793 ori a5,a5,3 80000208: fef42423 sw a5,-24(s0) xor_op = a ^ b; 8000020c: fdc42703 lw a4,-36(s0) 80000210: fd842783 lw a5,-40(s0) 80000214: 8fb9 xor a5,a5,a4 80000216: fef42223 sw a5,-28(s0) xor_op = xor_op ^ 3; 8000021a: fe442783 lw a5,-28(s0) 8000021e: 0037c793 xori a5,a5,3 80000222: fef42223 sw a5,-28(s0) return and_op + or_op + xor_op; 80000226: fec42703 lw a4,-20(s0) 8000022a: fe842783 lw a5,-24(s0) 8000022e: 9fb9 addw a5,a5,a4 80000230: 2781 sext.w a5,a5 80000232: fe442703 lw a4,-28(s0) 80000236: 9fb9 addw a5,a5,a4 80000238: 2781 sext.w a5,a5 } 8000023a: 853e mv a0,a5 8000023c: 7422 ld s0,40(sp) 8000023e: 6145 addi sp,sp,48 80000240: 8082 ret 0000000080000242 <compare>: unsigned int compare(unsigned int a, unsigned int b) 800001c6: 7179 addi sp,sp,-48 800001c8: f422 sd s0,40(sp) 800001ca: 1800 addi s0,sp,48 800001cc: 87aa mv a5,a0 800001ce: 872e mv a4,a1 800001d0: fcf42e23 sw a5,-36(s0) 800001d4: 87ba mv a5,a4 800001d6: fcf42c23 sw a5,-40(s0) unsigned and_op, or_op, xor_op; and_op = a & b; 800001da: fdc42703 lw a4,-36(s0) 800001de: fd842783 lw a5,-40(s0) 800001e2: 8ff9 and a5,a5,a4 800001e4: fef42623 sw a5,-20(s0) and_op = and_op & 3; 800001e8: fec42783 lw a5,-20(s0) 800001ec: 8b8d andi a5,a5,3 800001ee: fef42623 sw a5,-20(s0) or_op = a | b; 800001f2: fdc42703 lw a4,-36(s0) 800001f6: fd842783 lw a5,-40(s0) 800001fa: 8fd9 or a5,a5,a4 800001fc: fef42423 sw a5,-24(s0) or_op = or_op | 3; 80000200: fe842783 lw a5,-24(s0) 80000204: 0037e793 ori a5,a5,3 80000208: fef42423 sw a5,-24(s0) xor_op = a ^ b; 8000020c: fdc42703 lw a4,-36(s0) 80000210: fd842783 lw a5,-40(s0) 80000214: 8fb9 xor a5,a5,a4 80000216: fef42223 sw a5,-28(s0) xor_op = xor_op ^ 3; 8000021a: fe442783 lw a5,-28(s0) 8000021e: 0037c793 xori a5,a5,3 80000222: fef42223 sw a5,-28(s0) return and_op + or_op + xor_op; 80000226: fec42703 lw a4,-20(s0) 8000022a: fe842783 lw a5,-24(s0) 8000022e: 9fb9 addw a5,a5,a4 80000230: 2781 sext.w a5,a5 80000232: fe442703 lw a4,-28(s0) 80000236: 9fb9 addw a5,a5,a4 80000238: 2781 sext.w a5,a5 } 8000023a: 853e mv a0,a5 8000023c: 7422 ld s0,40(sp) 8000023e: 6145 addi sp,sp,48 80000240: 8082 ret 0000000080000242 <compare>: unsigned int compare(unsigned int a, unsigned int b) { 80000242: 1101 addi sp,sp,-32 80000244: ec22 sd s0,24(sp) 80000246: 1000 addi s0,sp,32 80000248: 87aa mv a5,a0 8000024a: 872e mv a4,a1 8000024c: fef42623 sw a5,-20(s0) 80000250: 87ba mv a5,a4 80000252: fef42423 sw a5,-24(s0) return a > b ? 1 : 0; 80000256: fec42703 lw a4,-20(s0) 8000025a: fe842783 lw a5,-24(s0) 8000025e: 2701 sext.w a4,a4 80000260: 2781 sext.w a5,a5 80000262: 00e7b7b3 sltu a5,a5,a4 80000266: 0ff7f793 zext.b a5,a5 8000026a: 2781 sext.w a5,a5 } 8000026c: 853e mv a0,a5 8000026e: 6462 ld s0,24(sp) 80000270: 6105 addi sp,sp,32 80000272: 8082 ret 0000000080000274 <branch>: void branch(unsigned int a, unsigned int b) { 80000274: 7179 addi sp,sp,-48 80000276: f422 sd s0,40(sp) 80000278: 1800 addi s0,sp,48 8000027a: 87aa mv a5,a0 8000027c: 872e mv a4,a1 8000027e: fcf42e23 sw a5,-36(s0) 80000282: 87ba mv a5,a4 80000284: fcf42c23 sw a5,-40(s0) int c; int i; if (a <= b) { 80000288: fdc42703 lw a4,-36(s0) 8000028c: fd842783 lw a5,-40(s0) 80000290: 2701 sext.w a4,a4 80000292: 2781 sext.w a5,a5 80000294: 00e7eb63 bltu a5,a4,800002aa <.L10>//无符号比较,if(a5<a4) 跳转到800002aa, a5=b,a4=a c = a + b; 80000298: fdc42703 lw a4,-36(s0) 8000029c: fd842783 lw a5,-40(s0) 800002a0: 9fb9 addw a5,a5,a4 800002a2: 2781 sext.w a5,a5 800002a4: fef42623 sw a5,-20(s0) 800002a8: a811 j 800002bc <.L11> 00000000800002aa <.L10>: } else { c = a - b; 800002aa: fdc42703 lw a4,-36(s0) 800002ae: fd842783 lw a5,-40(s0) 800002b2: 40f707bb subw a5,a4,a5 800002b6: 2781 sext.w a5,a5 800002b8: fef42623 sw a5,-20(s0) 00000000800002bc <.L11>: } for (i = 0; i < c; i ++) { 800002bc: fe042423 sw zero,-24(s0) 800002c0: a829 j 800002da <.L12> 00000000800002c2 <.L13>: c = c + i; 800002c2: fec42703 lw a4,-20(s0) 800002c6: fe842783 lw a5,-24(s0) 800002ca: 9fb9 addw a5,a5,a4 800002cc: fef42623 sw a5,-20(s0) for (i = 0; i < c; i ++) { 800002d0: fe842783 lw a5,-24(s0) 800002d4: 2785 addiw a5,a5,1 800002d6: fef42423 sw a5,-24(s0) 00000000800002da <.L12>: 800002da: fe842703 lw a4,-24(s0) 800002de: fec42783 lw a5,-20(s0) 800002e2: 2701 sext.w a4,a4 800002e4: 2781 sext.w a5,a5 800002e6: fcf74ee3 blt a4,a5,800002c2 <.L13> } } 800002ea: 0001 nop 800002ec: 0001 nop 800002ee: 7422 ld s0,40(sp) 800002f0: 6145 addi sp,sp,48 800002f2: 8082 ret 00000000800002f4 <mul_div_rem>: unsigned int mul_div_rem(unsigned int a, unsigned int b) { 800002f4: 7179 addi sp,sp,-48 800002f6: f422 sd s0,40(sp) 800002f8: 1800 addi s0,sp,48 800002fa: 87aa mv a5,a0 800002fc: 872e mv a4,a1 800002fe: fcf42e23 sw a5,-36(s0) 80000302: 87ba mv a5,a4 80000304: fcf42c23 sw a5,-40(s0) unsigned int prod, quo, rem; prod = a * b; 80000308: fdc42703 lw a4,-36(s0) 8000030c: fd842783 lw a5,-40(s0) 80000310: 02f707bb mulw a5,a4,a5 80000314: fef42623 sw a5,-20(s0) quo = a / b; 80000318: fdc42703 lw a4,-36(s0) 8000031c: fd842783 lw a5,-40(s0) 80000320: 02f757bb divuw a5,a4,a5 80000324: fef42423 sw a5,-24(s0) rem = b % a; 80000328: fd842703 lw a4,-40(s0) 8000032c: fdc42783 lw a5,-36(s0) 80000330: 02f777bb remuw a5,a4,a5 80000334: fef42223 sw a5,-28(s0) return prod + quo + rem; 80000338: fec42703 lw a4,-20(s0) 8000033c: fe842783 lw a5,-24(s0) 80000340: 9fb9 addw a5,a5,a4 80000342: 2781 sext.w a5,a5 80000344: fe442703 lw a4,-28(s0) 80000348: 9fb9 addw a5,a5,a4 8000034a: 2781 sext.w a5,a5 } 8000034c: 853e mv a0,a5 8000034e: 7422 ld s0,40(sp) 80000350: 6145 addi sp,sp,48 80000352: 8082 ret 0000000080000354 <fp_add>: float fp_add(float a, float b) { 80000354: 1101 addi sp,sp,-32 80000356: ec22 sd s0,24(sp) 80000358: 1000 addi s0,sp,32 8000035a: fea42627 fsw fa0,-20(s0) 8000035e: feb42427 fsw fa1,-24(s0) return a + b; 80000362: fec42707 flw fa4,-20(s0) 80000366: fe842787 flw fa5,-24(s0) 8000036a: 00f777d3 fadd.s fa5,fa4,fa5 } 8000036e: 20f78553 fmv.s fa0,fa5 80000372: 6462 ld s0,24(sp) 80000374: 6105 addi sp,sp,32 80000376: 8082 ret 0000000080000378 <fp_mul>: double fp_mul(double a, double b) { 80000378: 1101 addi sp,sp,-32 8000037a: ec22 sd s0,24(sp) 8000037c: 1000 addi s0,sp,32 //前奏阶段,咱不赘述 8000037e: fea43427 fsd fa0,-24(s0) //存储参数a数据, 80000382: feb43027 fsd fa1,-32(s0)//存储参数b数据 return a * b; 80000386: fe843707 fld fa4,-24(s0)//加载参数a的值 8000038a: fe043787 fld fa5,-32(s0)//加载参数b的值 8000038e: 12f777d3 fmul.d fa5,fa4,fa5/fa5=fa4*fa5 } 80000392: 22f78553 fmv.d fa0,fa5//返回值存储到fa0 80000396: 6462 ld s0,24(sp)//恢复上一个函数栈帧 80000398: 6105 addi sp,sp,32//销毁栈 8000039a: 8082 ret 000000008000039c <.LFE7>: 8000039c: 0000 unimp ... 00000000800003a0 <.LC0>: 800003a0: 0000 unimp 800003a2: 3f80 fld fs0,56(a5) 00000000800003a4 <.LC1>: 800003a4: 0000 unimp 800003a6: 4000 lw s0,0(s0) 00000000800003a8 <.LC2>: 800003a8: 0000 unimp 800003aa: 0000 unimp 800003ac: 0000 unimp 800003ae: 3ff0 fld fa2,248(a5) 00000000800003b0 <.LC3>: 800003b0: 0000 unimp 800003b2: 0000 unimp 800003b4: 0000 unimp 800003b6: 4000 lw s0,0(s0) 函数调用约定 函数调用过程可以分为6个阶段 将参数存放到函数可以访问的位置。 跳转到函数入口。 获取函数所需的局部存储资源,按需保存寄存器。 执行函数功能 将返回值存放到调用者可访问的位置,恢复寄存器,释放局部存储资源。 由于程序可从多处调用函数,需将控制权返回到调用点 stack 每一次函数调用,函数都会为自己创建一个栈帧(Stack Frame),只给自己用。函数通过移动Stack Pointer来完成Stack Frame的空间分配。栈空间是向下生长的, 创建一个栈帧的时候就是对sp多减法。一个函数的栈帧保存了寄存器,本地变量,以及超过8个的额外参数等。不同函数保存的大小是不一样的,因此每个函数栈帧的空间大小也是不一样的,但栈栈有一个共性的保存内容,函数的返回地址(ra寄存器)和上一个函数的栈帧指针(s0寄存器)。 ra寄存器:一般存放在栈帧的第一位 s0(fp)寄存器:一般存放在栈帧的第二位 之所以要保存ra寄存器是因为函数返回时可以找到返回地址,保存fp寄存器是因为函数返回时可以找到该函数的栈帧起始位置。 在栈帧中有两个重要的相关寄存器,那就是SP和FP;SP指向了栈帧的底部,表示分配了栈帧的空间结束地址,FP指向栈帧的顶部,指向栈帧的开始地址,ra和前一个栈帧的地址都在固定的栈帧位置,就可以通过FP很快的寻址到这两个寄存器,这也是为什么要有fp的一个原因之一。 一个函数的汇编代码构成,可以分为3部分,prologue(准备阶段)+body+epilogue(结束阶段) prologue:分配栈帧空间(减少SP的值),保存ra(函数中有子函数调用),s0,saved寄存器 body:函数执行体,实际C语言看到的内容 epilogue:恢复saved寄存器,恢复上一个函数的栈帧s0,必要情况恢复ra寄存器,增加sp,ret返回。 ra寄存器不一定会保存,只要当函数中需要调用其他子函数,在进入prologue的时候才会保存ra,否则不会保存ra到栈中, 函数调用中的寄存器,要么作为保存寄存器,其值在函数调用前后保持不变;要么作为零时寄存器,其值在函数调用前后可能改变。函数参数和返回地址在函数调用过程会被修改,与零时寄存器类似。函数的返回地址根据函数是否有子函数调用决定是否要入栈保存。
  • 环境搭建

    环境搭建

    risc-v32入门 https://github.com/plctlab/riscv-operating-system-mooc/blob/main/README_zh.md 按照上面的进行搭建,工具链和qemu都不用编译,直接解压设置环境变量后可使用,需要注意的是ubuntu使用20.04以上版本。 对于asm下面的code编译方式,需要修改一下Makefile --- a/asm/add/Makefile +++ b/asm/add/Makefile @@ -1 +1 @@ -../build.mk +include ../build.mk 编译 riscv64-unknown-elf-gcc -nostdlib -fno-builtin -march=rv32g -mabi=ilp32 -g -Wall xxx.c -Ttext=0x80000000 -o xxx.elf -nostdlib:不链接标准C库 -fno-builtin:关闭编译器对内置函数的使用。 -march=rv32g:指定目标架构为 RISC-V RV32G,其中 'rv32' 表示 32 位指令集宽度,'g' 表示该架构扩展包括通用整数指令集(I)、浮点数指令集(F)以及乘法累加指令集(D)。 -mabi=ilp32:设置应用程序二进制接口 (ABI) 为 ilp32,即在 32 位架构上使用 32 位长度的 int、long 和指针类型。 -g:生成调试信息,使得编译后的程序可以用 gdb 等调试器进行源代码级别的调试。 -Wall:开启编译器的所有警告信息,提高代码质量。 -Ttext:指定程序的起始地址或者入口点地址为 0x80000000。这对于裸机程序或嵌入式系统特别重要,因为它们通常需要在特定地址处加载并开始执行。 objcopy riscv64-unknown-elf-objcopy -O binary xx.elf xxx.bin objcopy -O binary 这个命令的作用是用来将目标文件(如ELF格式的可执行文件或对象文件)转换成纯粹的二进制格式文件。具体来说: 1.去除符号信息:当执行 -O binary 时,objcopy 工具会丢弃原始目标文件中的所有符号表信息、重定位信息以及其他非执行相关的数据。 2.生成纯二进制映像:输出的二进制文件仅包含原始文件中的可执行代码和初始化的数据,这些数据按照它们在内存中运行时的布局排列。这样的二进制文件可以直接用于固件烧录、设备内存加载等场景,因为它不再需要操作系统提供的加载器来解析额外的元数据。 3.简化部署:对于嵌入式系统或其他资源受限的环境,这种转换有助于创建一个单一的、最小化的二进制镜像,可以直接写入闪存或其他存储介质,并从特定地址启动执行。 举例来说,命令 arm-linux-objcopy -O binary -S source.elf destination.bin 将会把 source.elf 文件转换成名为 destination.bin 的纯二进制文件,适合于在没有操作系统的微处理器上直接运行或用于硬件编程。 qemu运行 qume:是一个支持跨平台虚拟化的虚拟机,有 user mode 和 system mode 两种配置方式。其中qemu 在system mode配置下模拟出整个计算机,可以在qemu之上运行一个操作系统,如linux系统。qemu 的system mode与常见的VMware和Virtualbox等虚拟机比较相似,但是qemu 的优势是可以跨指令集。例如,VMware和Virtualbox之类的工具通常只能在x86计算机上虚拟出一个x86计算机,而qemu 支持在x86上虚拟出一个ARM计算机。qemu在user mode配置下,可以运行跟当前平台指令集不同的平台可执行程序。例如可以用qemu在x86上运行ARM的可执行程序,但是两个平台必须是同一种操作系统,比如Linux。 qemu-system-riscv32 -nographic -smp 1 -machine virt -bios none -kernel ./xxx.elf -nographic:这个选项指示 QEMU 在没有图形界面的情况下运行,也就是说,不打开 VGA 显示设备,所有的输出将会直接在控制台显示。对于嵌入式开发或者远程服务器部署非常有用。 -smp 1:定义虚拟机中的 CPU 核心数量为 1。这意味着模拟的 RISC-V 系统将只有一个核心。 -machine virt:指定使用的虚拟机模型为 virt。这是 QEMU 提供的一个通用的、基于硬件的 RISC-V 虚拟机模型,适合于开发和测试目的。 -bios none:表示不使用任何固件(如 UEFI 或 OpenSBI)。在某些情况下,尤其是对于裸机环境或者自定义引导过程,不需要 BIOS 或者其他固件来启动系统。 -kernel ./xxx.elf:参数后面跟的是要加载作为虚拟机启动镜像的 ELF 文件的路径,这里是指向名为 xxx.elf 的文件。此 ELF 文件通常包含了已经编译好的操作系统内核或者是可以直接执行的裸机程序。会解析elf的入口地址,将其加载到对应的dram地址上。后续由bios执行结束后进行跳转,所以kernel的elf入口地址要与bios跳转地址一致,否则启动不了。 gdb调试 gdb-multiarch:是一个经过交叉编译后的、支持多架构版本的 gdb。 qemu-system-riscv32 -kernel xxx.elf -s -S & -s: “-gdb tcp::1234” 的缩写,启动 gdbserver 并在 1234 端口号上监听客户端 -S: 在启动时停止CPU (只有到在客户端键入'c' 才会开 始执行) gdb-multiarch xxx.elf -q -x gdbinit -q: 这是一个静默模式选项,表示在启动 GDB 时不打印欢迎信息和其他一些非关键信息,使得启动过程更简洁。 -x gdbinit: 使用 -x 选项可以让 GDB 加载并执行指定文件中的命令序列。这里是指定了 gdbinit 文件。在 gdbinit 文件中,你可以预先编写一系列的 GDB 自动化命令,比如设置断点、加载符号表、配置工作目录等,在 GDB 启动时自动执行,从而提升调试效率和一致性。 gdbinit 内容如下: display/z $x5 display/z $x6 display/z $x7 set disassemble-next-line on b _start target remote : 1234 c 1.启动与退出 gdb [program]:启动GDB并调试指定的程序。 quit 或 q:退出GDB调试器。 2.运行与控制 run [args] 或 r [args]:运行程序,可以传递命令行参数。 start:从程序的第一行开始单步执行。 continue 或 c:继续执行至下一个断点或程序结束。 next 或 n:单步执行,不进入函数内部。 nexti或ni:以机器指令为单位进行单步执行,不进入函数内部 step 或 s:单步执行,进入函数内部。 stepi或si:以机器指令为单位进行单步执行,遇到函数调用会进入函数内部 finish:执行到当前函数返回为止。 3.查看源代码 list 或 l [line number/function name]:显示源代码列表,默认显示当前行及其周围的代码。 4.断点管理 break 或 b [location]:在指定位置设置断点。如b a==2或b a.c:22或b func delete [breakpoint number] 或 d [number]:删除指定编号的断点。 clear [location]:清除在特定位置的断点。 info breakpoints 或 i b:列出所有已设置的断点信息。 5.查看 layout asm 可以查看所有的汇编代码 layout src 查看源代码,使用ctrl+x,再a,会到传统模式 display [expression]:动态监视表达式,在每次停止时自动显示其值。 info reg:查看所有寄存器 print 或 p [expression]:打印变量或表达式的值。可以查看具体代码变量的值,如p pxCurrentTCB->name。另外p还支持打印的格式/x来表示格式,p/x $reg:查看某个寄存器,如p/x $a5 6.栈回溯 backtrace 或 bt:显示当前调用栈的所有帧。 7.线程调试 thread 或 t:切换到指定线程。 info threads 或 i t:查看当前进程中所有线程的信息。 8.附加进程 attach [pid]:附加到一个正在运行的进程进行调试。 9.信号处理 handle signal:设置当GDB接收到特定信号时的行为。 10.读取内存 x/<count>/<format> <address>:查看内存内容 <count>:你要查看的内存单元的数量。 <format>:内存单元的显示格式,可以是: b(byte,字节), h(halfword,半字,对于RISC-V通常是16位), w(word,字,对于RISC-V通常是32位), g(giant,巨字,对于RISC-V通常是64位)等。 <address>:你要查看的内存地址,可以是具体的内存地址数值,也可以是变量的地址,如&myVar。 示例:x/6xw 0x地址;表示查看 6 个单元,每个单元是w(4字节), 11.其他 set args:设置下次运行时传递给程序的命令行参数。 show args:查看当前设置的程序运行参数。 file [executable]:加载新的可执行文件进行调试。 help [command] 或 h [command]:查看GDB中某个命令的帮助信息。 示例 display/z $x5: 此命令设置了一个动态观察点,每当程序停止时,GDB都会显示寄存器 $x5 的内容,且以十六进制(z)格式显示。在RISC-V架构中,$x5 是其中一个通用寄存器。 display/z $x6: 类似于上面的命令,但它关注的是 $x6 寄存器的值。 display/z $x7: 同样,设置了一个动态观察点来显示 $x7 寄存器的内容。 set disassemble-next-line on: 当执行 next 或 step 命令时,开启反汇编下一行代码的功能。这样在单步执行时,除了看到源代码,还会显示对应的汇编指令。 b _start: 在 _start 函数处设置一个断点,这是许多嵌入式或操作系统内核启动时的第一个入口点。 target remote : 1234: 这个命令告诉 GDB 与远程目标通信,并连接到本地主机的1234端口。在这种情况下,通常是通过QEMU模拟器运行的RISC-V系统,QEMU开启GDB服务器监听这个端口。 c 或 continue: 继续执行程序直至遇到断点(在这里是 _start 函数),或者如果没有断点,则一直执行到程序结束。如果之前已经建立了与远程目标的连接,那么GDB将控制远程系统的执行。 总结起来,这一系列命令首先设置了对几个寄存器的监控,然后开启了单步执行时显示下一行汇编代码的功能,接着在 _start 函数处设断点,最后连接到远程目标并继续执行程序。在整个过程中,GDB将根据设置实时展示相关寄存器的值和程序执行状态。 总结常用命令: layout src/asm, ctrl+x a退出 i b:查看断点,可以b 函数名补全打断点 n(i):不进入函数内部单步执行 s(i):进入函数内部单步执行 p:查看变量信息,可以配合/....使用 display/z $X1:动态监控寄存器 set disassemble-next-line on risc-v64 Xuantie 这里使用的是玄铁编译好的工具链,也是下载直接解压,设置环境变量即可。 下载工具链 https://www.xrvm.cn/community/download?id=4267734522939904000 下载的版本:Xuantie-900-gcc-linux-5.10.4-glibc-x86_64-V2.8.1-20240115.tar.gz 下载qemu https://www.xrvm.cn/community/download?id=4238019891233361920 下载的版本:XuanTie-DebugServer-linux-x86_64-V5.18.0-20230926.sh.tar.gz 执行qemu可能会少一些库,缺少就网上搜索一下,使用apt-get install xxx安装。 不带系统的测试例子:https://github.com/rvboards/test_c920 Linux运行 下载镜像(这里下载的版本920v2_images.tar.gz):https://github.com/c-sky/buildroot/releases 执行下面命令启动 qemu-system-riscv64 \\ -M virt \\ -cpu c910v \\ -bios ./images/fw_jump.bin \\ -kernel ./images/Image \\ -append "rootwait root=/dev/vda ro" \\ -drive file=./images/rootfs.ext2,format=raw,id=hd0 \\ -device virtio-blk-device,drive=hd0 \\ -nographic -smp 1 -M virt: 指定虚拟机使用 virt 平台。 -cpu c910v: 指定虚拟机使用 c910v CPU 模型。 -bios ./images/fw_jump.bin: 系统引导程序,opensbi。 -kernel ./images/Image: 指定使用指定路径下的 Image 文件作为内核镜像。 -append \"rootwait root=/dev/vda ro\": 设置内核启动参数,包括等待根文件系统、根文件系统路径为 /dev/vda、以只读模式挂载根文件系统。 -drive file=./images/rootfs.ext2,format=raw,id=hd0: 挂载名为 hd0 的硬盘设备,使用指定路径下的 rootfs.ext2 文件作为硬盘镜像,格式为 raw。 -device virtio-blk-device,drive=hd0: 添加一个 virtio 块设备,并将其连接到 hd0 硬盘。 -nographic: 在无图形界面的模式下运行虚拟机。 -smp 1: 设置虚拟机的 CPU 为单核。
\t