【版權所有,轉載請註明出處。出處:http://www.cnblogs.com/joey-hua/p/5570691.html 】 Linux內核從啟動到初始化也看了好些個源碼文件了,這次看到kernel文件夾下的system_call.s,這個文件主要就是系統調用的過程。但說到系統調用,不只是這一 ...
【版權所有,轉載請註明出處。出處:http://www.cnblogs.com/joey-hua/p/5570691.html 】
Linux內核從啟動到初始化也看了好些個源碼文件了,這次看到kernel文件夾下的system_call.s,這個文件主要就是系統調用的過程。但說到系統調用,不只是這一個文件這麼簡單,裡面牽扯到的內容太多,這裡就做個筆記記錄一下從建立中斷到最終調用系統調用的完整機制。
假設就從write這個函數作為系統調用來解釋。
系統調用的本質就是用戶進程需要訪問內核級別的代碼,但用戶進程的許可權是最低的,內核代碼是許可權最高的,不允許直接訪問,需要通過中斷門作為媒介來實現許可權的跳轉。簡單講就是用戶進程調用一個中斷,這個中斷再去訪問內核代碼。這裡就來學習一下Linux內核具體是怎麼做的。
1.建立中斷描述符表IDT
因為要用到中斷,所以首先要建立中斷描述符表IDT,作用如下圖:
在head.s文件中,建立好了IDT,比如要使用int 0x80,就從_idt開始找到偏移為0x80的地方執行代碼。
.align 3 # 按8 位元組方式對齊記憶體地址邊界。 _idt: .fill 256,8,0 # idt is uninitialized# 256 項,每項8 位元組,填0。 idt_descr: #下麵兩行是lidt 指令的6 位元組操作數:長度,基址。 .word 256*8-1 # idt contains 256 entries .long _idt lidt idt_descr # 載入中斷描述符表寄存器值。
2.建立0x80號中斷
所有的系統調用都是通過0x80號中斷來實現的,所以接下來就是建立第0x80號中斷,在sched.c中:
// 設置系統調用中斷門。 set_system_gate (0x80, &system_call);
這裡通過set_system_gate這個巨集定義就把0x80中斷和函數system_call關聯上了,這裡先不管system_call,先看set_system_gate,在system.h中:
//// 設置系統調用門函數。 // 參數:n - 中斷號;addr - 中斷程式偏移地址。 // &idt[n]對應中斷號在中斷描述符表中的偏移值;中斷描述符的類型是15,特權級是3。 #define set_system_gate(n,addr) _set_gate(&idt[n],15,3,addr) //// 設置門描述符巨集函數。 // 參數:gate_addr -描述符地址;type -描述符中類型域值;dpl -描述符特權層值;addr -偏移地址。 // %0 - (由dpl,type 組合成的類型標誌字);%1 - (描述符低4 位元組地址); // %2 - (描述符高4 位元組地址);%3 - edx(程式偏移地址addr);%4 - eax(高字中含有段選擇符)。 #define _set_gate(gate_addr,type,dpl,addr) \ __asm__ ( "movw %%dx,%%ax\n\t" \ // 將偏移地址低字與選擇符組合成描述符低4 位元組(eax)。 "movw %0,%%dx\n\t" \ // 將類型標誌字與偏移高字組合成描述符高4 位元組(edx)。 "movl %%eax,%1\n\t" \ // 分別設置門描述符的低4 位元組和高4 位元組。 "movl %%edx,%2": :"i" ((short) (0x8000 + (dpl << 13) + (type << 8))), "o" (*((char *) (gate_addr))), "o" (*(4 + (char *) (gate_addr))), "d" ((char *) (addr)), "a" (0x00080000))
這裡參考中斷門結構圖可知,這裡設置特權級是3,用戶進程也是3,就可以直接訪問此中斷,偏移地址對應的上面的system_call,也就是說如果調用中斷int 0x80,那麼就會去訪問system_call函數。註意這裡的n就是0x80,也就是idt數組的[0x80],idt在head.h中聲明,編譯後會變成符號_idt,在head.s中定義的,就此關聯上。
3.聲明系統調用函數
以write系統函數為例,在write.c中聲明此函數:
_syscall3 (int, write, int, fd, const char *, buf, off_t, count)
_syscall3又是一個巨集定義,在unistd.h中:
// 有3 個參數的系統調用巨集函數。type name(atype a, btype b, ctype c) // %0 - eax(__res),%1 - eax(__NR_name),%2 - ebx(a),%3 - ecx(b),%4 - edx(c)。 #define _syscall3(type,name,atype,a,btype,b,ctype,c) \ type name(atype a,btype b,ctype c) \ { \ long __res; \ __asm__ volatile ( "int $0x80" \ : "=a" (__res) \ : "" (__NR_##name), "b" ((long)(a)), "c" ((long)(b)), "d" ((long)(c))); \ if (__res>=0) \ return (type) __res; \ errno=-__res; \ return -1; \ }
所以翻譯過來就是在write.c中可以寫成:
int write(int fd,const char* buf,off_t count) \ { \ long __res; \ __asm__ volatile ( "int $0x80" \ : "=a" (__res) \ : "" (__NR_write), "b" ((long)(fd)), "c" ((long)(buf)), "d" ((long)(count))); \ if (__res>=0) \ return (type) __res; \ errno=-__res; \ return -1; \ }
是不是一下子就清晰明朗了,也就是說,如果一個用戶進程要使用write函數,就會去調用int 0x80中斷,然後把三個參數fd、buf、count分別存入ebx、ecx、edx寄存器,還有個最關鍵的是_NR_write,會把這個值存入eax寄存器,具體做什麼用等會再說,這個是在unistd.h中定義的:
#define __NR_write 4
好,現在各種初始化和聲明都完成了,萬事俱備只欠東風!
4.系統調用過程
用戶進程調用函數write,就會調用int 0x80中斷,上面第2點已經說了,如果調用中斷int 0x80會去訪問system_call函數,sched.c:
extern int system_call (void); // 系統調用中斷處理程式(kernel/system_call.s,80)。
是在system_call中定義,註意編譯後頭部會加上_,以下代碼只截取了前半部分:
_system_call: cmpl $nr_system_calls-1,%eax # 調用號如果超出範圍的話就在eax 中置-1 並退出。 ja bad_sys_call push %ds # 保存原段寄存器值。 push %es push %fs pushl %edx # ebx,ecx,edx 中放著系統調用相應的C 語言函數的調用參數。 pushl %ecx # push %ebx,%ecx,%edx as parameters pushl %ebx # to the system call movl $0x10,%edx # set up ds,es to kernel space mov %dx,%ds # ds,es 指向內核數據段(全局描述符表中數據段描述符)。 mov %dx,%es movl $0x17,%edx # fs points to local data space mov %dx,%fs # fs 指向局部數據段(局部描述符表中數據段描述符)。 # 下麵這句操作數的含義是:調用地址 = _sys_call_table + %eax * 4。參見列表後的說明。 # 對應的C 程式中的sys_call_table 在include/linux/sys.h 中,其中定義了一個包括72 個 # 系統調用C 處理函數的地址數組表。 call _sys_call_table(,%eax,4) pushl %eax # 把系統調用號入棧。(這個解釋錯誤,是函數返回值入棧) movl _current,%eax # 取當前任務(進程)數據結構地址??eax。
註意從pushl %edx開始的三句代碼,是前面第3點提到的三個參數依次從右向左入棧。重點是call _sys_call_table(,%eax,4)這句代碼,翻譯過來就是call [eax*4 + _sys_call_table],根據第3點,eax存的是_NR_write的值也就是4,因為_sys_call_table是sys.h中的一個int (*)()類型的數組,裡面存的是所有的系統調用函數地址,所以再翻譯一下就是訪問sys_call_table[4]也就是sys_write函數:
// 系統調用函數指針表。用於系統調用中斷處理程式(int 0x80),作為跳轉表。 fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, sys_write, ...}
sys_write在fs下的read_write.c:
int sys_write (unsigned int fd, char *buf, int count) { struct file *file; struct m_inode *inode; ... }
好了,到這裡為止才明白千迴百轉最終調用的就是這個sys_write函數。至此分析結束!