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的詳細實現流程,其中包括保留現場,堆棧切換,參數處理等等精彩內容,敬請期待。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章