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++中的异常处理(三)中是使用时的异常说明. ...
随机推荐
- iOS 开发 Xib 的嵌套使用
最近公司项目需要使用 Xib 中嵌套 Xib来布局界面的, 研究了很久才实现!!! 分享给大家,希望帮助到更多的开发者...... 开发中自定义界面有两种方式 一: 纯代码实现 适合单个极度复杂的界面 ...
- 文件下载(NSURLConnection/NSURLSession)
最基本的网络文件下载(使用原生的网络请求) #pragma mark - 小文件下载 // 方法一: NSData dataWithContentsOfURL - (void)downloadFile ...
- 使用三层交换配置DHCP为不同VLAN分配IP地址
三层交换的原理以及DHCP的原理,作者在这里就不详细的解释了,在这里通过一个案例来了解使用三层交换做DHCP服务器,并为不同网段分配IP地址.在生产环境中,使用路由器或交换机做DHCP服务器要常见一些 ...
- MySQL中 IFNULL、NULLIF和ISNULL函数的用法
mysql 中 ifnull().nullif().isnull()函数的用法讲解: 一.IFNULL(expr1,expr2)用法: 假如expr1不为NULL,则 IFNULL() 的返回值为ex ...
- MySQL如何复制一个表
MySQL如何复制一个表 1 复制 employee 表 => employee2 () create table employee2 like employee () insert into ...
- python数据类型之字典(dict)和其常用方法
字典的特征: key-value结构key必须可hash,且必须为不可变数据类型.必须唯一. # hash值都是数字,可以用类似于2分法(但比2分法厉害的多的方法)找.可存放任意多个值.可修改.可以不 ...
- python模块之datetime
相比于time模块,datetime模块的接口则更直观.更容易调用 datetime模块定义了下面这几个类: datetime.date:表示日期的类.常用的属性有year, month, day: ...
- JAVA基础篇—接口实现动态创建对象
Scanner在控制台输入内容 package com.Fruit; public interface Fruit {//提供接口 } package com.Fruit; public class ...
- MySQL主从复制(Master-Slave)
MySQL数据库自身提供的主从复制功能可以方便的实现数据的多处自动备份,实现数据库的拓展.多个数据备份不仅可以加强数据的安全性,通过实现读写分离还能进一步提升数据库的负载性能. 下图就描述了一个多个数 ...
- FFT、NTT学习笔记
参考资料 picks miskcoo menci 胡小兔 unname 自为风月马前卒 上面是FFT的,学完了就来看NTT吧 原根 例题:luogu3803 fft优化后模板 #include < ...