risc-v vector.S解析
.align 和 .balign
.align n 表示按照 2^n 字节对齐.
.balign n 表示按照 n 字节对齐.
.section .text.vector_table // 将以下代码放入名为.text.vector_table的段
.align 2 // 要求该段起始地址按4字节(2^2)对齐,这是硬件常见要求.global vector_table // 声明vector_table为全局符号,链接器可知其地址
vector_table:j default_handler // 0x0: 复位向量(例如,CPU上电后第一条指令)j software_int_handler // 0x4: 软件中断处理程序入口j timer_int_handler // 0x8: 定时器中断处理程序入口j external_int_handler // 0xC: 外部中断处理程序入口// ... 其他异常和中断向量// 下面是各个中断/异常处理函数(也称为中断服务程序)
software_int_handler:// 处理软件中断的代码...mret // 从中断返回timer_int_handler:// 处理定时器中断的代码...mretexternal_int_handler:// 处理外部中断(如键盘、网络)的代码...mretdefault_handler:// 默认处理,比如遇到未定义异常时进入死循环j default_handler.weak的作用:
为中断向量定义一个默认的处理函数,这个函数如果用户没有定义,则是用这个定义的weak函数,否则使用用户自己定义的函数。
譬如:
.weak reserved_int_handler
情况一:如果用户在整个工程中没有在任何地方重新定义名为 reserved_int_handler的函数,那么链接器就会使用向量表中指定的那个默认的、弱的 reserved_int_handler函数。
•
情况二:如果用户重新定义了一个同名的函数(并且这个新定义是“强”的,即没有用 .weak声明),链接器则会优先使用用户提供的版本,并忽略弱的那个。这样,就实现了对特定中断的个性化处理 
j是强制跳转的意思,表示跳转到某个处理函数入口,如:
j reserved_int_handler // 跳转到reserved_int_handler

下面是一段入口汇编代码:
# RISC-V 启动代码示例:entry.S
# 功能:系统启动入口点、异常处理框架和栈初始化.section .text.entry        # 将代码放入特定的入口段 [2](@ref)
.align 2                   # 按 4 字节对齐(2^2),满足 RISC-V 指令对齐要求 [3](@ref)# 程序全局入口点(由链接脚本指定为起始地址)
.global _start
_start:# 1. 设置栈指针(每个硬件线程一个栈)la sp, stack_top       # 加载栈顶地址到 sp 寄存器 [5](@ref)csrr a0, mhartid       # 读取当前硬件线程 ID (hartid) [3](@ref)addi a0, a0, 1         # 计算栈偏移:栈大小 = (hartid + 1) * 栈大小 [5](@ref)li a1, 4096            # 每个栈大小为 4KBmul a0, a0, a1add sp, sp, a0          # 调整栈指针指向当前 hart 的栈顶# 2. 设置异常向量表基地址(mtvec)la a0, trap_entry       # 加载异常处理入口点地址 [1](@ref)csrw mtvec, a0          # 写入 mtvec CSR,指定异常/中断处理程序地址 [3](@ref)# 3. 跳转到 C 主函数(如 main 或 kern_init)call kern_init          # 尾调用内核初始化函数 [2](@ref)j .                     # 防止返回,进入无限循环(安全兜底)# 异常处理入口点(所有异常和中断共用一个入口 [1](@ref))
.align 2
.global trap_entry
trap_entry:# 保存上下文:将所有通用寄存器压栈 [1](@ref)addi sp, sp, -32 * 8    # 为 32 个寄存器预留栈空间(每个寄存器 8 字节)sd x1, 0(sp)            # 保存 x1 (ra)sd x2, 8(sp)            # 保存 x2 (sp)# ... 省略保存 x3-x31 的代码(实际需完整保存)sd x31, 248(sp)# 读取异常原因和返回地址 [1](@ref)csrr a0, mcause         # 将异常原因码存入 a0(作为参数)csrr a1, mepc           # 将异常返回地址存入 a1(作为参数)mv a2, sp               # 将当前栈指针作为第三个参数# 调用 C 语言异常处理函数 [1](@ref)call handle_trap# 恢复上下文(从栈中还原寄存器)ld x31, 248(sp)# ... 省略恢复 x3-x30 的代码ld x2, 8(sp)ld x1, 0(sp)addi sp, sp, 32 * 8     # 恢复栈指针# 返回到异常发生前的指令位置 [3](@ref)mret# 弱符号定义:默认的异常处理函数(避免链接错误) [1](@ref)
.weak handle_trap
handle_trap:j handle_trap           # 默认实现为死循环(安全兜底)# 栈空间分配(在 .bss 段中)
.section .bss
.align 12                   # 按 4KB 对齐栈空间
stack_bottom:.space 4096 * 4         # 为 4 个硬件线程分配栈空间(每个 4KB) [5](@ref)
stack_top:                  # 栈顶地址(由链接脚本定义或此处标号)stack_top存储的是栈顶的地址(也是一个栈空间的数值最低的地址), mhartid 指的是ID值(0-N),假设每个栈的大小都是4KB,有3个hartid,则分配给每个hart的栈空间的地址stack_top + *(n+1)*4k(其中 n的取值是0,1,2)。
譬如
hart0的栈空间为 0x9000 ~ 0x8FFF
hart1的栈空间为 0xA000 ~ 0x9FFF
hart2的栈空间为 0xB000 ~ 0xAFFF
举个例子:stack_top=0x800,此时要入栈s0, s1寄存器存储的值,
分配栈空间:
栈需要先向下生长8字节,用来存放s0和s1的值,通过对SP值减去分配的字节长来向下生长,SP值变成 0x800-0x8 = 0x7F8
进栈的时候:
首先s0进栈,则,s0存放在SP+0的地址,即0x7F8,
接着s1进栈,则,s1存放在SP+4的地址,即0x7FC,
出栈的时候:
首先是s1出栈,即将0x7FC地址里的值加载到s1中,SP值不变
接着是s0出栈,即将0x7F8地址里的值加载到s0中,SP值不变
释放分配的空间:
将SP的值加上要释放的字节长度, SP = SP + 8, SP最后变成0x800
SP指向哪里?SP始终指向栈空间最后一个(即最新)存入的有效数据的位置。在上面的例子里,存入
s1后,SP就指向存放s1值的地址(0x7FF8)。栈的生长方向:在RISC-V和大多数现代计算机体系结构中,栈在需要更多空间时是从高地址向低地址方向“生长”的。这就是为什么分配空间时是给SP减去一个数(
addi sp, sp, -8)。先进后出(LIFO):你会发现,
s1是先被保存进去的,但它是后被取出来的。这就是栈的“后进先出”特点,就像叠盘子,你最后放上去的那个盘子,总是最先被拿走。内存安全:通过移动SP来分配和释放空间,确保了函数不会意外覆盖其他数据。只要SP在函数结束时被正确恢复,内存使用就是安全的。
.cfi_startproc 和 .cfi_undefined ra
.cfi_startproc 在定义的函数里面开始的地方,如:
_entry:
.cfi_startproc @ 在函数开头,标志函数的开始,并为生成函数的调用帧信息(CFI)做准备
.cfi_undefined ra @当前位置的ra(返回地址)寄存器的值还没有定义(无效)
