MIPS系统调用追踪(一)

说明:
内核版本: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的详细实现流程,其中包括保留现场,堆栈切换,参数处理等等精彩内容,敬请期待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值