1.系統調用函數接口是如何轉化為陷入命令
系統調用是通過一條陷入指令進入核心態,然後根據傳給核心的系統調用號為索引在系統調用表中找到相映的處理函數入口地址。這裡將詳細介紹這一過程。
我們以x86為例說明:
由於陷入指令是一條特殊指令,而且依賴與操作系統實現的平台,如在x86中,這條指令是int 0x80,這顯然不是用戶在編程時應該使用的語句,因為這將使得用戶程序難於移植。所以在操作系統的上層需要實現一個對應的系統調用庫,每個系統調用都在該庫中包含了一個入口點(如我們看到的fork, open, close等等),這些函數對程序員是可見的,而這些庫函數的工作是以對應系統調用號作為參數,執行陷入指令int 0x80,以陷入核心執行真正的系統調用處理函數。當一個進程調用一個特定的系統調用庫的入口點,正如同它調用任何函數一樣,對於庫函數也要創建一個棧幀。而當進程執行陷入指令時,它將處理機狀態轉換到核心態,並且在核心棧執行核心代碼。
這裡給出一個示例(Linux/include/asm/unistd.h):
#define _syscallN(type, name, type1, arg1, type2, arg2, . . . ) \
type name(type1 arg1,type2 arg2) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \
. . . . . .
__syscall_return(type,__res); \
}
在執行一個系統調用庫中定義的系統調用入口函數時,實際執行的是類似如上的一段代碼。這裡牽涉到一些gcc的嵌入式匯編語言,不做詳細的介紹,只簡單說明其意義:
其中__NR_##name是系統調用號,如name == ioctl,則為__NR_ioctl,它將被放在寄存器eax中作為參數傳遞給中斷0x80的處理函數。而系統調用的其它參數arg1, arg2, …則依次被放入ebx, ecx, . . .等通用寄存器中,並作為系統調用處理函數的參數,這些參數是怎樣傳入核心的將會在後面介紹。
下面將示例說明:
int func1()
{
int fd, retval;
fd = open(filename, ……);
……
ioctl(fd, cmd, arg);
. . .
}
func2()
{
int fd, retval;
fd = open(filename, ……);
……
__asm__ __volatile__(\
"int $0x80\n\t"\
:"=a"(retval)\
:"0"(__NR_ioctl),\
"b"(fd),\
"c"(cmd),\
"d"(arg));
}
這兩個函數在Linux/x86上運行的結果應該是一樣的。
若干個庫函數可以映射到同一個系統調用入口點。系統調用入口點對每個系統調用定義其真正的語法和語義,但庫函數通常提供一個更方便的接口。如系統調用exec有集中不同的調用方式:execl, execle,等,它們實際上只是同一系統調用的不同接口而已。對於這些調用,它們的庫函數對它們各自的參數加以處理,來實現各自的特點,但是最終都被映射到同一個核心入口點。
2. 系統調用陷入內核後作的參數傳遞過程
當進程執行系統調用時,先調用系統調用庫中定義某個函數,該函數通常被展開成前面提到的_syscallN的形式通過INT 0x80來陷入核心,其參數也將被通過寄存器傳往核心。
在這一部分,我們將介紹INT 0x80的處理函數system_call。
思考一下就會發現,在調用前和調用後執行態完全不相同:前者是在用戶棧上執行用戶態程序,後者在核心棧上執行核心態代碼。那麼,為了保證在核心內部執行完系統調用後能夠返回調用點繼續執行用戶代碼,必須在進入核心態時保存時往核心中壓入一個上下文層;在從核心返回時會彈出一個上下文層,這樣用戶進程就可以繼續運行。
那麼,這些上下文信息是怎樣被保存的,被保存的又是那些上下文信息呢?這裡仍以x86為例說明。
在執行INT指令時,實際完成了以下幾條操作:
(1) 由於INT指令發生了不同優先級之間的控制轉移,所以首先從TSS(任務狀態段)中獲取高優先級的核心堆棧信息(SS和ESP);
(2) 把低優先級堆棧信息(SS和ESP)保留到高優先級堆棧(即核心棧)中;
(3) 把EFLAGS,外層CS,EIP推入高優先級堆棧(核心棧)中。
(4) 通過IDT加載CS,EIP(控制轉移至中斷處理函數)
然後就進入了中斷0x80的處理函數system_call了,在該函數中首先使用了一個宏SAVE_ALL,該宏的定義如下所示:
#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__KERNEL_DS),%edx; \
movl %edx,%ds; \
movl %edx,%es;
該宏的功能一方面是將寄存器上下文壓入到核心棧中,對於系統調用,同時也是系統調用參數的傳入過程,因為在不同特權級之間控制轉換時,INT指令不同於CALL指令,它不會將外層堆棧的參數自動拷貝到內層堆棧中。所以在調用系統調用時,必須先象前面的例子裡提到的那樣,把參數指定到各個寄存器中,然後在陷入核心之後使用SAVE_ALL把這些保存在寄存器中的參數依次壓入核心棧,這樣核心才能使用用戶傳入的參數。
下面給出system_call的源代碼:
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb $0x20,flags(%ebx) # PF_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
. . . . . .
在這裡所做的所有工作是:
Ⅰ.保存EAX寄存器,因為在SAVE_ALL中保存的EAX寄存器會被調用的返回值所覆蓋;
Ⅱ.調用SAVE_ALL保存寄存器上下文;
Ⅲ.判斷當前調用是否是合法系統調用(EAX是系統調用號,它應該小於NR_syscalls);
Ⅳ.如果設置了PF_TRACESYS標志,則跳轉到syscall_trace,在那裡將會把當前程掛起並向其父進程發送SIGTRAP,這主要是為了設置調試斷點而設計的;
Ⅴ.如果沒有設置PF_TRACESYS標志,則跳轉到該系統調用的處理函數入口。這裡是以EAX(即前面提到的系統調用號)作為偏移,在系統調用表sys_call_table中查找處理函數入口地址,並跳轉到該入口地址。
(補充說明:
1.GET_CURRENT宏
#define GET_CURRENT(reg) \
movl %esp, reg; \
andl $-8192, reg;
其作用是取得當前進程的task_strUCt結構的指針返回到reg中,因為在Linux中核心棧的位置是task_struct之後的兩個頁面處(8192bytes),所以此處把棧指針與-8192則得到的是task_struct結構指針,而task_struct中偏移為4的位置是成員flags,在這裡指令testb $0x20,flags(%ebx)檢測的就是task_struct->flags。)
INT 0x80 SS
pt_regs
ESP
SALL_ALL 用戶棧
CALL syscall_entry
核心棧
堆棧中的參數分析:
正如前面提到的,SAVE_ALL是系統調用參數的傳入過程,當執行完SAVE_ALL並且再由CALL指令調用其處理函數時,堆棧的結構應該如上圖所示。這時的堆棧結構看起來和執行一個普通帶參數的函數調用是一樣的,參數在堆棧中對應的順序是(arg1, ebx),(arg2, ecx),(arg3, edx). . . . . .,這正是SAVE_ALL壓棧的反順序,這些參數正是用戶在使用系統調用時試圖傳送給核心的參數。下面是在核心的調用處理函數中使用參數的兩種典型方法:
asmlinkage int sys_fork(struct pt_regs regs);
asmlinkage int sys_open(const char * filename, int flags, int mode);
在sys_fork中,把整個堆棧中的內容視為一個struct pt_regs類型的參數,該參數的結構和堆棧的結構是一致的,所以可以使用堆棧中的全部信息。而在sys_open中參數filename, flags, mode正好對應與堆棧中的ebx, ecx, edx的位置,而這些寄存器正是用戶在通過C庫調用系統調用時給這些參數指定的寄存器。
__asm__ __volatile__(\
"int $0x80\n\t"\
:"=a"(retval)\
:"0"(__NR_open),\
"b"(filename),\
"c"(flags),\
"d"(mode));
核心如何使用用戶空間的參數:
在使用系統調用時,有些參數是指針,這些指針所指向的是用戶空間DS寄存器的段選擇子所描述段中的地址,而在2.2之前的版本中,核心態的DS段寄存器的中的段選擇子和用戶態的段選擇子描述的段地址不同(前者為0xC0000000, 後者為0x00000000),這樣在使用這些參數時就不能讀取到正確的位置。所以需要通過特殊的核心函數(如:memcpy_fromfs, mencpy_tofs)來從用戶空間數據段讀取參數,在這些函數中,是使用FS寄存器來作為讀取參數的段寄存器的,FS寄存器在系統調用進入核心態時被設成了USER_DS(DS被設成了KERNEL_DS)。在2.2之後的版本用戶態和核心態使用的DS中段選擇子描述的段地址是一樣的(都是0x00000000),所以不需要再經過上面那樣煩瑣的過程而直接使用參數了。
內存映射分析:
Linux將4G的地址劃分為用戶空間和內核空間兩部分。在Linux內核的低版本中(2。0。X),通常0-3G為用戶空間,3G-4G為內核空間。這個分界點是可以可以改動
的。
正是這個分界點的存在,限制了Linux可用
內存映射分析:
Linux將4G的地址劃分為用戶空間和內核空間兩部分。在Linux內核的低版本中(2。0。X),通常0-3G為用戶空間,3G-4G為內核空間。這個分界點是可以可以改動
的。
正是這個分界點的存在,限制了Linux可用