系統調用與軟中斷關系
1.EABI與OABI方式的系統調用
在linux中系統調用是通過軟中斷實作,應用層通過int syscall(int number, ...);接口将syscall number 放在提前約定好的位置,然後産生軟中斷swi,并跳轉到中斷向量表執行。沒有接觸過的人可能會有疑問:kernel是和什麼“提前約定”的。答案是編譯器,應用層的系統調用具體實作是在編譯器的連結庫裡,打開你的編譯器所在位址,你會發現編譯器的工具包裡有lib/ 和 include/檔案夾,這裡存放的就是kernel正常工作需要的連結庫,也就是說在應用層編寫的代碼發起一個open的syscall,它到底向kernel發出了什麼樣的指令是由編譯器決定的。由于這一塊我并沒有具體了解過,這裡隻做一個不精确的描述,不再具體展開。
早期linux采用OABI方式傳遞系統調用的number。流程如下:
1.将number 放在軟終斷的中斷号内( eg: swi __NR_OABI_SYSCALL_BASE | numbe)。
2.在跳轉到軟中斷後,再在之前已經執行過的指令所在記憶體位址中擷取當時的指令(ldr r10, [lr, #-4]),并解析出syscall number。
很明顯讀取已經執行過的指令的記憶體位址處的資料是一種很原始的行為,現在用的不多,但代碼依然保留了下來。
現在linux采用新的EABI(Embedded)方式傳遞系統調用的number。比如現在的android編譯器arm-linux-androideabi-gcc。這種新的系統調用方式流程如下:
1.将syscall number 存儲到r7寄存器中。
2. 執行swi指令跳轉到軟中斷處執行,并從r7中擷取syscall number。
注意這裡提到的軟中斷是 supervisor call exception(svc),由于原來名字是software interrupt(swi)一直沿用軟中斷的叫法,但與linux kernel的softirq實作是完全不同的兩個概念。前者是借助硬體實作的一種,使軟體可以進入特權模式的指令,後者是kernel完全以軟體實作的短暫延時的輕量級處理(出于軟中斷上下文)
2.軟中斷到syscall代碼分析
由于需要涉及彙編代碼,做一下說明,本文隻分析arm架構下的系統調用流程,下面是arm架構的準備知識:
1.應用運作于arm的usr_mode,kernel運作在arm的svc_mode,這兩種模式共用r0-r12寄存器以及PC(r15),usr_mode下沒有SPSR(r17)寄存器,svc_mode 下有自己的sp(r13),lr(r14),spsr(r17)。
2.從usr_mode發生軟中斷(swi)跳轉到svc_mode時,原來的cpsr會被arm core自動備份到spsr_svc中,以供從svc傳回時恢複usr完整的上下文(r0-r12+sp+lr+pc+cpsr)。
3.arm架構下linux使用的是滿減棧,棧頂指針sp永遠指向棧頂的元素,棧頂向低位址方向生長。
4.無論采取多少級的流水,arm架構下前三級是fetch,decode,execute。PC永遠指向fetch位址。
5.lr位址 "可以認為" 是指向跳轉前的decode位址。
整個中斷的過程,不是本文關注重點,我們隻從進入軟中斷後開始分析。涉及檔案:
arch/arm/kernel/entry-armv.S //中斷向量表定義位置,本文不分析
arch/arm/kernel/entry-common.S //軟中斷真正入口位址
arch/arm/kernel/entry-header.S //部分定義
entry-header.S:
scno .req r7 @ syscall number
tbl .req r8 @ syscall table pointer
why .req r8 @ Linux syscall (!= 0)
tsk .req r9 @ current thread_info 程序棧的棧底,存儲着該程序的基本資訊如id 排程優先級 等等
entry-common.S:
ENTRY(vector_swi)
/****************************将中斷發生前的寄存器狀态備份入棧**********/
sub sp, sp, #S_FRAME_SIZE /*在棧上開辟18*4的空間備份跳轉之前的寄存器值*/
stmia sp, {r0 - r12} /* Calling r0 - r12 備份公用寄存器r0-r12*/
ARM( add r8, sp, #S_PC ) /*r8指向棧空間中應該存pc的位置*/
ARM( stmdb r8, {sp, lr}^ )/* Calling sp, lr*/
/*備份usr_mode的r13 r14 這裡^表示被備份的sp lr 是usr_mode的寄存器不是目前所在svc_mode的寄存器*/
mrs r8, spsr // called from non-FIQ mode, so ok.從usr發生軟中斷跳轉到svc時,原來的cpsr會被存儲到spsr_svc中
str lr, [sp, #S_PC] // Save calling PC 備份pc
str r8, [sp, #S_PSR] // Save CPSR 備份跳轉前的cpsr到棧上
str r0, [sp, #S_OLD_R0] // Save OLD_R0 備份r0到棧空間中r17的位置(因為沒有SPSR 是以這裡多備份了一個r0,估計是為了标準化)
zero_fp //清空fp寄存器r11
/*********************取得syscall number**************************/
/*
* Get the system call number.
*/
#if defined(CONFIG_OABI_COMPAT)
/*這裡是OABI方式的syscall number傳遞
* If we have CONFIG_OABI_COMPAT then we need to look at the swi
* value to determine if it is an EABI or an old ABI call.
*/
ldr r10, [lr, #-4] // get SWI instruction 利用lr取得swi指令的内容
//跳轉前code段的記憶體{位址:内容:流水級},從下面的記憶體分布可以清楚看出lr - 4的含義
//{0x8:xxx:fetch<--pc},{0x4:xxx:decode<--lr},{0x0:swi num:exe}
#ifdef CONFIG_CPU_ENDIAN_BE8
rev r10, r10 // little endian instruction 考慮到CPU的大小端的設計。
#endif
#elif defined(CONFIG_AEABI)
/*EABI方式
* Pure EABI user space always put syscall number into scno (r7).注意 scno 與 r7等價
*/
#elif defined(CONFIG_ARM_THUMB)
/* Legacy ABI only, possibly thumb mode. */
tst r8, #PSR_T_BIT @ this is SPSR from save_user_regs
addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in
ldreq scno, [lr, #-4]
#else
/* Legacy ABI only. */
ldr scno, [lr, #-4] @ get SWI instruction
#endif
/************為進入syscall具體函數做準備,主要是使調用符合ATPCS規範***********/
#ifdef CONFIG_ALIGNMENT_TRAP
ldr ip, __cr_alignment
ldr ip, [ip] //以上兩句将ip(r12)清0
mcr p15, 0, ip, c1, c0 // update control register 協處理器控制指令
#endif
enable_irq //放開中斷屏蔽
/*
.macro get_thread_info, rd
mov \rd, sp, lsr #13 邏輯右移
mov \rd, \rd, lsl #13 邏輯左移
棧頂指針低13位清0,顯然高位是base位址,低13位看來是整個程序棧的偏移位址,由此可以得到棧的base位址
.endm
*/
get_thread_info tsk //tsk(r9)存儲了棧底的位址
adr tbl, sys_call_table // load syscall table pointer 将sys_call_table位址加載到tbl(r8)
#if defined(CONFIG_OABI_COMPAT)
/*
* If the swi argument is zero, this is an EABI call and we do nothing.
*
* If this is an old ABI call, get the syscall number into scno and
* get the old ABI syscall table address.
*/
bics r10, r10, #0xff000000
eorne scno, r10, #__NR_OABI_SYSCALL_BASE
//從r10 擷取中斷号,r10存儲的本來是swi指令的完整指令碼,可以從上面代碼看到
ldrne tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
bic scno, scno, #0xff000000 @ mask off SWI op-code
eor scno, scno, #__NR_SYSCALL_BASE @ check OS number
#endif
ldr r10, [tsk, #TI_FLAGS] @ check for syscall tracing
stmdb sp!, {r4, r5} // push fifth and sixth args
//将參數r4 r5入棧,因為系統調用最多有6個參數
//但APCS規定隻有r0-r3 4個參數可以用寄存器傳遞,是以r4 r5 兩個參數必然要通過棧來傳遞
#ifdef CONFIG_SECCOMP
tst r10, #_TIF_SECCOMP
beq 1f
mov r0, scno
bl __secure_computing
add r0, sp, #S_R0 + S_OFF @ pointer to regs
ldmia r0, {r0 - r3} @ have to reload r0 - r3
1:
#endif
tst r10, #_TIF_SYSCALL_WORK // are we tracing syscalls? 我們隻考慮無tracing情況
bne __sys_trace //隻考慮無tracing 這裡不跳轉
cmp scno, #NR_syscalls // check upper syscall limit NR_syscalls是系統調用總個數
adr lr, BSYM(ret_fast_syscall) // return address 将傳回用的位址預填到lr寄存器
ldrcc pc, [tbl, scno, lsl #2] //call sys_* routine
//如果scno小于NR_syscalls則執行該指令,将PC移動到tbl中對應的syscall位置(tbl+scno*4)
add r1, sp, #S_OFF //後面都是scno過大的出錯處理
2: mov why, #0 @ no longer a real syscall
cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
eor r0, scno, #__NR_SYSCALL_BASE @ put OS number back
bcs arm_syscall
b sys_ni_syscall @ not private func
ENDPROC(vector_swi)
上面注釋中已經詳細分析了代碼,這裡做一個總結,進入vector_swi後的執行流程:
(1)在棧上備份中斷前的寄存器。
(2)擷取syscall number(r7)。
(3)進入具體syscall函數的準備工作
r8-->tbl,r9-->tsk,lr-->新的傳回位址(ret_fast_syscall)
r0-r5 6個C函數的參數放在對應位置
(4)通過tbl記錄的table入口和r7記錄的偏移量,進入相應的系統調用。
(5)傳回到ret_fast_syscall,具體來說原理都一樣,不再分析了
calls.S在entry-common.S中的兩次包含及其具體意義
檔案:
linux-2.6.30.4/arch/arm/kernel/entry-common.S 程式軟中斷的入口
linux-2.6.30.4/arch/arm/kernel/calls.S 存儲了所有sys_func 函數的入口位址。
/* 0 */ CALL(sys_restart_syscall)
CALL(sys_exit)
CALL(sys_fork)
CALL(sys_read)
CALL(sys_write)
/* 5 */ CALL(sys_open)
CALL(sys_close)
CALL(sys_ni_syscall) /* was sys_waitpid */
CALL(sys_creat)
CALL(sys_link)
......
/* 380 */ CALL(sys_sched_setattr)
CALL(sys_sched_getattr)
#ifndef syscalls_counted
.equ syscalls_padding, ((NR_syscalls + 3) & ~3) - NR_syscalls
#define syscalls_counted
#endif
.rept syscalls_padding
CALL(sys_ni_syscall)
.endr
在entry-common.S中包含了上次calls.S,這裡簡單分析前兩次:
1.NR_syscalls記錄CALL的個數:
在entry-common.S中:
.equ NR_syscalls,0
#define CALL(x) .equ NR_syscalls,NR_syscalls+1
#include "calls.S"
#undef CALL
#define CALL(x) .long x
在calls.S的結尾:
CALL(sys_preadv)
CALL(sys_pwritev)
#ifndef syscalls_counted
.equ syscalls_padding, ((NR_syscalls + 3) & ~3) - NR_syscalls
#define syscalls_counted
#endif
.rept syscalls_padding
CALL(sys_ni_syscall)
.endr
.equ NR_syscalls,0 中涉及到彙編指令.equ:
.equ/.set: 指派語句, 格式如下:
.equ(.set) 變量名,表達式
例如:
.equ abc 3 @讓abc=3
這裡隻是定義了一個變量NR_syscalls,并将其初始化為0。可以了解為: NR_syscalls = 0
#define CALL(x) .equ NR_syscalls,NR_syscalls+1
即 将CALL(x) 定義為: NR_syscalls = NR_syscalls + 1
#include "calls.S" 将calls.S的内容包進來,因為上面對CALL(x)進行了定義是以相當于執行了多次NR_syscalls++,相當于統計了系統調用的個數,但是注意:在calls.S的結尾的對齊處理:
#ifndef syscalls_counted
.equ syscalls_padding, ((NR_syscalls + 3) & ~3) - NR_syscalls
#define syscalls_counted
#endif
.rept syscalls_padding
CALL(sys_ni_syscall)
.endr
由于是第一次包含,故syscalls_counted沒有定義,
.equ syscalls_padding, ((NR_syscalls + 3) & ~3) - NR_syscalls
為了保證NR_syscalls是4的整數倍,上面的語句相當于:syscalls_padding = ((NR_syscalls + 3) & ~3) - NR_syscalls;
即:假如NR_syscalls 是1,那麼syscalls_padding 就是3
.rept syscalls_padding
CALL(sys_ni_syscall)
.endr
這裡涉及到彙編指令.rept的用法:
.rept:重複定義僞操作, 格式如下:
.rept 重複次數
資料定義
.endr @結束重複定義
例如:
.rept 3
.byte 0x23
.endr
繼續上面的例子:syscalls_padding 為3,那麼上面的rept語句塊相當于:
CALL(sys_ni_syscall)
CALL(sys_ni_syscall)
CALL(sys_ni_syscall)
即又執行了三次:NR_syscalls++,此時NR_syscalls就變成了4,對齊了。
2. syscall()函數實作:
.type sys_call_table, #object
ENTRY(sys_call_table)
#include "calls.S"
#undef ABI
#undef OBSOLETE
@r0 = syscall number
@r8 = syscall table
sys_syscall:
bic scno, r0, #__NR_OABI_SYSCALL_BASE
cmp scno, #__NR_syscall - __NR_SYSCALL_BASE //比較scno 是否是syscall()的系統調用号
cmpne scno, #NR_syscalls // check range; cmp指令與lo條件保證 系統調用号不會超出範圍
stmloia sp, {r5, r6} // shuffle args 因為int syscall(int number, ...);作為最特殊的一個syscall支援可變的7個參數r0-r6
movlo r0, r1 //很容易了解因為syscall()需要支援最多6個參數的syscall,并且還要在r0參數中包含應該調用的syscall number
movlo r1, r2 //是以,r1-r6中存儲了規定的六個參數,在進如使用者所真正期望的系統調用之前,需要重新按照ATPCS規定,排列相應的參數清單
movlo r2, r3 //也就造成了r5 r6 入棧,r1-r4順次前移。
movlo r3, r4
ldrlo pc, [tbl, scno, lsl #2]
b sys_ni_syscall
ENDPROC(sys_syscall)
第二次包含是建立在第一次包含的基礎上,第一次包含的結果是:
- #undef CALL
- #define CALL(x) .long x
- NR_syscalls 是系統調用的個數,并且進行了4的倍數對齊(最後幾個系統調用可能隻是為了湊數)
-
syscalls_padding的數值保證了CALL(x)的個數可以4倍對齊,即.long x 的個數是4的倍數。目的是在下面的sys_syscall函數中的:
ldrlo pc, [tbl, scno, lsl #2]
即将“系統調用表基位址tbl+系統調用号scno*4”位址單元(上面的某個.long x)的資料(也就是某個系統調用處理函數的入口位址)放到PC寄存器中(因為有了對齊,是以不會産生通路越界的情況,又因為
bic scno, r0, #__NR_OABI_SYSCALL_BASE
@這一位清除指令 清除了SYSCALL_BASE的高位,保證存儲在scno中的系統調用号是一個基于__NR_OABI_SYSCALL_BASE的偏移量,隻表示是第幾個系統調用
(注意scno中隻是第幾個的意思,不是系統調用的實際位址或者相對位址,這一點後文會再次利用到)
cmp scno, #__NR_syscall - __NR_SYSCALL_BASE
@CMP指令實際是影響CPSR的減法指令。
@這條指令有兩種情況 1,傳遞下來的scno是syscall()的系統調用号,那麼 CPSR的 C位為1,Z位為1 這樣帶ne與lo的指令都不執行函數結束
@2 傳遞下來的scno是其他系統調用的系統調用号,那麼 CPSR的 C位為1,Z位為0 進入下一句cmpne scno, #NR_syscalls
cmpne scno, #NR_syscalls @ check range
@這一句在已知scno為一個系統調用的相對偏移量的基礎上 scno <#NR_syscalls。
@這一相減指令的結果一定是<0 則,CPSR的C=0 Z=0,下面的lo指令全部生效
以上兩條語句保證了系統調用号scno的大小不會超出範圍。超出範圍則跳過所有指令直接執行 b sys_ni_syscall 這一句無條件指令 然後退出。
可以看到,在calls.S結尾的系統調用:sys_ni_syscall。它除了傳回-ENOSYS之外啥也沒幹:
/*
* Non-implemented system calls get redirected here.
*/
asmlinkage long sys_ni_syscall(void)
{
return -ENOSYS;
}
stmloia sp, {r5, r6} @ shuffle args
movlo r0, r1
movlo r1, r2
movlo r2, r3
movlo r3, r4
ldrlo pc, [tbl, scno, lsl #2]
這幾句指令是調整系統調用參數的位置,友善下一步調用。 因為系統調用最多6個參數,後文會提到原因。
我們分析一下這段語句運作前的寄存器内容 r0(系統調用号) r1(參數1) r2(參數2) r3(參數3) r4(參數4) r5(參數5) r6(參數6)
(其實從代碼可以看出syscall(int number,...) 是linux下唯一一個有7個參數的函數,r0是實際調用的其他系統調用号,r1-r6是其他系統調用的6個參數)
(對于一般的系統調用如果是直接調用參數存儲在r0-r5共6個)
因為在C和ARM彙程式設計式間互相調用須遵守ATPCS規則。函數的前四個參數用r0-r3傳遞,之後的參數5 6 用棧傳遞,是以前5句指令的作用正如注釋所說,是調整參數存儲位置友善C函數調用。
ldrlo pc, [tbl, scno, lsl #2] @跳轉到真正的系統調用的C或者彙編函數入口
@該指令将scno邏輯左移2位加到tbl上,把結果位置處所存儲的值指派給PC。 PC = *(tbl+(scno<<2))。
@tbl 是系統調用table的入口位址,可以把tbl看作char數組頭。char tbl[] = {.......};
@因為scno隻是第幾個系統調用的意思,不是系統調用的實際位址或者相對位址。
@而實際每一個系統調用的位址(也就是形象了解的函數名),是 .long x 32位,需要四個位元組存儲。
@scno邏輯左移2位,意思為在tbl上偏移scno<<2個位元組的位置上,存儲着所需的系統調用函數的入口位址。tbl[scno<<2] 存儲着所需的系統調用函數的入口位址。
3.syscall調用過程中各個寄存器都幹了什麼
note:這裡就解釋清楚了另一個小問題,為什麼kernel将r7定義為syscall number的存儲寄存器?為什麼系統調用最多隻有6個參數?為什麼syscall()函數可以有7個參數?
(當然syscall:是彙編中的lable 定義上來看不具備函數的完整特點,不過我們暫且把它說成是函數也沒有問題。)
svc模式共有r0-r16 17個寄存器,其中arm架構制定有特殊作用的 r16-->cpsr r15-->pc r14-->lr
ATPCS(ARM-THUMB procedure call standard)規定使用:r13-->sp r12-->ip r11-->fp r10-->SL
這些在編譯标準C代碼的時候,顯然是編譯器會不通知你直接使用的,我們當然不能用他們固定的存儲資料。
是以留給我們可以自由使用的寄存器就是r0-r9,我們再來看kernel在系統調用階段是如何使用這些寄存器的吧
r9-->tsk (存棧底指針,這是程序棧的起始位置,記錄了程序的所有資訊,非常重要,很多地方會用到)
r8-->tbl (存儲系統調用table入口位址)
r7-->scno (系統調用号)
r0-r5 (syscall的六個參數)
r6-->在syscall:函數實作的注釋中我們已經提到了,是存儲syscall()這個系統調用的第七個參數,因為syscall()函數需要能夠正确調用所有的系統調用,是以他必須比其他系統調用多用一個寄存器。
syscall 号定義位置
所有系統調用的編号定義在arch/arm/include/asm/unistd.h中:
#define __NR_OABI_SYSCALL_BASE 0x900000
#if defined(__thumb__) || defined(__ARM_EABI__)
#define __NR_SYSCALL_BASE 0
#else
#define __NR_SYSCALL_BASE __NR_OABI_SYSCALL_BASE
#endif
/*
* This file contains the system call numbers.
*/
#define __NR_restart_syscall (__NR_SYSCALL_BASE+ 0)
#define __NR_exit (__NR_SYSCALL_BASE+ 1)
#define __NR_fork (__NR_SYSCALL_BASE+ 2)
#define __NR_read (__NR_SYSCALL_BASE+ 3)
#define __NR_write (__NR_SYSCALL_BASE+ 4)
#define __NR_open (__NR_SYSCALL_BASE+ 5)
.................
#define __NR_pipe2 (__NR_SYSCALL_BASE+359)
#define __NR_inotify_init1 (__NR_SYSCALL_BASE+360)
#define __NR_preadv (__NR_SYSCALL_BASE+361)
#define __NR_pwritev (__NR_SYSCALL_BASE+362)
#define __NR_rt_tgsigqueueinfo (__NR_SYSCALL_BASE+363)
#define __NR_perf_event_open (__NR_SYSCALL_BASE+364)
#define __NR_recvmmsg (__NR_SYSCALL_BASE+365)
syscall的定義宏
在 include/linux/syscalls.h 中
#define SYSCALL_DEFINE0(name) asmlinkage long sys_##name(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__)
#define SYSCALL_DEFINEx(x, sname, ...) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))
在無strace的模式下實作很簡單,隻是将每個函數定義前面加上sys_字首。
使用方法
SYSCALL_DEFINE0(fork) //asmlinkage long sys_fork(void)
{
struct pt_regs *regs = task_pt_regs(current);
return do_fork(SIGCHLD, regs->gprs[15], regs, 0, NULL, NULL);
}
SYSCALL_DEFINE2(dup2, unsigned int, oldfd, unsigned int, newfd) // asmlinkage long sys_dup2(unsigned int old fd,unsigned int newfd)
{
...
}
添加自己的系統調用
開始之前必須說明一下,實際上,是沒有任何必要也不應該自己添加一個系統調用,這隻是便于了解系統調用的一種方式
1)核心中增加一個新的函數
vi arch/arm/kernel/sys_arm.c
asmlinkage int sys_add(int x, int y)
{
printk("enter %s\n", __func__);
return x+y;
}
注意:該函數必須是全局的,核心程式設計不能進行浮點數運算 float double
2)更新unistd.h
vi arch/arm/include/asm/unistd.h
#define __NR_add (__NR_SYSCALL_BASE+366)
注意:隻能添加在所有系統調用号的最後面
3)更新系統調用表
vi arch/arm/kernel/calls.S
CALL(sys_add)
注意:隻能添加在所有CALL的最後面,并且與(2)的調用号相對應。否則一定使系統調用表混亂。
4)重新編譯核心,加載新的核心
5)編寫一個測試程式
#include <unistd.h>
#include <sys/syscall.h>
int main (void)
{
syscall(366, x, y);
return 0;
}