说明:
内核版本:4.4
架构:mips64
c库:glibc-2.24
一、用户态到c库
这里以系统调用 sync_file_range举例。
原因有两点:一是这个系统调用在参数多达6个;二是参数中有64位参数,
这个系统调用的原型为:
int sync_file_range (int fd, __off64_t from, __off64_t to, unsigned int flags)
{
return SYSCALL_CANCEL (sync_file_range, fd,
__LONG_LONG_PAIR ((long) (from >> 32), (long) from),
__LONG_LONG_PAIR ((long) (to >> 32), (long) to),
flags);
}
将这个SYSCALL_CANCEL宏层层展开后,sysnc_file_range的定义如下所示:
int
sync_file_range (int fd, __off64_t from, __off64_t to, unsigned int flags)
{
return SYSCALL_CANCEL (sync_file_range, fd,
__LONG_LONG_PAIR ((long) (from >> 32), (long) from),
__LONG_LONG_PAIR ((long) (to >> 32), (long) to),
flags);
}
sync_file_range 展开==>
|SYSCALL_CANCEL (sync_file_range, fd,
__LONG_LONG_PAIR ((long) (from >> 32), (long) from),
__LONG_LONG_PAIR ((long) (to >> 32), (long) to),
flags);
==>
/* 为了表述方便 将上面的fd...flags这6个参数用a1~a6表示 */
|__SYSCALL_CALL (__VA_ARGS__) ==>
|__SYSCALL_DISP (__SYSCALL, __VA_ARGS__) ==>
|__SYSCALL_CONCAT (b,__SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__) ==>
|__SYSCALL6(sync_file_range, a1, a2, a3, a4, a5, a6) ==>
|INLINE_SYSCALL (sync_file_range, 6, a1, a2, a3, a4, a5, a6)
层展开后系统调用逐渐舒展开,最终到INLINE_SYSCALL()宏逐渐显露出系统调用的面目。对于mips64/n64,INLINE_SYSCALL展开如下:
#define INLINE_SYSCALL(name, nr, args...) \
({ INTERNAL_SYSCALL_DECL (_sc_err); \
long result_var = INTERNAL_SYSCALL (name, _sc_err, nr, args); \
if ( INTERNAL_SYSCALL_ERROR_P (result_var, _sc_err) ) \
{ \
__set_errno (INTERNAL_SYSCALL_ERRNO (result_var, _sc_err)); \
result_var = -1L; \
} \
result_var; })
这个宏大概包括三个部分:
(1) 变量申明
INTERNAL_SYSCALL_DECL (_sc_err)
/* sysdeps/unix/sysv/linux/mips/mips64/n64/sysdep.h */
#define INTERNAL_SYSCALL_DECL(err) long err __attribute__ ((unused))
声明了一个变量err用于保存函数调用的错误值
(2) 执行系统调用
INTERNAL_SYSCALL (name, _sc_err, nr, args)
这个是整个宏的核心部分,其功能就是执行系统调用这个动作。
(3)返回值与错误码处理
INTERNAL_SYSCALL_ERROR_P (result_var, _sc_err)和INTERNAL_SYSCALL_ERRNO (result_var, _sc_err)两个宏用于系统调用的返回值和错误码处理。
下面分别说一下第(2)和第(3)步。
1 执行系统调用
执行真正系统调用的宏INTERNAL_SYSCALL (name, _sc_err, nr, args)定义在 sysdeps/unix/sysv/linux/mips/mips64/n64/sysdep.h 文件,展开如下:
#define INTERNAL_SYSCALL(name, err, nr, args...) \
internal_syscall##nr ("li\t%0, %2\t\t\t# " #name "\n\t", \
"IK" (SYS_ify (name)), \
0, err, args)
其中SYS_ify(syscall_name)展开后就是代表name对应的系统调用号:
#define SYS_ify(syscall_name) __NR_##syscall_name
将系统调用name sync_file_range和SYS_ify宏展开后如下:
#define INTERNAL_SYSCALL(sync_file_range, err, 6, args...) \
internal_syscall6 ("li\t%0, %2\t\t\t# " #sync_file_range "\n\t", \
"IK" (__NR_sync_file_range), \
0, err, args)
接着,展开宏internal_syscall6:
#define internal_syscall6(v0_init, input, number, err, \
arg1, arg2, arg3, arg4, arg5, arg6) \
({ \
long _sys_result; \
\
{ \
register long __s0 asm ("$16") __attribute__ ((unused)) \
= (number); \
register long __v0 asm ("$2"); \
register long __a0 asm ("$4") = (long) (arg1); \
register long __a1 asm ("$5") = (long) (arg2); \
register long __a2 asm ("$6") = (long) (arg3); \
register long __a3 asm ("$7") = (long) (arg4); \
register long __a4 asm ("$8") = (long) (arg5); \
register long __a5 asm ("$9") = (long) (arg6); \
__asm__ volatile ( \
".set\tnoreorder\n\t" \
v0_init \
"syscall\n\t" \
".set\treorder" \
: "=r" (__v0), "+r" (__a3) \
: input, "r" (__a0), "r" (__a1), "r" (__a2), "r" (__a4), \
"r" (__a5) \
: __SYSCALL_CLOBBERS); \
err = __a3; \
_sys_result = __v0; \
} \
_sys_result; \
})
将入参和其他宏展开到internal_syscall6中===>
#define internal_syscall6(v0_init, input, 0, err,
fd, __LONG_LONG_PAIR ((long) (from >> 32), (long) from),
__LONG_LONG_PAIR ((long) (to >> 32), (long) to),flags)
{
long _sys_result;
{
/*
* 定义通用寄存器,并将入参放到合入的寄存器
* 对于mips n64,参数小于8个时依次放入a0~a7
*/
register long __s0 asm ("$16") __attribute__ ((unused))
= (number);
register long __v0 asm ("$2");
register long __a0 asm ("$4") = (long) (arg1);
register long __a1 asm ("$5") = (long) (arg2);
register long __a2 asm ("$6") = (long) (arg3);
register long __a3 asm ("$7") = (long) (arg4);
register long __a4 asm ("$8") = (long) (arg5);
register long __a5 asm ("$9") = (long) (arg6);
/* 下面这段内联汇编 开始真正系统调用 */
__asm__ volatile (
".set\tnoreorder\n\t"
"li\t%0, %2\t\t\t" #sync_file_range "\n\t /* 将系统调用号取到v0寄存器 */
"syscall\n\t" /* 前面参数已经准备就绪,执行syscall指令陷入内核 */
".set\treorder"
: "=r" (__v0), "+r" (__a3)
: "IK" (__NR_sync_file_range), "r" (__a0), "r" (__a1), "r" (__a2), "r" (__a4),
"r" (__a5)
: __SYSCALL_CLOBBERS);
err = __a3; /* 系统调用的错误码存放在a3寄存器 */
_sys_result = __v0; /* 函数返回值存放在v0 */
}
_sys_result;
}
上面的internal_syscall6()宏主要做的两件事情:(1)设置参数;(2)执行syscall指令陷入内核; (3) 保存返回值和错误码。
关于函数中参数如何传递,可以参考在mips n64的ABI标准如下:
$2~$3对应v0~v1,用于函数返回值;
$4~$11对应a0~a7,用于参数传递;
$16~$23对应s0~s7,需要保存的寄存器。
2 返回值与错误码处理
前面long result_var = INTERNAL_SYSCALL (name, _sc_err, nr, args);这个宏执行了真正的系统调用,执行完成后将返回值放到result_var,同时将a3寄存器的值放到_sc_err变量以指示是否发生错误。
if ( INTERNAL_SYSCALL_ERROR_P (result_var, _sc_err) )
{
__set_errno (INTERNAL_SYSCALL_ERRNO (result_var, _sc_err));
result_var = -1L;
}
result_var; })
将INTERNAL_SYSCALL_ERROR_P与INTERNAL_SYSCALL_ERRNO两个宏展开后即为:
if(((void) (result_var), (long) (_sc_err)))
{
errno = ((void) (_sc_err), result_var);
result_var = -1L;
}
首先检查_sc_err是否为0,如果不为0表示系统调用失败,并将系统调用返回值result_var赋值给errno。这样用户程序就可以通过errno知道发生了什么错误。
二. 异常发生时cpu的工作
当异常发生时,cpu会进行如下工作:
(1) 设置EPC,指向异常返回的位置;
(2) Status寄存器EXL自动置位,这会强行进入核心态并禁止中断;
(3) 设置Cause寄存器,使得软件可以查到异常的原因;
(4) CPU从 "普通异常"向量入口点取指执行,接下来的工作就交给软件去执行了。
有几点需要进行说明:
首先,上述这些工作都是自动完成,不需要代码来干预;
其次,mips架构中一旦发生异常SR寄存器的EXL位会自动置位,例如我们这里执行”syscall”指令;
最后,这个"普通异常"处理入口并非系统调用处理函数的入口。
三. 异常处理点分析
第三章中提到异常发生时cpu会自动跳转到普通异常处理入口点取指令准备执行,然后剩下的时候就交由软件来处理了。
问题来了:"普通异常"处理入口在哪里?它和syscall系统调用处理函数入口有何关系?
答案是:"普通异常"处理入口在地址在ebase + 0x180 处。
其中ebase是异常入口基地址,也就是寄存器ebase的值;而ebase+0x180存放的是称为“普通(generic)异常”处理函数except_vec3_generic的代码。实际上内核在初始化初期是将except_vec3_generic处理代码拷贝到ebase + 0x180处,拷贝的大小为0x80;
而内核中所有"普通异常"处理函数地址集中起来放到一个unsigned long exception_handlers[32]的数组中,其中syscall"异常"的异常处理程序地址这个数组的第8号位,即index为8;在发生异常时Cause寄存器的ExcCode域置为8就表示发生了syscall"异常"。
上面这些地址的安装与准备都是在trap_init函数中实施的,大致情况如下:
trap_init-->
|set_handler(0x180, &except_vec3_generic, 0x80);
|memcpy((void *)(ebase + 0x180), &except_vec3_generic, 0x80);
|set_except_vector(8, handle_sys);
|xchg(&exception_handlers[8], handle_sys)
拷贝到ebase + 0x180处的except_vec3_generic代码的主要任务就是根据casue寄存器的ExcCode域值,结合except_handlers[]数组找到实际发生的异常对应的处理函数的入口地址,然后跳转到异常处理函数去执行对应的异常处理,如下所示:
/* arch/mips/kernel/genex.S */
NESTED(except_vec3_generic, 0, sp)
.set push
.set noat
#if R5432_CP0_INTERRUPT_WAR
mfc0 k0, CP0_INDEX
#endif
/* 取casue寄存器的ExcCode域到k1,即[bit6,bit2]*/
mfc0 k1, CP0_CAUSE
andi k1, k1, 0x7c
#ifdef CONFIG_64BIT
dsll k1, k1, 1
#endif
/* 根据ExcCode值将具体处理函数地址放到k0寄存器,然后跳转去执行 */
PTR_L k0, exception_handlers(k1)
jr k0
.set pop
END(except_vec3_generic)
总结一下:
A、Syscall异常入口函数为handle_sys;
B、内核启动时先将Generic异常例程&except_vec3_generic拷贝到BASE+0X180处;
C、再将syscall异常入口函数handle_sys放到全局数组&exception_handlers[8]中;
D、当发生syscall异常,cpu自动跳转到BASE+0X180处执行except_vec3_generic,接着这个函数会根据cause寄存器的ExcCode域取出值8作为全局数组exception_handlers[]的index,这样就找到了syscall的异常入口函数handle_sys并跳转执行。
下一次,也就是MIPS系统调用追踪(二)中,我们将会探索mips-o32中系统调用处理函数handle_sys的详细实现流程,其中包括保留现场,堆栈切换,参数处理等等精彩内容,敬请期待。