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