Linux系統調用(syscall)原理

引言:分析Android源碼的過程中,要想從上至下完全明白一行代碼,往往涉及app、framework、native一直到kernel,可能迷失到代碼世界,明白了系統調用原理,或許能幫你峯迴路轉,找到進入kernel函數的入口。本文主要講解ARM架構相關源碼:

/bionic/libc/kernel/uapi/asm-arm/asm/unistd.h
/bionic/libc/arch-arm/syscalls/kill.S

/kernel/arch/arm/kernel/calls.S
/kernel/arch/arm/include/Uapi/asm/unistd.h
/kernel/include/uapi/asm-generic/unistd.h
/kernel/include/linux/syscalls.h
/kernel/kernel/signal.c
/kernel/arch/arm/kernel/entry-common.S
/kernelarch/arm/kernel/entry-armv.S

一、Syscall意義

內核提供用戶空間程序與內核空間進行交互的一套標準接口,這些接口讓用戶態程序能受限訪問硬件設備,比如申請系統資源,操作設備讀寫,創建新進程等。用戶空間發生請求,內核空間負責執行,這些接口便是用戶空間和內核空間共同識別的橋樑,這裏提到兩個字“受限”,是由於爲了保證內核穩定性,而不能讓用戶空間程序隨意更改系統,必須是內核對外開放的且滿足權限的程序才能調用相應接口。

在用戶空間和內核空間之間,有一個叫做Syscall(系統調用, system call)的中間層,是連接用戶態和內核態的橋樑。這樣即提高了內核的安全型,也便於移植,只需實現同一套接口即可。Linux系統,用戶空間通過向內核空間發出Syscall,產生軟中斷,從而讓程序陷入內核態,執行相應的操作。對於每個系統調用都會有一個對應的系統調用號,比很多操作系統要少很多。

安全性與穩定性:內核駐留在受保護的地址空間,用戶空間程序無法直接執行內核代碼,也無法訪問內核數據,通過系統調用

性能:Linux上下文切換時間很短,以及系統調用處理過程非常精簡,內核優化得好,所以性能上往往比很多其他操作系統執行要好。

二、Syscall查找方式

這裏以文章理解殺進程的實現原理中的kill()方法爲例子,來找一找kill()方法系統調用的過程。

Tips 1: 用戶空間的方法xxx,對應系統調用層方法則是sys_xxx
Tips 2: unistd.h文件記錄着系統調用中斷號的信息。

故用戶空間kill方法則對應系統調用層便是sys_kill,這個方法去哪裏找呢?從/kernel/include/uapi/asm-generic/unistd.h等還有很多unistd.h去慢慢查看,查看關鍵字sys_kill,便能看到下面幾行:

/* kernel/signal.c */
__SYSCALL(__NR_kill, sys_kill)

根據這個能得到一絲線索,那就是kill對應的方法sys_kill位於/kernel/signal.c文件。

Tips 3: 宏定義SYSCALL_DEFINEx(xxx,…),展開後對應的方法則是sys_xxx
Tips 4: 方法參數的個數x,對應於SYSCALL_DEFINEx。

kill(int pid, int sig)方法共兩個參數,則對應方法於SYSCALL_DEFINE2(kill,...),進入signal.c文件,再次搜索關鍵字,便能看到方法:

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) 基本等價於 asmlinkage long sys_kill(int pid, int sig),這裏用的是基本等價,往下看會解釋原因。

實用技巧

比如kill命令, 有兩個參數. 則可以直接在kernel目錄下搜索 “SYSCALL_DEFINE2(kill”,即可直接找到,所有對應的Syscall方法位於signal.c

三、Syscall流程

Syscall是通過中斷方式實現的,ARM平臺上通過swi中斷來實現系統調用,實現從用戶態切換到內核態,發送軟中斷swi時,從中斷向量表中查看跳轉代碼,其中異常向量表定義在文件/kernelarch/arm/kernel/entry-armv.S(彙編語言文件)。當執行系統調用時會根據系統調用號從系統調用表中來查看目標函數的入口地址,在calls.S文件中聲明瞭入口地址信息。

總體流程:kill() -> kill.S -> swi陷入內核態 -> 從sys_call_table查看到sys_kill -> ret_fast_syscall -> 回到用戶態執行kill()下一行代碼。 下面介紹部分核心流程:

3.1: 用戶程序通過軟中斷swi指令切入內核態,執行vector_swi處的指令。vector_swi在文件/kenel/arch/arm/kernel/entry-common.S中定義,此處省略。像每一個異常處理程序一樣,要做的第一件事當然就是保護現場了。緊接着是獲得系統調用的系統調用號

3.2: 仍以kill()函數爲例,來詳細說說Syscall調用流程,用戶空間kill()定義位於文件kill.S

#include <private/bionic_asm.h>
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)

當調用kill時, 系統先保存r7內容, 然後將__NR_kill值放入r7, 再執行swi軟中斷指令切換進內核態。

3.3: Linux內核中,每個Syscall都有唯一的系統調用號對應,kill的系統調用號爲__NR_kill,用戶空間的系統調用號定義於/bionic/libc/kernel/uapi/asm-generic/unistd.h,如下:

#define __NR_kill (__NR_SYSCALL_BASE + 37)

其中__NR_SYSCALL_BASE=0,也就是__NR_kill系統調用號=37。

3.4: 在內核中有與系統調用號對應的系統調用表,定義在文件/kernel/arch/arm/kernel/calls.S,如下:

/* 35 */    CALL(sys_ni_syscall)        /* was sys_ftime */
            CALL(sys_sync)
            CALL(sys_kill)  //此處爲37號
            CALL(sys_rename)
            CALL(sys_mkdir)

到這裏可知37號系統調用對應sys_kill(),該方法所對應的函數聲明在syscalls.h文件

3.5: 文件/kernel/include/linux/syscalls.h中有如下聲明:

asmlinkage long sys_kill(int pid, int sig);

asmlinkage是gcc標籤,代表函數讀取的參數來自於棧中,而非寄存器。

3.1 SYSCALL_DEFINE

sys_kill()定義在內核源碼找不到直接定義,而是通過syscalls.h文件中的SYSCALL_DEFINE宏定義。前面已經講過sys_kill是通過語句SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)來定義,下面來一層層剖開,這條宏定義的真面目:

等價 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__)

可得出原語句等價:

SYSCALL_DEFINEx(2, _kill, pid_t, pid, int, sig)

等價 2:

syscalls.h中有如下宏定義:

#define SYSCALL_DEFINEx(x, sname, ...)                \
    SYSCALL_METADATA(sname, x, __VA_ARGS__)            \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

可得出原語句等價:

SYSCALL_METADATA(_kill, 2, pid_t, pid, int, sig)
__SYSCALL_DEFINEx(2, _kill, pid_t, pid, int, sig)

define __SYSCALL_DEFINEx(x, name, …)

等價 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__))

可得出原語句等價:

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

等價 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__)

相關宏定義:

#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) //等價於 pid_t pid, int sig
__MAP(2,__SC_LONG,__VA_ARGS__) //等價於 long pid, long sig
__MAP(2,__SC_CAST,__VA_ARGS__) //等價於 (pid_t) pid, (int)sig
__MAP(2,__SC_ARGS,__VA_ARGS__) //等價於 pid, sig

可得出原語句等價:

//函數聲明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);
//函數聲明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()函數;
  • 定義SYSC_kill()函數和SyS_kill()函數;
  • sys_kill,通過別名機制等同於SyS_kill().

看到這或許很多人(包括我)會覺得詫異,爲何要如此複雜呢,後來查資料,發現這是由於之前64位Linux存在CVE-2009-2009的漏洞,簡單說就是32位參數存放在64位寄存器,修改符號擴展可能導致產生一個非法內存地址,從而導致系統崩潰或者提升權限。 爲了修復這個問題,把寄存器高位清零即可,但做起來比較困難,爲了做儘可能少的修改,將調用參數統一採用使用long型來接收,再強轉爲相應參數。 窺見一斑,可見Linux大師們精湛的宏定義,已經用得出神入化。

如果覺得很複雜,那麼可以忽略這個宏定義,只要記住SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) 基本等價於 asmlinkage long sys_kill(int pid, int sig) 就足夠了。

四、總結

4.1 內核空間

  1. 系統調用的函數原型的指針:位於文件/kernel/arch/arm/kernel/calls.S,格式爲CALL(sys_xxx),指定了目標函數的入口地址。
  2. 系統調用號的宏定義:位於文件/kernel/arch/arm/include/Uapi/asm/unistd.h,記錄着內核空間的系統調用號,格式爲#define__NR_xxx (__NR_SYSCALL_BASE+[num])
  3. 系統調用的函數聲明:位於文件/kernel/include/linux/syscalls.h,格式爲asmlinkage long sys_xxx(args ...);
  4. 系統調用的函數實現:不同函數位於不同文件,比如kill()位於/kernel/kernel/signal.c文件,格式爲SYSCALL_DEFINEx(x, sname, ...)

前面這4步都是在內核空間相關的文件定義,有了這些,那麼內核就可以使用相應的系統調用號。

4.2 用戶空間

  1. 系統調用號的宏定義:位於文件/bionic/libc/kernel/uapi/asm-arm/asm/unistd.h,記錄着用戶空間的系統調用號,格式爲#define__NR_xxx (__NR_SYSCALL_BASE+[num])。這個文件就是由內核空間同名的頭文件自動生成的,所以該文件與內核空間的系統調用號是完全一致。

  2. 彙編定義相關函數的中斷調用過程:位於文件/bionic/libc/arch-arm/syscalls/xxx.S,比如kill()位於kill.S,格式爲:

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code class="hljs bash"> ENTRY(xxx)
     mov     ip, r7
     ldr     r7, =__NR_xxx
     swi     <span class="hljs-comment">#0</span>
     mov     r7, ip
     cmn     r0, <span class="hljs-comment">#(MAX_ERRNO + 1)</span>
     bxls    lr
     neg     r0, r0
     b       __<span class="hljs-built_in">set</span>_errno_internal
    

END(xxx)

當然kill()方法還有函數聲明,有了這些,用戶空間也能在程序中使用系統調用。明白了這些過程,那麼自己新添加系統調用其實也並不是多困難的一件事,新增系統調用號還需要修改syscalls總個數,但強烈不建議自己新增系統調用號,儘量保持與linux kernel主線一致,兼容性更好,所以就不進一步介紹新增流程了。

發佈了9 篇原創文章 · 獲贊 1 · 訪問量 1091
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章