前言
系統調用的基本原理
系統調用其實就是函數調用,只不過調用的是內核態的函數,但是我們知道,用戶態是不能隨意調用內核態的函數的,所以采用軟中斷的方式從用戶態陷入到內核態。在內核中通過軟中斷0X80,系統會跳轉到一個預設好的內核空間地址,它指向了系統調用處理程序(不要和系統調用服務例程混淆),這里指的是在entry.S文件中的system_call函數。就是說,所有的系統調用都會統一跳轉到這個地址執行system_call函數,那么system_call函數如何派發它們到各自的服務例程呢?
我們知道每個系統調用都有一個系統調用號。同時,內核中一個有一個system_call_table數組,它是個函數指針數組,每個函數指針都指向了系統調用的服務例程。這個系統調用號是system_call_table的下標,用來指明到底要執行哪個系統調用。當int ox80的軟中斷執行時,系統調用號會被放進eax寄存器中,system_call函數可以讀取eax寄存器獲得系統調用號,將其乘以4得到偏移地址,以sys_call_table為基地址,基地址加上偏移地址就是應該執行的系統調用服務例程的地址。
系統調用的傳參問題
當一個系統調用的參數個數大於5時(因為5個寄存器(eax, ebx, ecx, edx,esi)已經用完了),執行int 0x80指令時仍需將系統調用功能號保存在寄存器eax中,所不同的只是全部參數應該依次放在一塊連續的內存區域里,同時在寄存器ebx中保存指向該內存區域的指針。系統調用完成之后,返回值扔將保存在寄存器eax中。由於只是需要一塊連續的內存區域來保存系統調用的參數,因此完全可以像普通函數調用一樣使用棧(stack)來傳遞系統調用所需要的參數。但是要注意一點,Linux采用的是c語言的調用模式,這就意味着所有參數必須以相反的順序進棧,即最后一個參數先入棧,而第一個參數則最后入棧。如果采用棧來傳遞系統調用所需要的參數,在執行int 0x80指令時還應該將棧指針的當前值復制到寄存器ebx中。
1.添加系統調用的兩種方法
方法一:編譯內核法
拿到源碼之后
- 修改內核的系統調用庫函數
/usr/include/asm-generic/unistd.h,在這里面可以使用在syscall_table中沒有用到的223號- 添加系統調用號,讓系統根據這個號,去找到syscall_table中的相應表項。在
/arch/x86/kernel/syscall_table_32.s文件中添加系統調用號和調用函數的對應關系- 接着就是my_syscall的實現了,在這里有兩種方法:第一種方法是在kernel下自己新建一個目錄添加自己的文件,但是要編寫Makefile,而且要修改全局的Makefile。第二種比較簡便的方法是,在kernel/sys.c中添加自己的服務函數,這樣子不用修改Makefile.
以上准備工作做完之后,然后就要進行編譯內核了,以下是我編譯內核的一個過程。
1.make menuconfig (使用圖形化的工具,更新.config文件)
2.make -j3 bzImage (編譯,-j3指的是同時使用3個cpu來編譯,bzImage指的是更新grub,以便重新引導)
3.make modules (對模塊進行編譯)
4.make modules_install(安裝編譯好的模塊)
5.depmod (進行依賴關系的處理)
6.reboot (重啟看到自己編譯好的內核)
方法二:內核模塊法
這種方法是采用系統調用攔截的一種方式,改變某一個系統調用號對應的服務程序為我們自己的編寫的程序,從而相當於添加了我們自己的系統調用。具體實現,我們來看下:
2.通過內核模塊實現添加系統調用
這種方法其實是系統調用攔截的實現。系統調用服務程序的地址是放在sys_call_table中通過系統調用號定位到具體的系統調用地址,那么我們通過編寫內核模塊來修改sys_call_table中的系統調用的地址為我們自己定義的函數的地址,就可以實現系統調用的攔截。
想法有了:那就是通過模塊加載時,將系統調用表里面的那個系統調用號的那個系統調用號對應的系統調用服務例程改為我們自己實現的系統歷程函數地址。但是內核已經不知道從哪個版本就不支持導出sys_call_table了。所以首先要獲取sys_call_table的地址。
網上介紹了好多種方法來得到sys_call_table的地址,這里介紹最簡單的一種方法
grep sys_call_table /boot/System.map-`uname -r`

這樣就得到了sys_call_table的地址,但同時也得到了一個重要的信息,該符號對應的內存區域是只讀的。所以我們要修改它,必須對它進行清楚寫保護,這里介紹兩種方法:
第一種方法::我們知道控制寄存器cr0的第16位是寫保護位。cr0的第16位置為了禁止超級權限,若清零了則允許超級權限往內核中寫入數據,這樣我們可以再寫入之前,將那一位清零,使我們可以寫入。然后寫完后,又將那一位復原就行了。
unsigned int clear_and_return_cr0(void)
{
unsigned int cr0 = 0;
unsigned int ret;
asm("movl %%cr0, %%eax":"=a"(cr0));
ret = cr0;
cr0 &= 0xfffeffff;
asm("movl %%eax, %%cr0"::"a"(cr0));
return ret;
}
void setback_cr0(unsigned int val) //讀取val的值到eax寄存器,再將eax寄存器的值放入cr0中
{
asm volatile("movl %%eax, %%cr0"::"a"(val));
}
第二種方法:通過設置虛擬地址對應的也表項的讀寫屬性來設置:
int make_rw(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level);//查找虛擬地址所在的頁表地址
if (pte->pte & ~_PAGE_RW) //設置頁表讀寫屬性
pte->pte |= _PAGE_RW;
return 0;
}
int make_ro(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level);
pte->pte &= ~_PAGE_RW; //設置只讀屬性
return 0;
}
3.編寫系統調用指定自己的系統調用
內核的初始化函數
在這里我使用系統空閑的223號空閑的系統調用號,你也可以換成其他系統調用的調用號,這樣你在執行其他函數時,就會調用自己的寫的函數的內容。
static int syscall_init_module(void)
{
printk(KERN_ALERT "sys_call_table: 0x%p\n", sys_call_table);//獲取系統調用表的地址
orig_saved = (unsigned long *)(sys_call_table[223]); //保存原有的223號的系統調用表的地址
printk(KERN_ALERT "orig_saved : 0x%p\n", orig_saved );
make_rw((unsigned long)sys_call_table); //修改頁的寫屬性
sys_call_table[223] = (unsigned long *)sys_mycall; //將223號指向自己寫的調用函數
make_ro((unsigned long)sys_call_table);
return 0;
}
自己的系統調用服務例程
asmlinkage long sys_mycall(void)
{
printk(KERN_ALERT "i am hack syscall!\n");
return 0;
}
移除內核模塊時,將原有的系統調用進行還原
static void syscall_cleanup_module(void)
{
printk(KERN_ALERT "Module syscall unloaded.\n");
make_rw((unsigned long)sys_call_table);
sys_call_table[223] = (unsigned long *) orig_saved ;
make_ro((unsigned long)sys_call_table);
}
模塊注冊相關
module_init(syscall_init_module);
module_exit(syscall_cleanup_module);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("mysyscall");
4.編寫用戶態的測試程序
1 #include <linux/unistd.h>
2 #include <syscall.h>
3 #include <sys/types.h>
4 #include <stdio.h>
5
6 int main(void)
7 {
8 long pid = 0;
9 pid = syscall(223);
10 printf("%ld\n",pid);
11 return 0;
12 }
當我們使用syscall()這個函數去觸發223的系統調用時,dmesg會發現我們自己寫的服務函數的輸出結果:

