使用者態和核心態
intel x86 CPU有4種不同的執行級別,分別為0,1,2,3
按照intel的設想,核心執行在Ring0級別,驅動執行在Ring1和Ring2級別,應用執行在Ring3級別
linux系統中,只使用了0和3兩個級別,分別對應核心態和使用者態,使用暫存器CS:EIP
的指向範圍區分
- 使用者態下,只能訪問
0x00000000
~0xBFFFFFFF
的地址空間 - 核心態下,可以指向任意地址,
0xC0000000
只能在核心態下訪問
在32位x86機器上有4GB程序地址空間,MMU負責邏輯地址和實體地址的轉換
| |核心空間1GB
| |<- 0xc0000000
| |
| |使用者空間3GB
| |<- 0x00000000
中斷
int
指令觸發中斷機制(包括系統呼叫)
中斷觸發時,會在堆疊上儲存暫存器的值,儲存使用者態棧頂地址、當時的狀態字和CS:EIP
值
同時將核心態的棧頂地址、核心態的狀態字載入CPU暫存器中,並將ES:EIP
暫存器指向中斷處理程式入口(對系統呼叫來說是system_call
)
int
指令觸發後,進入中斷處理程式,開始執行核心程式碼SAVE_ALL
儲存現場
中斷處理程式結束後,執行恢復現場操作,在3.18.6的x86-32核心中,restore_all
和INTERRUPT_RETURN
(iret
)負責將中斷時儲存的使用者態暫存器值恢復到當前CPU
系統呼叫
作業系統管理硬體,提高系統安全性,使得使用者程式具有可移植性
-
Linux下系統呼叫透過觸發
int 0x80
中斷完成
中斷儲存了使用者態CS:EIP
的值,及當前堆疊段暫存器的棧頂,將EFLAGS
暫存器的值儲存到核心堆疊中
同時將當前的中斷訊號或系統呼叫和終端服務例程的入口載入在CS:EIP
中,將當前的堆疊段SS:ESP
也載入到CPU中 -
觸發系統呼叫及引數傳遞方式
當Linux透過執行int 0x80
觸發系統呼叫時(Intel Pentium II還引入sysenter
指令,Linux2.6後支援),進入核心,開始執行中斷向量128
對應的中斷服務例程system_call
使用者態程序需要指明呼叫哪個系統呼叫,透過EAX
暫存器傳遞系統呼叫號引數來區分
除了系統呼叫號外,系統呼叫可能需要傳遞其他引數,由於系統呼叫從使用者態切換到核心態,使用不同的堆疊空間,無法透過壓棧的方式傳遞引數,而是透過暫存器傳遞引數,引數個數若超過暫存器數量,可將某個暫存器作為指標指向記憶體,此時可以透過記憶體傳遞更多引數
使用庫函式libc和C嵌入彙編程式碼觸發同一個系統呼叫
使用庫函式libc
#include<stdio.c>
#include<time.c>
int main(){
time_t tt; // int
struct tm* t;
tt= time(NULL);
t= localtime(&tt);
printf("time:%d:%d:%d:%d:%d:%d\n",
t->tm_year+1900, t->tm_mon, t->tm_mda,
t->tm_hour, t->tm_min, t->tm_sec);
return 0;
}
編譯gcc -m32 time.c -o time
使用C嵌入彙編程式碼
#include<stdio.h>
#include<time.h>
int main(){
time_t tt;
struct tm* t;
// 使用匯編代替 time(NULL)
asm volatile(
"mov $0, %%ebx\n\t"
"mov $0xd, %%eax\n\t" // 傳遞系統呼叫號 13
"int $0x80\n\t" // 觸發系統呼叫
"mov %%eax, %0\n\t"
: "=m" (tt)
);
t= localtime(&tt);
printf("time:%d:%d:%d:%d:%d:%d\n",
t->tm_year+1900, t->tm_mon, t->tm_mda,
t->tm_hour, t->tm_min, t->tm_sec);
return 0;
}
編譯gcc time-asm.c -o time-asm -m32
含有兩個引數的系統呼叫示例
重新命名函式rename()
,在核心中對應的系統呼叫核心處理函式sys_rename()
,系統呼叫號為38
asmlinkage long sys_rename(const char __user* oldname, const char __user *newname);
使用庫函式libc
#include<stdio.h>
int main(){
int ret;
char* oldname= "hello.c";
char* newname= "newhello.c";
ret= rename(oldname, newname);
if(ret==0){
printf("renamed successfully\n");
}else{
printf("unable to rename the file\n");
}
return 0;
}
使用C嵌入彙編程式碼
#include<stdio.h>
int main(){
int ret;
char* oldname= "hello.c";
char* newname= "newhello.c";
asm volatile(
"movl %2, %%ecx\n\t"
"movl %1, %%ebx\n\t"
"movl $0x26, %%eax\n\t"
"int $0x80"
: "=a" (ret)
: "b" (oldname), "c" (newname)
);
if(ret==0){
printf("renamed successfully\n");
}else{
printf("unable to rename the file\n");
}
return 0;
}
EAX用來傳遞系統呼叫號,其他引數按順序賦給EBX,ECX,EDX,ESI,EDI,EBP
將系統呼叫號38存入EAX暫存器,將oldname存入EBX暫存器,將newname存入ECX暫存器,由於引數是字串,實際傳遞的是指標變數
透過執行int $0x80
指令來執行系統呼叫,進入核心態,system_call()
根據系統呼叫號在系統呼叫列表中查詢對應的系統呼叫核心函式sys_rename()
,執行完將結果存入EAX暫存器,再將EAX暫存器的值傳給ret
使用gdb跟蹤MenuOS系統呼叫過程
將time
和time-asm
命令整合到MenuOS中
rm -rf menu
git clone https://github.com/mengning/menu.git
make rootfs
透過在test.c:main()
中增加兩行程式碼,來給MenuOS增加兩個命令
MenuConfig("time");
MenuConfig("time-asm");
使用gdb跟蹤系統呼叫核心函式
除錯time命令所用到的系統呼叫核心處理函式
cd ..
qemu-system-i386 -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -S -s
啟動核心後,先啟動gdb,再載入核心
(gdb) file vmlinux # cd linux-3.18.6
(gdb) target remote:1234
time()
系統呼叫是系統呼叫號13
對應的核心處理函式,即sys_time
(gdb) b sys_time
(gdb) c
此時,在已經啟動的 MenuOS 中執行time-asm
命令,程式會停在sys_time
處
sys_time
位於kernel/time/time.c,使用宏實現,所以無法直接看到sys_time
單步執行,會進入get_seconds()
,位於kernel/time/timekeeping.c
使用gdb的finish
命令將該函式執行完,再單步執行,直到return i
,即獲取到系統時間
若繼續單步除錯,會出現cannot find bounds of current function
這裡的程式碼比較特殊,不好除錯,因為這時會返回到system_call位置的彙編程式碼,完成恢復現場並返回到使用者態
當執行int 0x80
時,實際上會跳轉到system_call()
,可以直接將斷點設在該處
該函式是彙編程式碼,位於arch/x86/kernel/entry_32.S#490
當執行time-asm
命令時,並不能在system_call()
處停下,該函式不是一個正常的C函式,gdb不支援跟蹤彙編程式碼
ENTRY(system_call)
system_call還有一個函式原型宣告,是一段彙編程式碼的起點,內部沒有遵守函式呼叫堆疊機制,所有gdb不能跟蹤
該段程式碼是理解Linux運作過程的關鍵,系統呼叫作為一種特殊的中斷,其執行過程可以類推到其他中斷訊號觸發的中斷服務處理過程,下面分析:系統呼叫time在核心程式碼中的處理過程
time -> system_call -> sys_time
system_call還涉及一個程序排程時機
中斷向量0x80和system_call中斷服務程式入口
start_kernel()
呼叫了trap_init()
,該函式呼叫了set_system_trap_gate()
#ifdef CONFIG_X86_32
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
set_bit(SYSCALL_VECTOR, used_vectors);
#endif
這是trap_init()
中的一段程式碼,位於arch/x86/kernel/traps.c#838
其中,system_call
被宣告為一個函式,是彙編程式碼的入口
透過set_system_trap_gate()
繫結中斷向量0x80和system_call
中斷服務程式入口,一旦執行int 0x80
則自動跳轉到system_call
SYSCALL_VECTOR
時系統呼叫中斷向量0x80,位於arch/x86/include/asm/irq_vectors.h#49
#define IA32_SYSCALL_VECTOR 0x80
#ifdef CONFIG_X86_32
#define SYSCALL_VECTOR 0x80
#endif
後面再分析int
指令執行或中斷訊號發生時,CPU的具體行為
system_call彙編程式碼和系統呼叫核心處理函式
system_call和其他中斷一樣,也有儲存現場SAVE_CALL和恢復現場restore_all
程式碼中的sys_call_table
是一個系統呼叫表,EAX暫存器傳遞系統呼叫號,在呼叫時會根據該值呼叫對應的系統呼叫核心處理函式,在退出時會進入syscall_exit_work,此時有一個程序排程時機
system_call
ENTRY(system_call)
RING0_INT_FRAME
ASM_CLAC
pushl_cfi %eax # 儲存系統呼叫號
SAVE_ALL # 儲存現場,將暫存器值壓棧
GET_THREAD_INFO(%ebp) # ebp用於存放當前程序thread_info結構的地址
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax # 檢查系統呼叫號,系統呼叫號應小於NR_syscalls
jae syscall_badsys # 不合法,跳轉到異常處理
syscall_call:
call *sys_call_table(, %eax, 4) # 透過系統呼叫號在表中找到對應的系統呼叫核心處理函式
movl %eax, PT_EAX(%esp) # 儲存返回值到棧
syscall_exit:
testl $_TIF_ALLWORK_MASK,%ecx # 檢查是否有任務需要處理
jne syscall_exit_work # 若需要,進入syscall_exit_work,這裡是最常見的程序排程時機
restore_all:
TRACE_ITQS_IRET # 恢復現場
irq_return:
INTERRUPT_RETURN # iret
在syscall_call中判斷當前任務是否需要處理,若需要,進入syscall_exit_work,這裡是最常見的程序排程時機
sys_call_table(,%eax,4) 透過系統呼叫號在表中找到對應的系統呼叫核心處理函式
系統呼叫表中每個表項佔4位元組,所以先將系統呼叫號(EAX暫存器)乘4,再加上表起始地址,即得到對應的系統呼叫核心處理函式指標
sys_call_table分派表由一段指令碼根據arch/x86/syscalls/syscall_32.tbl自動生成
整體上理解系統呼叫核心處理過程
流程圖中涉及system_call_exit
內部處理,大致過程是需要跳轉到work_pending
,裡面有work_notifysig
處理訊號,還有work_resched
需要重新排程,這裡是程序排程時機點call_schedule
,排程結束會跳轉到restore_all
恢復現場返回系統呼叫到使用者態,位於arch/x86/kernel/entry_32.S#593
ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
ASM_CLAC
pushl_cfi %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(NR_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
syscall_after_call:
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work
restore_all:
TRACE_IRQS_IRET
restore_all_notrace:
#ifdef CONFIG_X86_ESPFIX32
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss # returning to user-space with LDT SS
#endif
restore_nocheck:
RESTORE_REGS 4 # skip orig_eax/error_code
irq_return:
INTERRUPT_RETURN
從系統呼叫處理過程的入口開始,SAVE_ALL儲存現場,然後找到syscall_badsys和sys_call_table
call*sys_call_table(,%eax,4)就是呼叫了系統呼叫的核心處理函式,之後restore_all和INTERRUPT_RETURN(iret)用於恢復現場並返回系統呼叫到使用者態,這個過程中可能會執行syscall_exit_work,裡面有work_pending,其中的work_notifysig用來處理訊號,work_pending還有可能呼叫schedule