最近做了我们院的操作系统课程设计,本来是想把我的报告拿出来和大家分享的,可寝室的朋友说我这么做容易使后来的童鞋不经过自己的思考就全盘copy,所以我决定还是分类讲讲我做的过程吧!首先完成的是一个linux系统功能调用的增加。
我们都知道系统功能调用是Unix/Linux操作系统向用户程序提供支持的接口,通过这些接口应用程序向操作系统请求服务,控制转向操作系统,而操作系统在完成服务后,将控制和结果返回给用户程序。
系统调用的主要目的是使得用户可以使用操作系统提供的有关设备管理、输入/输出系统、文件系统和进程控制、通信以及存储管理等方面的功能,而不必了解系统程序的内部结构和有关硬件细节,从而起到减轻用户负担和保护系统以及提高资源利用率的作用。
在进行系统功能调用时会由用户态(也称目态)转到核态(也称管态)。昨天我的一信安的朋友还问过我管态和目态的区别,我想来想去也就是这里有大的区别了,在管态下能使用系统的所有资源,调用系统的特权函数,而目态是不行的。在执行系统功能调用时就必须是在管态下执行。
而如果我需要给我的linux系统增加一个系统功能调用的话,就必须弄清楚系统是如何调用那些功能函数的,又是如何由目态变为管态的。在linux下可以通过中断来进入管态,这类中断称为访管中断。
在Linux系统中,系统调用是作为一种异常类型实现的,它将执行相应的机器代码指令来产生异常信号。产生中断或异常的重要效果是系统自动将用户态切换为核心态来对它进行处理。
用户态的程序只有通过门(gate)陷入(trap)到系统内核中去(执行int指令),才能执行一些具有特权的内核函数。系统调用完成后,系统执行另一组特征指令(iret指令)将系统返回到用户态,控制权返回给进程。
Linux用来实现系统调用异常的实际指令是:
int $0x80
这一指令使用中断/异常向量号128(即16进制的80)将控制权转移给内核(进行模式切换)。
为达到在使用系统调用时不必用机器指令编程,在标准的C语言库中为每一系统调用提供了一段短的子程序,完成机器代码的编程工作。
事实上,机器代码段非常简短。它所要做的工作只是将送给系统调用的参数加载到CPU寄存器中,接着执行int $0x80指令。然后运行系统调用。
系统调用的返回值将送入CPU的一个寄存器中,标准的库子程序取得这一返回值,并将它送回用户程序。
下面以getuid()系统调用为例来看调用过程:
我们可以看到其中有一些宏定义,我们可以看看这些宏的定义
(arch/i386/kernel/ entry.S).
………
#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS),%edx; \
movl %edx,%ds; \
movl %edx,%es;
我们可以看到SAVE_ALL主要是保存寄存器信息,即现场保留。其中, movl $(__USER_DS), %edx;从这一句开始是重新填充DS,ES段。
#define RESTORE_INT_REGS \
popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax
#define RESTORE_REGS \
RESTORE_INT_REGS; \
1: popl %ds; \
2: popl %es; \
.section .fixup,"ax"; \
3: movl $0,(%esp); \
jmp 1b; \
4: movl $0,(%esp); \
jmp 2b; \
.previous; \
.section __ex_table,"a";\
.align 4; \
.long 1b,3b; \
.long 2b,4b; \
.previous
ENTRY(ret_from_fork)
pushl %eax
call schedule_tail
GET_THREAD_INFO(%ebp)
popl %eax
jmp syscall_exit
这里主要完成现场恢复并返回。
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation
/* Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb */
testw $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SECCOMP),TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,EAX(%esp) # store the return value
syscall_exit:
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx # current->work
jne syscall_exit_work
restore_all:
movl EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb OLDSS(%esp), %ah
movb CS(%esp), %al
andl $(VM_MASK | (4 << 8) | 3), %eax
cmpl $((4 << 8) | 3), %eax
je ldt_ss # returning to user-space with LDT SS
restore_nocheck:
RESTORE_REGS
addl $4, %esp
1: iret
这一段中,主要是完成调用。eax放置的是系统调用号,因为eax有可能被使用,所以先保存其值。call *sys_call_table(,%eax,4)这一句是计算调用的入口。
其中,sys_call_table是LINUX的系统调用表,它存在目录arch/i386/kernel/ sys_call_table.S 下。
.data
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_mq_timedreceive /* 280 */
.long sys_mq_notify
.long sys_mq_getsetattr
.long sys_ni_syscall /* reserved for kexec */
.long sys_waitid
.long sys_ni_syscall /* 285 */ /* available */
.long sys_add_key
.long sys_request_key
.long sys_keyctl
.代表当前地址,sys_call_table代表数组首地址。这个表依次保存所有系统调用的函数指针,以方便总的系统调用处理函数(system_call)进行索引。
调用具体的实现在kernel/sys.c中。
asmlinkage long
sys_getuid16(void)
{
return hig2lowuid(current_uid);
}
刚才我们提到,这一指令使用中断/异常向量号128(即16进制的80)将控制权转移给内核,那么中断向量是怎么形成的。它的定义在(arch/i386/kernel/traps.c)中。
void __init trap_init(void)
{
……
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_intr_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
set_trap_gate(18,&machine_check);
set_trap_gate(19,&simd_coprocessor_error);
set_system_gate(,&system_call);
……
}
上一句就是设置system_call的值。SYSCALL_VECTOR的值就是0X80.
那么概括起来,系统调用的过程大致如下:
(1) 系统调用初始化
在traps.c中,系统在初始化程序trap_init()中,通过调用
set_system_gate(0x80,*system_call)
完成中断描述表的填充。这样当每次用户执行指令int 0x80时,系统能把控制转移到entry.S中的函数中去。
(2) 系统调用执行
system_call会根据用户传进来系统调用号,在系统调用表 system_call中寻找到相应偏移地址的内核处理函数,进行相应的处理。当然在这个过程之前,要保存环境(SAVE_ALL)。
(3) 系统调用的返回
系统调用处理完毕后,通过sys_call_exit返回。返回之前,程序会检查一些变量,相应地返回。不一定是返回到用户进程。真正返回到用户空间时,要恢复环境(restore_all)。
在前面提到system_call会根据用户传进来系统调用号,在系统调用表 system_call中寻找到相应偏移地址的内核处理函数,进行相应的处理。
那么系统调用号怎么产生,在include/asm-i386/unistd.h中可以看到系统调用号的定义。
#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_waitpid 7
#define __NR_creat 8
#define __NR_link 9
……
#define __NR_mq_open 277
#define __NR_mq_unlink (__NR_mq_open+1)
#define __NR_mq_timedsend (__NR_mq_open+2)
#define __NR_mq_timedreceive (__NR_mq_open+3)
#define __NR_mq_notify (__NR_mq_open+4)
#define __NR_mq_getsetattr (__NR_mq_open+5)
#define __NR_sys_kexec_load 283
#define __NR_waitid 284
/* #define __NR_sys_setaltroot 285 */
#define __NR_add_key 286
#define __NR_request_key 287
#define __NR_keyctl 288
#define NR_syscalls 289
此处的代码是从2.6.11中的代码,其中系统调用号已到了288,并且与前面system_call中的相对应。每一个系统调用号前都是相应函数名加了__NR_。
内核跟用户程序的交互,其实有标准C库作为它们之间的桥梁。标准C库把用户希望传递的参数装载到CPU的寄存器中,然后触发0X80中断。
当从系统调用返回的时候(sys_call_exit),标准C库又接过控制权,处理返回值。
对于__NR_,标准C库会作相应处理。转换成相应函数。
对于系统函数的调用,有几个通用的宏在include/asm-i386/unistd.h中定义。
#define __syscall_return(type, res) \
do { \
if ((unsigned long)(res) >= (unsigned long)(-(128 + 1))) { \
errno = -(res); \
res = -1; \
} \
return (type) (res); \
} while (0)
#else
# define __syscall_return(type, res) return (type) (res)
#endif
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
__syscall_return(type,__res); \
}
这是无参函数调用的形式。
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1))); \
__syscall_return(type,__res); \
}
这是含一个参数的调用形式,
……
标准C库会把我们的调用如pause()转换成相应的形式。
pause()
int pause(void)
{
long __res;
__asm__ volatile(“int $0x80”
:”=a”(__res)
:””(__NR_pause));
__syscall_return(int,__res);
}
进入内核调用过程。
基础知识介绍完了,下面来进行我们的实验:
如果你安装的系统包含内核源文件,一般在/usr/src路径下可以看到,那么可以直接跳到步骤3进行内核修改。
首先下载最新的linux2.6.37内核,先修改/usr/src/linux下的Makefile文件,将内核版本修改成自己的。
把2.6.37中Makefile文件头几行为:
VERSION = 2
PATCHLEVEL = 6
SUBLEVEL = 37
EXTRAVERSION = .1
我们可以修改成自己版本(2.6.37.rangercyh):
VERSION = 2
PATCHLEVEL = 6
SUBLEVEL = 37
EXTRAVERSION = rangercyh
如果系统不包含源文件,则需要在网站上下载系统源代码。
网址:http://kernel.org
在官方网站上下到类似 linux-2.6.37.1.tar.gz的代码后(大概有70兆左右),放在/usr/src/ 的目录下,
然后解压(如何解压请看我的博文:http://rangercyh.blog.51cto.com/1444712/494660),解压后会出现文件夹 linux-2.6.37.1。不过貌似最新的代码已经出到2.6.38了。
(1) 修改(添加)源代码
第一个任务是编写加到内核中的源程序,即将要加到一个内核文件中去的一个函数,该函数的名称应该是新的系统调用名称前面加上sys_标志。
假设新加的系统调用为mycall(int number),在/usr/src/linux—2.6.37.1/kernel/sys.c文件中添加源代码,如下所示:
作为一个最简单的例子,我新加的系统调用是实现一个文件复制功能。
代码如下:
asmlinkage int sys_mysyscall(const char* s_file, const char* t_file)
{
//printk("hello,world!\n");
int bytes_read, bytes_write;
int from_fd, to_fd; //文件标识符
char buffer[100];
char *ptr;
mm_segment_t old_fs;
old_fs = get_fs(); //保存原来的段
set_fs(KERNEL_DS); //设置为数据段
if ((from_fd = sys_open(s_file,O_RDONLY,0)) == -1)
return -1;
if ((to_fd = sys_open(t_file,O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR)) == -1)
return -2;
while(bytes_read=sys_read(from_fd,buffer,1)) //读源文件,一个字符一个字符的读
{
if((bytes_read==-1)) break;
else if(bytes_read>0)
{
ptr=buffer;
while(bytes_write=sys_write(to_fd,ptr,bytes_read)) //写入文件
{
if((bytes_write==-1))break;
/* 写完了所有读的字节 */
else if(bytes_write==bytes_read) break;
else if(bytes_write>0)
{
ptr+=bytes_write;
bytes_read-=bytes_write;
}
}
if(bytes_write==-1)break;
}
}
set_fs(old_fs);
return 0;
}
说明:在以后的文档中,我们所指的/usr/src/linux—2.6.37.1/都是指内核路径,要根据自己的内核存放位置进行相应的改变。
(2) 连接新的系统调用
添加新的系统调用后,下一个任务是使Linux内核的其余部分知道该程序的存在。为了从已有的内核程序中增加到新的函数的连接,需要编辑两个文件。
在我们所用的Linux内核版本(2.6.37.1)中,首先要修改的文件是unistd.h:
/usr/src/linux—2.6.37.1/arch/x86/include/asm/unistd_32.h
该文件中包含了系统调用清单,用来给每个系统调用分配一个唯一的号码。
文件中每一行的格式如下:
#define __NR_name NNN
其中,name用系统调用名称代替,而NNN则是该系统调用对应的号码
应该将新的系统调用名称加到清单的最后,并给它分配号码序列中下一个可用的系统调用号。我们的系统调用如下:
#define __NR_sync_file_vange 337
#define __NR_tee 338
#define __NR_vmsplice 339
#define __NR_syscalls 340
最后一行NR_syscalls 说明内核自身的系统调用号已经使用到339,共有340个(从0开始)。我们新添加的系统调用应该加到最后,并修改系统调用总数。修改如下:
#define __NR_sync_file_vange 337
#define __NR_tee 338
#define __NR_vmsplice 339
#define __NR_mysyscall 340 /*这是我们自己添加的系统调用*/
#define __NR_syscalls 341
第二个要修改的文件是:
/usr/src/linux—2.6.37.1/arch/x86/kernel/syscall_table_32.s
在2.6版本以前,需要修改的是/usr/src/linux/arch/i386/kernel/entry.s,在该文件下有sys_call_table,直接进行修改。
在2.6版本后,entry.s文件中的sys_call_table独立出来,我们可以在该文件下看到类似于#include "syscall_table.s"字样,说明我们正真需要修改syscall_table.s。 在syscall_table.s中有类似如下的清单:
.long sys_name
该清单用来对sys_call_table[]数组进行初始化。该数组包含指向内核中每个系统调用的指针。这样就在数组中增加了新的内核函数的指针。
我们在清单上与系统调用号相对应的位置添加一行:
.long sys_mysyscall
必须注意添加的行的位置,否则容易造成内核编译的失败。
首先需要清空以前的编译信息,make mrproper命令清除旧的配置等文件,避免编译内核时生成的文件不一致。
make menuconfig命令生成配置清单文件。
make -j5命令编译新内核,这个编译过程非常长,需要1~2个小时。j5参数代表使用5个线程同时编译,这样速度会点。
make bzImage命令生成系统视图。
make modules命令生成模块。
make modules_install命令安装模块。
mkinitramfs -o /boot/initrd.img-mycall命令生成系统镜像文件。
make install命令安装新的系统。
update-grub命令更新启动程序grub,使启动界面上出现新安装的系统。
测试代码如下,使用syscall函数调用自己编写的系统功能函数:
- #include <stdio.h>
- void main()
- {
- syscall(341, "./2.c", "./newfile.c");
- }
注意:syscall()中的第一个参数341是系统调用号,第二个参数是源文件,第三个参数是目标文件。
至此这个系统功能调用就完成了,这次实验有个小插曲,就是我同时在帮一位信安的朋友一起做这个课设,所以文档很多地方都是直接copy的,结果我是先把他的文档完成了再做我自己的文档,结果没有留意到一个地方写了他的名字,结果文档被老师收走了,现在我只能听天由命了,希望老师不要发现这个名字的问题,要不然我就会悲剧的被指名抄袭~~~我冤不冤~~有木有~~有木有~~
本文出自 “菜鸟浮出水” 博客,请务必保留此出处http://rangercyh.blog.51cto.com/1444712/521200
- asmlinkage int sys_mysyscall(const char* s_file, const char* t_file)
-
- {
-
-
-
- int bytes_read, bytes_write;
-
- int from_fd, to_fd;
-
- char buffer[100];
-
- char *ptr;
-
- mm_segment_t old_fs;
-
- old_fs = get_fs();
-
- set_fs(KERNEL_DS);
-
- if ((from_fd = sys_open(s_file,O_RDONLY,0)) == -1)
-
- return -1;
-
- if ((to_fd = sys_open(t_file,O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR)) == -1)
-
- return -2;
-
- while(bytes_read=sys_read(from_fd,buffer,1))
-
- {
-
- if((bytes_read==-1)) break;
-
- else if(bytes_read>0)
-
- {
-
-
-
- ptr=buffer;
-
- while(bytes_write=sys_write(to_fd,ptr,bytes_read))
-
- {
-
- if((bytes_write==-1))break;
-
-
-
- else if(bytes_write==bytes_read) break;
-
- else if(bytes_write>0)
-
- {
-
- ptr+=bytes_write;
-
- bytes_read-=bytes_write;
-
- }
-
- }
-
- if(bytes_write==-1)break;
-
- }
-
- }
-
- set_fs(old_fs);
-
- return 0;
-
- }
说明:在以后的文档中,我们所指的/usr/src/linux—2.6.37.1/都是指内核路径,要根据自己的内核存放位置进行相应的改变。
(2) 连接新的系统调用
添加新的系统调用后,下一个任务是使Linux内核的其余部分知道该程序的存在。为了从已有的内核程序中增加到新的函数的连接,需要编辑两个文件。
在我们所用的Linux内核版本(2.6.37.1)中,首先要修改的文件是unistd.h:
/usr/src/linux—2.6.37.1/arch/x86/include/asm/unistd_32.h
该文件中包含了系统调用清单,用来给每个系统调用分配一个唯一的号码。
文件中每一行的格式如下:
#define __NR_name NNN
其中,name用系统调用名称代替,而NNN则是该系统调用对应的号码
应该将新的系统调用名称加到清单的最后,并给它分配号码序列中下一个可用的系统调用号。我们的系统调用如下:
#define __NR_sync_file_vange 337
#define __NR_tee 338
#define __NR_vmsplice 339
#define __NR_syscalls 340
最后一行NR_syscalls 说明内核自身的系统调用号已经使用到339,共有340个(从0开始)。我们新添加的系统调用应该加到最后,并修改系统调用总数。修改如下:
#define __NR_sync_file_vange 337
#define __NR_tee 338
#define __NR_vmsplice 339
#define __NR_mysyscall 340 /*这是我们自己添加的系统调用*/
#define __NR_syscalls 341
第二个要修改的文件是:
/usr/src/linux—2.6.37.1/arch/x86/kernel/syscall_table_32.s
在2.6版本以前,需要修改的是/usr/src/linux/arch/i386/kernel/entry.s,在该文件下有sys_call_table,直接进行修改。
在2.6版本后,entry.s文件中的sys_call_table独立出来,我们可以在该文件下看到类似于#include "syscall_table.s"字样,说明我们正真需要修改syscall_table.s。 在syscall_table.s中有类似如下的清单:
.long sys_name
该清单用来对sys_call_table[]数组进行初始化。该数组包含指向内核中每个系统调用的指针。这样就在数组中增加了新的内核函数的指针。
我们在清单上与系统调用号相对应的位置添加一行:
.long sys_mysyscall
必须注意添加的行的位置,否则容易造成内核编译的失败。
首先需要清空以前的编译信息,make mrproper命令清除旧的配置等文件,避免编译内核时生成的文件不一致。
make menuconfig命令生成配置清单文件。
make -j5命令编译新内核,这个编译过程非常长,需要1~2个小时。j5参数代表使用5个线程同时编译,这样速度会点。
make bzImage命令生成系统视图。
make modules命令生成模块。
make modules_install命令安装模块。
mkinitramfs -o /boot/initrd.img-mycall命令生成系统镜像文件。
make install命令安装新的系统。
update-grub命令更新启动程序grub,使启动界面上出现新安装的系统。
测试代码如下,使用syscall函数调用自己编写的系统功能函数:
- #include <stdio.h>
- void main()
- {
- syscall(341, "./2.c", "./newfile.c");
- }
注意:syscall()中的第一个参数341是系统调用号,第二个参数是源文件,第三个参数是目标文件。
至此这个系统功能调用就完成了,这次实验有个小插曲,就是我同时在帮一位信安的朋友一起做这个课设,所以文档很多地方都是直接copy的,结果我是先把他的文档完成了再做我自己的文档,结果没有留意到一个地方写了他的名字,结果文档被老师收走了,现在我只能听天由命了,希望老师不要发现这个名字的问题,要不然我就会悲剧的被指名抄袭~~~我冤不冤~~有木有~~有木有~~
本文出自 “菜鸟浮出水” 博客,请务必保留此出处http://rangercyh.blog.51cto.com/1444712/521200