一、Syscall意義
內(nèi)核提供用戶空間程序與內(nèi)核空間進(jìn)行交互的一套標(biāo)準(zhǔn)接口,這些接口讓用戶態(tài)程序能受限訪問硬件設(shè)備,比如申請(qǐng)系統(tǒng)資源,操作設(shè)備讀寫,創(chuàng)建新進(jìn)程等。用戶空間發(fā)生請(qǐng)求,內(nèi)核空間負(fù)責(zé)執(zhí)行,這些接口便是用戶空間和內(nèi)核空間共同識(shí)別的橋梁,這里提到兩個(gè)字“受限”,是由于為了保證內(nèi)核穩(wěn)定性,而不能讓用戶空間程序隨意更改系統(tǒng),必須是內(nèi)核對(duì)外開放的且滿足權(quán)限的程序才能調(diào)用相應(yīng)接口。
在用戶空間和內(nèi)核空間之間,有一個(gè)叫做Syscall(系統(tǒng)調(diào)用, system call)的中間層,是連接用戶態(tài)和內(nèi)核態(tài)的橋梁。這樣即提高了內(nèi)核的安全型,也便于移植,只需實(shí)現(xiàn)同一套接口即可。Linux系統(tǒng),用戶空間通過向內(nèi)核空間發(fā)出Syscall,產(chǎn)生軟中斷,從而讓程序陷入內(nèi)核態(tài),執(zhí)行相應(yīng)的操作。對(duì)于每個(gè)系統(tǒng)調(diào)用都會(huì)有一個(gè)對(duì)應(yīng)的系統(tǒng)調(diào)用號(hào),比很多操作系統(tǒng)要少很多。
安全性與穩(wěn)定性:內(nèi)核駐留在受保護(hù)的地址空間,用戶空間程序無法直接執(zhí)行內(nèi)核代碼,也無法訪問內(nèi)核數(shù)據(jù),通過系統(tǒng)調(diào)用
性能:Linux上下文切換時(shí)間很短,以及系統(tǒng)調(diào)用處理過程非常精簡(jiǎn),內(nèi)核優(yōu)化得好,所以性能上往往比很多其他操作系統(tǒng)執(zhí)行要好。
二、Syscall查找方式
這里以文章理解殺進(jìn)程的實(shí)現(xiàn)原理中的kill()方法為例子,來找一找kill()方法系統(tǒng)調(diào)用的過程。
Tips 1:?用戶空間的方法xxx,對(duì)應(yīng)系統(tǒng)調(diào)用層方法則是sys_xxx;?
Tips 2:?unistd.h文件記錄著系統(tǒng)調(diào)用中斷號(hào)的信息。
故用戶空間kill方法則對(duì)應(yīng)系統(tǒng)調(diào)用層便是sys_kill,這個(gè)方法去哪里找呢?從/kernel/include/uapi/asm-generic/unistd.h等還有很多unistd.h去慢慢查看,查看關(guān)鍵字sys_kill,便能看到下面幾行:
/* kernel/signal.c */
__SYSCALL(__NR_kill, sys_kill)
根據(jù)這個(gè)能得到一絲線索,那就是kill對(duì)應(yīng)的方法sys_kill位于/kernel/signal.c文件。
Tips 3:?宏定義SYSCALL_DEFINEx(xxx,…),展開后對(duì)應(yīng)的方法則是sys_xxx;?
Tips 4:?方法參數(shù)的個(gè)數(shù)x,對(duì)應(yīng)于SYSCALL_DEFINEx。
kill(int pid, int sig)方法共兩個(gè)參數(shù),則對(duì)應(yīng)方法于SYSCALL_DEFINE2(kill,...),進(jìn)入signal.c文件,再次搜索關(guān)鍵字,便能看到方法:
SYSCALL_DEFINE2(kill, pid_t, pid,?int, sig)
{
struct siginfo info;
info.si_signo = sig;
info.si_errno =?0;
info.si_code = SI_USER;
info.si_pid = task_tgid_vnr(current);
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
return kill_something_info(sig, &info, pid);
}
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)?基本等價(jià)于?asmlinkage long sys_kill(int pid, int sig),這里用的是基本等價(jià),往下看會(huì)解釋原因。
回到頂部
實(shí)用技巧
比如kill命令, 有兩個(gè)參數(shù). 則可以直接在kernel目錄下搜索 “SYSCALL_DEFINE2(kill”,即可直接找到,所有對(duì)應(yīng)的Syscall方法位于signal.c
三、Syscall流程
Syscall是通過中斷方式實(shí)現(xiàn)的,ARM平臺(tái)上通過swi中斷來實(shí)現(xiàn)系統(tǒng)調(diào)用,實(shí)現(xiàn)從用戶態(tài)切換到內(nèi)核態(tài),發(fā)送軟中斷swi時(shí),從中斷向量表中查看跳轉(zhuǎn)代碼,其中異常向量表定義在文件/kernelarch/arm/kernel/entry-armv.S(匯編語言文件)。當(dāng)執(zhí)行系統(tǒng)調(diào)用時(shí)會(huì)根據(jù)系統(tǒng)調(diào)用號(hào)從系統(tǒng)調(diào)用表中來查看目標(biāo)函數(shù)的入口地址,在calls.S文件中聲明了入口地址信息。
總體流程:kill() -> kill.S -> swi陷入內(nèi)核態(tài) -> 從sys_call_table查看到sys_kill -> ret_fast_syscall -> 回到用戶態(tài)執(zhí)行kill()下一行代碼。 下面介紹部分核心流程:
3.1:?用戶程序通過軟中斷swi指令切入內(nèi)核態(tài),執(zhí)行vector_swi處的指令。vector_swi在文件/kenel/arch/arm/kernel/entry-common.S中定義,此處省略。像每一個(gè)異常處理程序一樣,要做的第一件事當(dāng)然就是保護(hù)現(xiàn)場(chǎng)了。緊接著是獲得系統(tǒng)調(diào)用的系統(tǒng)調(diào)用號(hào)
3.2:?仍以kill()函數(shù)為例,來詳細(xì)說說Syscall調(diào)用流程,用戶空間kill()定義位于文件kill.S。
#include
ENTRY(kill)
mov ip, r7
ldr r7, =__NR_kill
swi?#0
mov r7, ip
cmn r0,?#(MAX_ERRNO + 1)
bxls lr
neg r0, r0
b __set_errno_internal
END(kill)
當(dāng)調(diào)用kill時(shí), 系統(tǒng)先保存r7內(nèi)容, 然后將__NR_kill值放入r7, 再執(zhí)行swi軟中斷指令切換進(jìn)內(nèi)核態(tài)。
3.3:?Linux內(nèi)核中,每個(gè)Syscall都有唯一的系統(tǒng)調(diào)用號(hào)對(duì)應(yīng),kill的系統(tǒng)調(diào)用號(hào)為__NR_kill,用戶空間的系統(tǒng)調(diào)用號(hào)定義于/bionic/libc/kernel/uapi/asm-generic/unistd.h,如下:
#define __NR_kill (__NR_SYSCALL_BASE + 37)
其中__NR_SYSCALL_BASE=0,也就是__NR_kill系統(tǒng)調(diào)用號(hào)=37。
3.4:?在內(nèi)核中有與系統(tǒng)調(diào)用號(hào)對(duì)應(yīng)的系統(tǒng)調(diào)用表,定義在文件/kernel/arch/arm/kernel/calls.S,如下:
/* 35 */?CALL(sys_ni_syscall)?/* was sys_ftime */
CALL(sys_sync)
CALL(sys_kill)?//此處為37號(hào)
CALL(sys_rename)
CALL(sys_mkdir)
到這里可知37號(hào)系統(tǒng)調(diào)用對(duì)應(yīng)sys_kill(),該方法所對(duì)應(yīng)的函數(shù)聲明在syscalls.h文件
3.5:?文件/kernel/include/linux/syscalls.h中有如下聲明:
asmlinkage?long?sys_kill(int pid,?int sig);
asmlinkage是gcc標(biāo)簽,代表函數(shù)讀取的參數(shù)來自于棧中,而非寄存器。
回到頂部
3.1 SYSCALL_DEFINE
sys_kill()定義在內(nèi)核源碼找不到直接定義,而是通過syscalls.h文件中的SYSCALL_DEFINE宏定義。前面已經(jīng)講過sys_kill是通過語句SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)來定義,下面來一層層剖開,這條宏定義的真面目:
等價(jià) 1:
syscalls.h中有大量如下宏定義:
#define SYSCALL_DEFINE0(sname)
SYSCALL_METADATA(_##sname, 0);
asmlinkage?long sys_##sname(void)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
可得出原語句等價(jià):
SYSCALL_DEFINEx(2, _kill, pid_t, pid,?int, sig)
等價(jià) 2:
syscalls.h中有如下宏定義:
#define SYSCALL_DEFINEx(x, sname, ...)
SYSCALL_METADATA(sname, x, __VA_ARGS__)
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
可得出原語句等價(jià):
SYSCALL_METADATA(_kill,?2, pid_t, pid,?int, sig)
__SYSCALL_DEFINEx(2, _kill, pid_t, pid,?int, sig)
define __SYSCALL_DEFINEx(x, name, …)
等價(jià) 3:
syscalls.h中有如下宏定義:
#define __SYSCALL_DEFINEx(x, name, ...)
asmlinkage?long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
__attribute__((alias(__stringify(SyS##name))));
static?inline?long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));
asmlinkage?long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));
asmlinkage?long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))
{
long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));
__MAP(x,__SC_TEST,__VA_ARGS__);
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));
return ret;
}
static?inline?long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
可得出原語句等價(jià):
asmlinkage?long?sys_kill(__MAP(2,__SC_DECL,__VA_ARGS__))
__attribute__((alias(__stringify(SyS_kill))));
static inline?long?SYSC_kill(__MAP(2,__SC_DECL,__VA_ARGS__));
asmlinkage?long?SyS_kill(__MAP(2,__SC_LONG,__VA_ARGS__));
asmlinkage?long?SyS_kill(__MAP(2,__SC_LONG,__VA_ARGS__))
{
long ret = SYSC_kill(__MAP(2,__SC_CAST,__VA_ARGS__));
__MAP(2,__SC_TEST,__VA_ARGS__);
__PROTECT(2, ret,__MAP(2,__SC_ARGS,__VA_ARGS__));
return ret;
}
static inline?long?SYSC_kill(__MAP(2,__SC_DECL,__VA_ARGS__))
這里__VA_ARGS__等于?pid_t, pid, int, sig。
等價(jià) 4:
先說說這里涉及的宏定義
__MAP宏定義:
#define __MAP0(m,...)
#define __MAP1(m,t,a) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)
相關(guān)宏定義:
#define __SC_DECL(t, a) t a
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_CAST(t, a) (t) a
#define __SC_ARGS(t, a) a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
展開:
__MAP(2,__SC_DECL, pid_t, pid,?int, sig)?//等價(jià)于 pid_t pid, int sig
__MAP(2,__SC_LONG,__VA_ARGS__)?//等價(jià)于 long pid, long sig
__MAP(2,__SC_CAST,__VA_ARGS__)?//等價(jià)于 (pid_t) pid, (int)sig
__MAP(2,__SC_ARGS,__VA_ARGS__)?//等價(jià)于 pid, sig
可得出原語句等價(jià):
//函數(shù)聲明sys_kill(),并別名指向SyS_kill
asmlinkage?long sys_kill(pid_t pid,?int sig) __attribute__((alias(__stringify(SyS_kill))));
static?inline?long SYSC_kill(pid_t pid,?int sig);
//函數(shù)聲明SyS_kill()
asmlinkage?long SyS_kill(long pid,?long sig);
asmlinkage?long SyS_kill(long pid,?long sig)
{
long ret = SYSC_kill((pid_t) pid, (int)sig);
BUILD_BUG_ON_ZERO(sizeof(pid_t) >?sizeof(long));
BUILD_BUG_ON_ZERO(sizeof(int) >?sizeof(long));
__PROTECT(2, ret, pid, sig);
return ret;
}
static?inline?long SYSC_kill(pid_t pid,?int sig)
通過以上分析過程:
kill添加了sys_前綴,聲明sys_kill()函數(shù);
定義SYSC_kill()函數(shù)和SyS_kill()函數(shù);
sys_kill,通過別名機(jī)制等同于SyS_kill().
看到這或許很多人(包括我)會(huì)覺得詫異,為何要如此復(fù)雜呢,后來查資料,發(fā)現(xiàn)這是由于之前64位Linux存在CVE-2009-2009的漏洞,簡(jiǎn)單說就是32位參數(shù)存放在64位寄存器,修改符號(hào)擴(kuò)展可能導(dǎo)致產(chǎn)生一個(gè)非法內(nèi)存地址,從而導(dǎo)致系統(tǒng)崩潰或者提升權(quán)限。 為了修復(fù)這個(gè)問題,把寄存器高位清零即可,但做起來比較困難,為了做盡可能少的修改,將調(diào)用參數(shù)統(tǒng)一采用使用long型來接收,再強(qiáng)轉(zhuǎn)為相應(yīng)參數(shù)。?窺見一斑,可見Linux大師們精湛的宏定義,已經(jīng)用得出神入化。
如果覺得很復(fù)雜,那么可以忽略這個(gè)宏定義,只要記住SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)?基本等價(jià)于?asmlinkage long sys_kill(int pid, int sig)?就足夠了。
四、總結(jié)
回到頂部
4.1 內(nèi)核空間
系統(tǒng)調(diào)用的函數(shù)原型的指針:位于文件/kernel/arch/arm/kernel/calls.S,格式為CALL(sys_xxx),指定了目標(biāo)函數(shù)的入口地址。
系統(tǒng)調(diào)用號(hào)的宏定義:位于文件/kernel/arch/arm/include/Uapi/asm/unistd.h,記錄著內(nèi)核空間的系統(tǒng)調(diào)用號(hào),格式為#define__NR_xxx (__NR_SYSCALL_BASE+[num])
系統(tǒng)調(diào)用的函數(shù)聲明:位于文件/kernel/include/linux/syscalls.h,格式為asmlinkage long sys_xxx(args ...);
系統(tǒng)調(diào)用的函數(shù)實(shí)現(xiàn):不同函數(shù)位于不同文件,比如kill()位于/kernel/kernel/signal.c文件,格式為SYSCALL_DEFINEx(x, sname, ...)
前面這4步都是在內(nèi)核空間相關(guān)的文件定義,有了這些,那么內(nèi)核就可以使用相應(yīng)的系統(tǒng)調(diào)用號(hào)。
回到頂部
4.2 用戶空間
系統(tǒng)調(diào)用號(hào)的宏定義:位于文件/bionic/libc/kernel/uapi/asm-arm/asm/unistd.h,記錄著用戶空間的系統(tǒng)調(diào)用號(hào),格式為#define__NR_xxx (__NR_SYSCALL_BASE+[num])。這個(gè)文件就是由內(nèi)核空間同名的頭文件自動(dòng)生成的,所以該文件與內(nèi)核空間的系統(tǒng)調(diào)用號(hào)是完全一致。
匯編定義相關(guān)函數(shù)的中斷調(diào)用過程:位于文件/bionic/libc/arch-arm/syscalls/xxx.S,比如kill()位于kill.S,格式為:
ENTRY(xxx)
mov ip, r7
ldr r7, =__NR_xxx
swi?#0
mov r7, ip
cmn r0,?#(MAX_ERRNO + 1)
bxls lr
neg r0, r0
b __set_errno_internal
END(xxx)
當(dāng)然kill()方法還有函數(shù)聲明,有了這些,用戶空間也能在程序中使用系統(tǒng)調(diào)用。明白了這些過程,那么自己新添加系統(tǒng)調(diào)用其實(shí)也并不是多困難的一件事,新增系統(tǒng)調(diào)用號(hào)還需要修改syscalls總個(gè)數(shù),但強(qiáng)烈不建議自己新增系統(tǒng)調(diào)用號(hào),盡量保持與linux kernel主線一致,兼容性更好,所以就不進(jìn)一步介紹新增流程了。
?
評(píng)論