深入浅出内核学习之系统调用:
系统调用是用户空间访问内核空间的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。
总的概括来讲,系统调用在系统中的主要用途无非以下几类:
l 控制硬件——系统调用往往作为硬件资源和用户空间的抽象接口,比如读写文件时用到的write/read调用。
l 设置系统状态或读取内核数据——因为系统调用是用户空间和内核的唯一通讯手段[2],所以用户设置系统状态,比如开/关某项内核服务(设置某个内核变量),或读取内核数据都必须通过系统调用。比如getpgid、getpriority、setpriority、sethostname
l 进程管理——一系统调用接口是用来保证系统中进程能以多任务在虚拟内存环境下得以运行。比如 fork、clone、execve、exit等
第二,什么服务应该存在于内核;或者说什么功能应该实现在内核而不是在用户空间。这个问题并没有明确的答案,有些服务你可以选择在内核完成,也可以在用户空间完成。选择在内核完成通常基于以下考虑:
l 服务必须获得内核数据,比如一些服务必须获得中断或系统时间等内核数据。
l 从安全角度考虑,在内核中提供的服务相比用户空间提供的毫无疑问更安全,很难被非法访问到。
l 从效率考虑,在内核实现服务避免了和用户空间来回传递数据以及保护现场等步骤,因此效率往往要比在用户空间实现高许多。比如,httpd等服务。
l 如果内核和用户空间都需要使用该服务,那么最好实现在内核空间,比如随机数产生。
现在以加入一个foo的系统调用来看具体步骤:
1. 首先将sys_foo系统调用加入到系统调用表中,大多数体系结构中,系统调用表位于syscall_table.S中,
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid
.long sys_creat
.long sys_link
.long sys_unlink /* 10 */
.long sys_execve
.long sys_chdir
.long sys_time
.long sys_mknod
.long sys_chmod /* 15 */
......
.long sys_tee /* 315 */
.long sys_vmsplice
.long sys_move_pages
/*加入我们的系统调用*/
.long sys_foo /*318*/
对于每种需要支持的平台体系,我们都必须将我们的系统调用加入到其系统调用表中去,以让你的系统调用支持每种体系。
2) 将我们的系统调用号加入到
中,他包含了所有的系统调用号。
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
......
#define __NR_vmsplice 316
#define __NR_move_pages 317
/*加入我们的系统调用号*/
#define __NR_foo 318
3)实现foo系统调用
#include
asmlinkage long sys_foo(void)
{
return THREAD_SIZE;
}
到此,现在就可以在用户空间使用foo系统调用了
如何从用户空间访问系统调用,如下方法:
1)通过库来支持,由库函数实际来调用系统调用。需要实现库函数
1)通常系统调用靠C库支持,用户程序通过包含标准头文件和C库链接,就可以使用系统调用(或者通过库函数,再由库函数实际调用),
2)如果仅仅写出系统调用,glibc并不提供支持。但是linux本身提供了一组宏来实现用户空间的系统调用,他们是syscalln(),其中n是0~6,表示需要传递给系统调用的参数,这是由于宏必须要知道有多少参数按照什么次序压入栈,举个例子,open系统调用的定义是:
long open(const char * filename,int flags, int mode)
#define __NR_open 5
_syscall3(long,open,const char *,filename,int,flags,int,mode)
这样应用程序就可以直接使用open了。
__NR_open是系统调用号,该宏会被扩展为内嵌汇编的c函数,将系统调用号和参数压入寄存器并触发软中断来陷入内核。
在应用程序中实现我们的系统调用:
#deine __NR_foo 318
__syscall0(long,foo)
int main()
{
long stack_size;
stack_size = foo();
printf("the kernel stack size is %ld\n",stack_size);
return 0;
}
内核公开的内核函数——export出来的——可以使用命令ksyms 或 cat /proc/ksyms来查看
linux实现系统调用是利用0x86体系结构的软中断,通过软件指令而非外设引发中断,也就是
调用int $0x80,这条指令将产生中断向量为128的编程异常,这个编程异常对应的是中断描述符表IDT中的第128项——也就是对应的系统门描述符。门描述符中含有一个预设的内核空间地址,它指向了系统调用处理程序:system_call()(别和系统调用服务程序混淆,这个程序在entry.S文件中用汇编语言编写)。
系统调用大致可以归结为以下几个步骤:
1 应用程序调用libc库的封装函数,封装函数将系统调用号__NR_XXX()压入到EAX寄存器,
2 调用 软中断0x80陷入内核
以下进入内核
3 在内核首先执行system_call,直接执行根据系统调用号在调用表中查找到的对应的系统调用服务例程sys_XXX。
4 执行sys_xxx
5 执行完毕后,转入ret_from_sys_call例程,系统调用中返回。
每一个系统调用都是通过int 0x80中断进入核心,中断描述符表把中断服务程序和中断向量对应起来。对于系统调用来说,操作系统会调用system_call中断服务程序。 system_call函数在系统调用表中根据系统调用号找到并调用相应的系统调用服务例程。idtr寄存器指向中断描述符表的起始地址,用 sidt[asm ("sidt %0" : "=m" (idtr));]指令得到中断描述符表起始地址,从这条指令中得到的指针可以获得int 0x80中断服描述符所在位置,然后计算出system_call函数的地址。反编译一下system_call函数可以看到在system_call函数内,是用call sys_call_table指令来调用系统调用函数的。因此,只要找到system_call里的call sys_call_table(,eax,4)指令的机器指令就可以获得系统调用表的入口地址了。
中断描述符表:
在实地址模式下,CPU把内存中从0开始的1K字节作为一个中断向量表,表中的每个表项占4个字节,由2个字节的段地址和2字节的段偏移量组成,这样构成的地址便是中断处理程序的入口地址(这样就支持1K/4=256个中断处理程序),但是在保护模式下显然满足不了由四字节构成的中断向量表的要求,除了2个字节的段描述符,偏移量必须要用4个字节来表示以反映模式切换。在保护模式下,中断描述符是由8字节组成,由所有的中断描述符组成的表叫做中断描述符表,其中的每个表项也叫一个门描述符,当中断发生时,必须先通过这些门,然后才能进入相应的处理程序。
其中类型占3位,表示门描述符的类型
1:中断门
2:陷阱门
3:系统门,这是用来让用户态的进程访问intel的陷阱门,通过系统门来激活4个linux异常处理程序,它们的向量是3、4、5、128。即在用户态可以通过int3、into、bound以及0x80汇编指令来陷入内核。
在保护态,IDT不是放在0开始地方,需要使用中断描述符表接触器(IDTR)来获取IDT在内存中的起始地址,它是一个48位寄存器,低16位保存IDT的大小,高32位保存IDT的基址
中断描述符表寄存器IDTR
由上面可知要得到一个中断向量处理函数的入口地址方法如下:
1)获取IDTR寄存器地址
2)获取IDT地址
3)由中断向量号以及中断向量描述符中的入口函数地址偏移量来获得中断处理程序的入口地址
typedef unsigend int u32;
typedef unsigned short u16;
typedef unsigned char u8;
IDTR结构体:
typedef struct idtr{
u16 limted;
u32 base;
}IDTR;
typedef struct idt{
u16 off_low;
u16 segment_select;
u8 reserved;
u8 flags;
u16 off_high;
}IDT;
IDTR idtr;
IDT *idt;
1) 获取IDT起始地址
__asm__ __volatile__("sidt %0":"=m"(idtr));
2)由IDTR获得0x80在IDT中的地址
idt=(void *)(idtr.base+ 8*128);
3)由入口函数地址偏移量来获得(system_call)中断处理程序的入口地址
u32 system_call = idt->off_high << 16 | idt->off_low
4)从中断处理例程中(指令码)搜索sys_call_table地址
------------------------------------------------------------------------------------------
(gdb) disas system_call
Dump of assembler code for function system_call:
0xc1003004 : push %eax
0xc1003005 : cld
0xc1003006 : push %es
0xc1003007 : push %ds
0xc1003008 : push %eax
0xc1003009 : push %ebp
0xc100300a : push %edi
0xc100300b : push %esi
0xc100300c : push %edx
0xc100300d : push %ecx
0xc100300e : push %ebx
0xc100300f : mov $0x7b,%edx
0xc1003014 : movl %edx,%ds
0xc1003016 : movl %edx,%es
0xc1003018 : mov $0xfffff000,%ebp
0xc100301d : and %esp,%ebp
0xc100301f : testl $0x100,0x30(%esp)
0xc1003027 : je 0xc100302d
0xc1003029 : orl $0x10,0x8(%ebp)
End of assembler dump.
(gdb) disas syscall_call
Dump of assembler code for function syscall_call:
0xc1003044 : call *0xc1275480(,%eax,4)
0xc100304b : mov %eax,0x18(%esp)
End of assembler dump.
查看call语句的指令码为0xc03094c08514ff
--------------------------------------------------------------------------------------
于是只需要匹配到
char *p=system_call;
for(i=0;i<100;i++){
if( ((p[i]='xff') && (p[i+1]=='x14') && (p[i+2]=='x85')){
sys_call_table= &p[i]+3;
break;
}
}
return sys_call_table;
具体程序实现如下:
//function:实现系统调用的截获以及隐藏技术的实现
#include
#include
#include
#include
#include
#include
#include
#define pr_dbg(fmt,arg...) printk(KERN_DEBUG fmt,##arg)
/******
__attribute__(()):主要是用来在函数或者数据声明中设置属性。给函数赋予属性的目的是让编译器进行优化。
__attribute__((packed)):告诉编译器取消结构在编译过程中的优化对齐(gcc编译器不是紧凑模式的),按照实际字节数进行对齐。
使用该属性使得变量或者结构体成员使用最小大对齐方式,即对变量是一字节对齐,对域是位对齐。
例:struct test{char h,int a} sizeof(int)=4;sizeof(test)=8;
struct test{char h,int a}__attrubte__((packed)) sizeof(int)=4;sizeof(test)=5;
*****/
}
typedef asmlinkage int (*__routine)(struct pt_regs);
int (*orig_mkdir)(const char *path);
int sys_hackedmkdir(const char *path)
{
printk("there is nothing\n");
return 0;
}
long **sys_call_table;
__routine old,new;
int counts=0;
/*中断描述符寄存器,高32位保存idt在内存中的起始地址*/
typedef struct{
u16 limted;
u32 base;//idt在内存中的基址;
}__attribute__((packed))IDTR;
typedef struct{
u16 offset_low;
u16 segmet_select;
u8 reserve;
u8 flags;
u16 offset_high;
}__attribute__((packed))IDT;
unsigned char call_hex[]={0xff,0x14,0x85};
u32 get_syscall_table(void)
{
IDTR idtr;
IDT *idt;
u32 system_call;
unsigned char *serch_ptr,*psys=NULL;
int i=0;
/*通过sidt指令获取idtr*/
__asm__ __volatile__("sidt %0":"=m"(idtr));
idt = (IDT *)(idtr.base + 8*0x80);
// memcpy(&idt,idtr.base + 8*128,sizeof(idt));
/*获取系统调用system_call的入口地址;*/
system_call = idt->offset_high << 16;
system_call &= 0xffff0000;
system_call |= idt->offset_low;;
printk("addr of idt 0x80: %x\n", system_call);
serch_ptr = (u8 *)system_call;
printk("addr of serch_ptr: %x,serch_ptr[0]=%x\n", serch_ptr,serch_ptr[0]);
while(i<100){
printk("serch_ptr[%d]=%c\n",i,serch_ptr[i]);
if((serch_ptr[i]== call_hex[0]) && (serch_ptr[i+1] == call_hex[1]) && (serch_ptr[i+2] == call_hex[2])){
psys=&serch_ptr[i] + 3;
break;
}
i++;
}
printk("addr of sys_call_table: %x\n", *(u32 *)psys);
return *(u32 *)psys;
}
static int __init mysys_init(void)
{
long sct;
long *tempj;
sct = get_syscall_table();
if(sct){
tempj = (long *)sct;
sys_call_table = (long **)tempj;
printk("sct=%x\n",sys_call_table);
orig_mkdir = sys_call_table[__NR_mkdir];
printk("sys_call_table[__NR_mkdir]=%x,sys_testsyscall=%x\n",sys_call_table[__NR_mkdir],sys_hackedmkdir);
sys_call_table[__NR_mkdir] = (long *)&sys_hackedmkdir;
set_symbol_addr((unsigned) &symname, sct);
EXPORT_SYMBOL(sys_call_table);
printk("sys_call_table=%x\n",sys_call_table);
return 0;
}
static void __exit mysys_exit(void)
{
printk("keep back the sys_call_getpid\n");
sys_call_table[__NR_mkdir]=orig_mkdir;
printk("aft exit,sys_call_table[__NR_mkdir]=%x\n",sys_call_table[__NR_mkdir]);
}
module_init(mysys_init);
module_exit(mysys_exit);
MODULE_LICENSE("GPL");
阅读(2326) | 评论(0) | 转发(0) |