MIPS中的异常处理和系统调用【转】
转自:http://blog.csdn.net/jasonchen_gbd/article/details/44044091
权声明:本文为博主原创文章,转载请附上原博链接。
异常入口
系统调用是用户态和内核态通信的一种方式,用户程序可以直接调用系统调用的接口陷入内核中执行相关任务,完成后返回用户态继续运行。
应用程序使用系统调用很简单,直接调用C库提供的系统调用接口即可。在C库中,对用户传入的参数进行分析和保存,然后通过syscall指令引发系统调用异常,之后便陷入内核。
内核处理根据系统调用号执行相应的处理函数,并将结果返回到用户态。
图1 系统调用大体流程
当发生异常时,协处理器0的Cause寄存器会记录发生了什么种类的异常。Cause寄存器的每个域如图2所示。其中bit6-2(ExcCode)位中保存了具体发生了什么异常,系统可以根据异常种类决定调用哪一个异常处理例程。
图2 Cause寄存器
所有的异常入口都位于mips内存映射中不需要地址转换的区域——非缓存的kseg1段和缓存的kseg0段。如图3所示,RAM中的异常入口点的起始地址为BASE+0x000,BASE表示EBase寄存器编程的异常基地址。一些特殊的异常的处理例程有单独的地址存放其异常处理例程,如缓存异常和TLB重填等,其他异常处理例程都放在BASE+0x180地址处。
图3 异常处理入口
BASE+0x180共存放了32种异常的入口函数地址,图4中显示了部分异常类型对应的ExcCode值,可以看到其中系统调用对应的ExcCode等于8。当发生系统调用时,内核就可以根据Cause寄存器查看异常类型,然后跳转到BASE+0x180地址处执行,执行的结果就是找到对应的处理函数并跳转到处理函数的地址去执行。
图4 异常类型
这些异常处理函数的注册在trap_init()函数中完成,该函数将上面所说的32个异常的处理函数地址放到一个全局数组exception_handlers中,这个全局变量定义为:
unsigned long exception_handlers[32];
这个全局变量是unsigned long型,每个元素的值就是一种异常向量处理函数的入口地址。
那BASE+0x180地址处的代码如何找到异常对应的处理函数呢。trap_init()函数中将except_vec3_generic拷贝到了BASE+0x180,这是一个函数,其实现如下:
NESTED(except_vec3_generic, 0, sp)
.set push
.set noat
mfc0 k1, CP0_CAUSE #读取协处理器0的cause寄存器保存到k1中。
andi k1, k1, 0x7c #取得k1的2-6位,即excCode #取得exception_handlers[excCode]的值
PTR_L k0, exception_handlers(k1)
jr k0 #跳转到excCode对应的处理函数去执行
.set pop
END(except_vec3_generic)
由except_vec3_generic的实现可知,它负责读取Cause寄存器并跳转到异常处理函数。
产生异常时,MIPS CPU所要做的主要工作为:
- 设置EPC,指向异常返回的位置。
- 置Status寄存器的EXL位,迫使CPU进入内核模式(高特权级)并且禁用中断。
- 设置Cause寄存器,使得软件可以看到异常的原因。
- CPU从异常处理入口点取指执行,即执行异常处理程序。
异常处理的流程主要包括以下步骤:
- 保护现场,将各个寄存器的值压栈,以便处理完之后回到原来的指令流。
- 根据硬件设置的寄存器标志,判断是什么异常并执行具体的异常处理函数。
- 恢复现场,将栈里保存的寄存器的值再写回。
- 跳转到正常指令流断点,回到CPU正常的指令流。
以系统调用为例,用户程序执行系统调用后,C库通过执行syscall指令产生一个软件异常,进行上面的一系列工作后会定位到系统调用的处理函数handle_sys。
系统调用代码分析
系统调用表
内核支持的系统调用都放在一张全局的系统调用表sys_call_table中,所有的系统调用按照系统调用号从大到小的顺序存放。这个表中每个条目的大小为8字节,定义如下:
.macro sys function, nargs
PTR \function
LONG (\nargs << 2) - (5 << 2)
.endm
可以看出,系统调用表中每个条目由两部分组成,前4个字节是处理函数function的地址,后4个字节为(\nargs << 2) -(5 << 2),nargs是系统调用的参数个数,这个表达式的结果用来判断参数个数是否超过4个。
下面通过分析handle_sys函数的实现介绍系统调用在内核态所做的工作。
备份通用寄存器
将异常发生时的当前进程的通用寄存器的值保存起来,并确定异常返回地址epc的值,使其可以正常返回。
在C库执行syscall指令之前,会先把系统调用号存放到寄存器v0中,并将需要传递的参数放到a0-a4中,如果参数个数大于四个,就需要保存在栈里面。
handle_sys的开头先通过SAVE_SOME宏将当前进程的通用寄存器的值备份到进程栈中:
NESTED(handle_sys, PT_SIZE, sp)
.set noat
SAVE_SOME # 见下面对该函数的分析
TRACE_IRQS_ON_RELOAD # not implemented
STI #进入内核模式,使能全局中断
.set at lw t1, PT_EPC(sp) # 取出epc的值。这时应该指向syscall指令 /* v0中存放着系统调用号,由于系统调用号是从4000开始的,所以将v0修改为实际的序号: v0 = v0 – 4000 */
subu v0, v0, __NR_O32_Linux
/* 判断系统调用号的合法性 */
sltiu t0,v0, __NR_O32_Linux_syscalls + 1
addiu t1, 4 #skip to next instruction
sw t1, PT_EPC(sp) # 跳过syscall指令,这样返回时可以继续执行
beqz t0, illegal_syscall # if(t0== 0) illegal syscall.
SAVE_SOME宏的定义如下:
.macro SAVE_SOME
.set push
.set noat
.set reorder
mfc0 k0, CP0_STATUS
sll k0, 3 /* k0 = k0 << 3,即CU0成了最高位 */
.set noreorder
/* 最高位是1就是负数,小于0。CU0=1则得到用户特权级别 */
bltz k0, 8f /*if k0 < 0, goto 8: */
move k1, sp /* 延迟槽,如果是内核态进来的,直接获取sp的值放到k1中 */
.set reorder
/* 如果是从用户态进来的,则需要使用kernel中保存的sp */
get_saved_sp /* 读取全局kernelsp中的sp的值到k1中。 */ 8: move k0, sp /* 把原来的sp的值放到k0中保存。 */
/* sp = k1 - sizeof(struct pt_regs),由于kernelsp存放的是sp + _THREAD_SIZE - 32,所以这里得到的sp就是进程地址空间的栈顶。 */
PTR_SUBU sp, k1, PT_SIZE /* 将k0的值(即刚保存的sp)保存到进程的pt_regs.regs[29] */
LONG_S k0, PT_R29(sp)
LONG_S $3, PT_R3(sp) /* 保存v1的值 */
/*
* You might think that you don't need to save$0,
* but the FPU emulator and gdb remote debugstub
* need it to operate correctly
*/
LONG_S $0, PT_R0(sp) /* 保存$0的值 */
mfc0 v1, CP0_STATUS
LONG_S $2, PT_R2(sp) /* 保存v0的值 */
LONG_S v1, PT_STATUS(sp) /* 保存cp0_status的值 */ LONG_S $4, PT_R4(sp) /* 保存a0的值 */
mfc0 v1, CP0_CAUSE
LONG_S $5, PT_R5(sp) /* 保存a1的值 */
LONG_S v1, PT_CAUSE(sp) /* 保存cp0_cause的值 */
LONG_S $6, PT_R6(sp) /* 保存a2的值 */
MFC0 v1, CP0_EPC
LONG_S $7, PT_R7(sp) /* 保存a3的值 */ LONG_S v1, PT_EPC(sp) /* 保存cp0_epc的值 */
LONG_S $25, PT_R25(sp) /* 保存t9的值 */
LONG_S $28, PT_R28(sp) /* 保存gp的值 */
LONG_S $31, PT_R31(sp) /* 保存ra的值 */
ori $28, sp, _THREAD_MASK /* gp = sp | 0x1FFF */
/* gp = gp ^ 0x1FFF,即sp的末13位清0赋值给gp,内核栈的大小就是8K,所以,这里的结果就是gp指向栈顶。 */
xori $28, _THREAD_MASK .set pop
.endm
这里需要说明一下内核线程的内核栈空间,内核栈是从高地址向下延伸的,大小为两个页,即8K。为了方便的定位到进程的task_struct结构,进程的thread_info结构被放在栈底(低地址),这样,在进程地址空间内的任何地址,只需将末13位清零就是thread_info的位置,再通过thread_info结构体的task指针可以很快找到进程的task_struct结构。
在创建进程时,在栈顶(高地址)会预留32字节的空间,这32字节目前没有被使用,可能是为了防止溢出而导致覆盖了进程的重要信息。在32字节下面是一个struct pt_regs结构体,它的目的是为了在发生系统调用或其他异常时,保存进程的重要寄存器的值,如通用寄存器和CP0的寄存器。在距离栈顶32Bytes +sizeof(struct pt_regs)的位置才是sp的初始位置。
获得参数个数并执行处理程序
根据系统调用号在sys_call_table中找到该系统调用需要几个参数。
# v0左移3位。因为sys_call_table中每个条目占用8字节。
sll t0, v0, 3
la t1, sys_call_table # t1中存放sys_call_table的地址
addu t1, t0 # t1 = t1 + t0。得到要找的系统调用的地址。 lw t2, (t1) # 把处理函数地址放到t2中
lw t3, 4(t1) # t3中存放是否参数个数大于4
beqz t2, illegal_syscall # 如果找不到处理函数,非法 sw a3, PT_R26(sp) # save a3for syscall restarting
bgez t3, stackargs # 如果t3>=0,则参数大于4个,需要栈
在上面的代码中,t2中保存了系统调用处理函数的地址。而t3的值就有两层意思:
1. 如果t3小于0,说明系统调用的参数少于或等于4个。
2. 如果t3大于等于0,那t3的取值可能是0,4,8,16,分别对应5,6,7,8个参数的情况。这里t3赋值成4的倍数是为了两个相邻值之间相差一个指令的长度,在下面获取参数时利用了这一点。
如果参数个数小于等于4个,就不需要使用栈保存参数,那处理很简单:如果需要跟踪系统调用,在执行系统调用之前,需要通知父进程。一般情况下,我们不需要跟踪系统调用,所以直接跳转到系统调用的处理函数。
stack_done:
lw t0, TI_FLAGS($28) # 得到进程的thread_info.flags
li t1, _TIF_SYSCALL_TRACE | _TIF_SYSCALL_AUDIT
and t0, t1 # thread_info.flags是否设置了上面两个标志
bnez t0, syscall_trace_entry # 如果设置了,跳到处理函数 jalr t2 # 进入系统调用处理函数
如果参数个数大于4个,我们需要在栈用获取多余的参数,然后再跳转到上面的stack_done调用处理函数。
stackargs:
lw t0, PT_R29(sp) # get olduser stack pointer /*
* We intentionally keep the kernel stack alittle below the * top of userspace so we don't have to do a slower byteaccurate check here.
*/
lw t5, TI_ADDR_LIMIT($28) # 获得thread_info.addr_limit
addu t4, t0, 32 # sp + 32就是栈的高地址
and t5, t4
/*
* addr_limit有两种:
* 0-0x7FFFFFFF for user-thead
* 0-0xFFFFFFFF for kernel-thread
*/
bltz t5, bad_stack # t5 < 0即位于了内核态,不合法 /* Ok, copy the args fromthe luser stack to the kernel stack.
* t3 is the precomputed number of instructionbytes needed to
* load or store arguments 6-8.
*/ la t1, 5f # load up to 3arguments
# 通过上面赋值,t3可能取0, 4, 8, 16对应5,6, 7, 8个参数。
subu t1, t3
1: lw t5, 16(t0) # argument #5from usp 取出#5
.set push
.set noreorder
.set nomacro
jr t1 # 根据参数个数跳转
addiu t1,6f - 5f # 延迟槽,跳转同时把t1加上6f -5f. 2: lw t8, 28(t0) # argument #8from usp
3: lw t7, 24(t0) # argument #7from usp
4: lw t6, 20(t0) # argument #6from usp
5: jr t1
sw t5,16(sp) # argument #5 to ksp 延迟槽,跳转同时存入#5 sw t8, 28(sp) # argument #8 toksp
sw t7, 24(sp) # argument #7 toksp
sw t6, 20(sp) # argument #6 toksp
6: j stack_done # 跳回和小于等于4个参数相同的处理流程
nop
.set pop
准备返回到用户态
系统调用的处理程序执行完成后,就要准备返回用户空间了。
#
# 准备系统调用的返回值。
#
li t0, -EMAXERRNO - 1 # error?
sltu t0, t0, v0 # if t0< v0, t0 =1, else t0 = 0.
sw t0, PT_R7(sp) #把t0的值存到a3里去。
beqz t0, 1f # if t0 == 0,goto 1: negu v0 # error, v0 = -v0
sw v0, PT_R0(sp) # set flagfor syscall
# restarting
1: sw v0, PT_R2(sp) # result, v0存到pt_regs[2]中 o32_syscall_exit:
local_irq_disable # make sure need_resched and
# signalsdont change between
# samplingand return
# 下面的内容还是和trace syscall相关的,在系统调用完成后,通知父进程。
lw a2, TI_FLAGS($28) #current->work
li t0, _TIF_ALLWORK_MASK
and t0, a2
bnez t0, o32_syscall_exit_work j restore_partial /* 恢复寄存器,并返回 */
可以看到,系统调用将a3和v0返回给用户态,经过上面的代码处理,这两个寄存器中的值的含义如下:
- a3存放系统调用是否成功,成功就是0,失败就是1。
- v0存放系统调用的返回值,如果是负数且位于[-EMAXERRNO,-1]之间,v0就是错误码。否则,v0是该系统调用本来想返回的东西。注意,有效错误码的范围在1~ EMAXERRNO之间。
- 如果v0是错误码,就先转换成正数,再返回,这样用户态可直接识别。
返回到C库后,会根据a3判断是成功还是失败,如果成功就给用户程序返回v0。如果失败,就将v0写到errno中,然后根据该系统调用的规定,给用户程序返回失败时的返回值。
代码的最后跳转到restore_partial中去,它的定义很简单:
FEXPORT(restore_partial) #restore partial frame
RESTORE_SOME
RESTORE_SP_AND_RET
其中RESTORE_SOME对应最开头的SAVE_SOME。而RESTORE_SP_AND_RET做了两件事情:
1. 将进程栈中保存的sp的值恢复,赋值给sp寄存器。
2. 将进程栈中保存的epc的值恢复,并跳转到epc指向的地址。而开头讲到过,这时epc指向syscall指令的下一条指令,即继续执行C库中调用syscall指令之后的代码。
.macro RESTORE_SP_AND_RET
.set push
.set noreorder
LONG_L k0, PT_EPC(sp)
LONG_L sp, PT_R29(sp)
jr k0
rfe #在异常返回前恢复CPU状态
.set pop
.endm
MIPS中的异常处理和系统调用【转】的更多相关文章
- ASID 与 MIPS 中 TLB 相关
ASID 为了提高TLB的性能,将TLB分成Global和process-specific.global 是指常驻在tlb中不会被刷出的,例如内核空间的翻译,process-specific 是指每个 ...
- 【repost】JS中的异常处理方法分享
我们在编写js过程中,难免会遇到一些代码错误问题,需要找出来,有些时候怕因为js问题导致用户体验差,这里给出一些解决方法 js容错语句,就是js出错也不提示错误(防止浏览器右下角有个黄色的三角符号,要 ...
- 第65课 C++中的异常处理(下)
1. C++中的异常处理 (1)catch语句块可以抛出异常 ①catch中获捕的异常可以被重新抛出 ②抛出的异常需要外层的try-catch块来捕获 ③catch(…)块中抛异常的方法是throw; ...
- Swift基础--Swift中的异常处理
Swift中的异常处理 OC中的异常处理:方法的参数要求传入一个error指针地址,方法执行完后,如果有错误,内部会给error赋值 Swift中的异常处理:有throws的方法,就要try起来,然后 ...
- ASP.NET Web API 中的异常处理(转载)
转载地址:ASP.NET Web API 中的异常处理
- Struts2中的异常处理
因为在Action的execute方法声明时就抛出了Exception异常,所以我们无需再execute方法中捕捉异常,仅需在struts.xml 中配置异常处理. 为了使用Struts2的异常处理机 ...
- C++中的异常处理(三)
C++中的异常处理(三) 标签: c++C++异常处理 2012-11-24 23:00 1520人阅读 评论(0) 收藏 举报 分类: 编程常识(2) 版权声明:本文为博主原创文章,未经博主允许 ...
- C++中的异常处理(二)
C++中的异常处理(二) 标签: c++C++异常处理 2012-11-24 20:56 1713人阅读 评论(2) 收藏 举报 分类: C++编程语言(24) 版权声明:本文为博主原创文章,未经 ...
- C++中的异常处理(一)
来自:CSDN 卡尔 后续有C++中的异常处理(二)和C++中的异常处理(三),C++中的异常处理(二)是对动态分配内存后内部发生错误情况的处理方法,C++中的异常处理(三)中是使用时的异常说明. ...
随机推荐
- 原型与原型继承demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 【转】Qt Socket简单通信
最近要用到Qt的Socket部分,网上关于这部分的资料都比较复杂,我在这总结一下,把Socket的主要部分提取出来,实现TCP和UDP的简单通信. 1.UDP通信 UDP没有特定的server端和cl ...
- 控件中添加的成员变量value和control的区别
control型变量是这个控件所属类的一个实例(对象)可以通过这个变量来对该控件进行一些设置.而value只是用来传递数据,不能对控件进行其它的操作.control型变量可以获得控件的实例,通过这个变 ...
- 【转】浅谈对主成分分析(PCA)算法的理解
以前对PCA算法有过一段时间的研究,但没整理成文章,最近项目又打算用到PCA算法,故趁热打铁整理下PCA算法的知识.本文观点旨在抛砖引玉,不是权威,更不能尽信,只是本人的一点体会. 主成分分析(PCA ...
- Unity基础-外部导入C# Dll(汇编集)
外部导入C# Dll(汇编集) 使用创建一个dll工程 添加依赖的dll 导入Unity中,放入Assets的任意文件夹中 使用代码生成的dll汇编集只要"use dll的名字"引 ...
- Linux中的常见命令
1. ls 查看当前目录下的所有文件夹 2. pwd 查看当前所在的文件夹 3. cd 目录名 切换文件夹 4. touch 文件名 创建文件 5. mkdir 目录名 创建文件夹 6 ...
- python之绝对导入和相对导入
绝对导入 import sys, os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) sys.path.append(BASE_DIR) ...
- HDU 4565 So Easy! 矩阵快速幂
题意: 求\(S_n=\left \lceil (a+\sqrt{b})^n \right \rceil mod \, m\)的值. 分析: 设\((a+\sqrt{b})^n=A_n+B_n \sq ...
- selenium - js日历控件处理
# 13. js处理日历控件 ''' 在web自动化的工程中,日历控制大约分为两种: 1. 可以直接输入日期 2. 通过日历控件选择日期 基本思路: 利用js去掉readonly属性,然后直接输入时间 ...
- Leetcode 427.建立四叉树
建立四叉树 我们想要使用一棵四叉树来储存一个 N x N 的布尔值网络.网络中每一格的值只会是真或假.树的根结点代表整个网络.对于每个结点, 它将被分等成四个孩子结点直到这个区域内的值都是相同的. 每 ...