荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: igmp (igmp), 信区: Security
标  题: 论文集(二)
发信站: 荔园晨风BBS站 (Wed Jun 27 23:12:06 2001), 转信

                         Linux系统调用与实例分析
一.    系统调用的基本概念
通常,在OS的核心中都设置了一组用于实现各种系统功能的子程序(过程),并将
它们提供给用户调用。每当用户在程序中需要OS提供某种服务时,便可利用一条系
统调用命令,去调用系统过程。它一般运行在核心态;通过软中断进入;返回时通
常需要重新调度(因此不一定直接返回到调用过程)。
系统调用是沟通用户(应用程序)和操作系统内核的桥梁。
二.    Linux的系统调用
        Linux系统调用的流程非常简单,它由0x80号软中断进入系统调用入口,通过使用
系统调用表保存系统调用服务函数的入口地址来实现。
2.1      Linux系统调用的数据结构
在文件"arch/i386/entry.S"中定义了系统调用表(sys_call_table),该表保存了
Linux的所有基于Intel x86系列体系结构的计算机的166个系统调用入口地址(其
中3个保留,Linux开辟的系统调用表可容纳256项),其中每项都被说明成 long型
。下面是其中几项:

.data
ENTRY(sys_call_table)
        .long SYMBOL_NAME(sys_setup)            /* 0 */
        .long SYMBOL_NAME(sys_exit)
        .long SYMBOL_NAME(sys_fork)
        ……
        ……
        .long SYMBOL_NAME(sys_nanosleep)   /* 162 */
        .long SYMBOL_NAME(sys_mremap)
        .long 0,0
        .long SYMBOL_NAME(sys_vm86)
        .space (NR_syscalls-166)*4


NR_syscalls是在"include/linux/sys.h"文件中定义的宏,其值为256,表示x86微
机上最多可容纳的系统调用个数。

在文件"include/asm-i386/ptrace.h"中定义了一种寄存器帧结构
struct pt_regs {
        long ebx;
        long ecx;
        long edx;
        long esi;
        long edi;
        long ebp;
        long eax;
        unsigned short ds, __dsu;
        unsigned short es, __esu;
        unsigned short fs, __fsu;
        unsigned short gs, __gsu;
        long orig_eax;
        long eip;
        unsigned short cs, __csu;
        long eflags;
        long esp;
        unsigned short ss, __ssu;
};

该帧结构定义了各寄存器在系统调用时保存现场的堆栈结构。

2.2 设置0x80 软中断
Linux的系统调用由0x80号软中断进入,中断向量表的初始化在系统启动时进行,各
种trap入口start_kernel()函数(init/main.c)中通过调用trap_init()(
arch/i386/kernel/traps.c)被设置,其中set_system_gate(0x80,
&system_call)设置了0x80号软中断。
"set_system_gate()"是一个宏,它在"include/asm-i386/system.h"中被定义。调
用该宏,将使addr地址值置入gate_addr中的地址值所指向的内存单元中,以上过
程,使中断向量描述表中的第128项(即16进制第80项)保存了0x80号中断的中断
服务程序,即system_call的入口地址。

2.3系统调用入口
在头文件"include/asm-i386/unistd.h"中,定义了一系列的与系统调用有关的宏
,包括系统调用序号,如:
#define __NR_setup        0
#define __NR_exit                 1
#define __NR_fork                 2
#define __NR_read                 3

还定义了设置系统调用入口的宏,_syscallX(),其中X表示系统调用的参数个数,
Linux定义的各种系统调用的参数个数不超过5个,因此,在该文件中,共定义了6
个宏(_syscall0(type,name),……,_syscall5(type,name,type1,arg1,……,
type5,arg5)。

下面以_syscall2()为例:
#define _syscall2(type,name,type1,arg1,type2,arg2) \
type name(type1 arg1,type2 arg2) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \
if (__res >= 0) \
        return (type) __res; \
errno = -__res; \
return -1; \
}
该宏的第一个参数是一类类型参数,它指明系统调用返回值的类型,第二个参数指
明系统调用的名称。参数列表中若还有参数,typei,argi(i =1,2)分别表示第i
 个参数的类型和第i个参数
该宏的主体部分是一内联汇编,在内联汇编中只有一条扩展汇编指令,即"int
$0x80",该语句两个冒号后的语句设置寄存器。
其中第一个冒号后的语句指明返回参数(即__res)使用eax寄存器。
第二个冒号后面""0" (__NR_##name),"将参数name与"__NR_"串接起来,形成的标
志符存入eax寄存器,作为区别系统调用类型的唯一参数,例如设置name为
"clone",那么,编译时将把"__NR_"与之串接,被视为标志符"__NR_clone",由于
在文件"include/asm-i386/unistd.h"中已定义其为120,那么,传给eax的值将为
120。
后面的语句将参数arg1,arg2分别传给寄存器ebx和ecx,在"_syscallX"宏中,约定
五个参数分别与五个寄存器对应:
arg1值存入寄存器ebx;
arg2值存入寄存器ecx;
arg3值存入寄存器edx;
arg4值存入寄存器esi;
arg5值存入寄存器edi;
在该宏的最后,判断返回值"__res"是否合法,若为负数,表明在系统调用中出错
,将其相反数作为出错号赋给全局变量"errno",并返回-1,否则返回__res的值。

该宏展开得到一个与系统调用同名的函数,
该函数中的内联汇编指令int $0x80使程序转入系统调用。

2.4 系统调用过程
system_call是在汇编语言文件"/arch/i386/entry.S"中定义的入口,在Linux中,
所有的系统调用都是通过中断"int &0x80"语句来实现的,因而,system_call是所
有系统调用的入口。
    下面解释关于它的一些重要指令,以清晰它的流程:
1.     首先,调用宏过程"SAVE_ALL"保存现有通用寄存器值,寄存器值的压栈不但可
以保存系统调用前个寄存器的数据,而且提供了一种传递参数的方法,堆栈中的结
构,与该过程所要传递的pt_regs结构类型的参数结构一致。在该宏中,还使ds和
es指向内核的数据段,使fs指向用户的数据段,使进程进入核心态。
2.     语句"cmpl $(NR_syscalls),%eax"比较NR_syscalls与eax的大小,如果eax大
于或等于NR_syscalls (即256),表明指定的系统调用不合法,"jae
ret_from_sys_call"使系统调用直接返回。
3.     system_call接下去执行语句
    movl  SYMBOL_NAME(sys_call_table)(,%eax,4),%eax
    该语句以    sys_call_table为基地址,eax寄存器中的内容(即系统调用的序号
)乘以4为偏移量(因为long型为32位即4字节),即得到所需调用的系统调用函数
的入口地址,将其存入寄存器eax。

 接着判断寄存器eax值是否
为0,若是,表明出错,直接返回。
4.      执行movl SYMBOL_NAME(current_set),%ebx 语句,ebx寄存器就得到了指向当
前进程的指针。
        movl %db6,%edx
                movl %edx,dbgreg6(%ebx)
以上两条语句用来保存当前调试信息,在进程控制块task_struct结构中,第8项是
debugreg[8],用来指示硬件调试信息。在entry.S中,定义了一系列宏作为偏移量
,用来得到当前进程的信息,它们是:
state                   = 0
counter                 = 4
priority                        = 8
signal                  = 12
blocked                 = 16
flags                   = 20
dbgreg6                 = 52
dbgreg7                 = 56
exec_domain             = 60
这样,在当前进程的task_struct结构中,保存了当前的调试信息。
5.      语句"testb $0x20,flags(%ebx)"检测当前进程是否正跟踪系统调用,如果不是

话,直接调用所选系统调用函数,执行完后直接返回。如当前进程正处于跟踪系统
调用状态,则调用函数syscall_trace()(在文件"arch/i386/kernel/ptrace.c"中
定义),使当前进程状态转为TASK_STOPPED,将该进程转入睡眠状态,然后从压入
寄存器的堆栈中重新找到原来的eax值,再重新设置系统调用函数的偏移量,调用
实现相应系统调用的函数。执行完后再次调用syscall_trace()。
6.      函数返回以后,流程进入ret_from_sys_call,该过程内处理一些系统调用返回
前应该处理的事情,如检测bottom half缓冲区,判断CPU是否需要重新调度等,之
后,系统调用返回。
全局变量intr_count,它虽然不是信号量,但也部分的具有了信号量的作用,它在
系统处理bottom_half时增1,则其为非零,表示已有进程进入bottom_half。
语句"cmpl $0,SYMBOL_NAME(intr_count)"就是进行上述判断,若非零,处理
bottom half 缓冲区。("jne handle_bottom_half")。
handle_bottom_half很简单,包含下列语句:
    pushl $ret_from_intr
    jmp SYMBOL_NAME(do_bottom_half)
    ALIGN
它将ret_from_init的地址压入堆栈,然后转入do_bottom_half程序中,
处理那些被激活的中断程序。然后返回。
下面两条语句判断CPU是否需要重新调度:
        cmpl $0,SYMBOL_NAME(need_resched)
                jne reschedule
其中,need_resched是一全程量,它置位,表示CPU需要重新调度,程序转向过程
reschedule,先将ret_from_sys_call的地址压入堆栈,然后再跳转到进程调度程
序的入口-SYMBOL_NAME(schedule),完成进程调度并将need_resched重新置零。
如果有信号(signals),则执行signal_return,它根据当前模式是否是虚拟
8086模式。若是,则先调用函数save_v86_state()(定义在文件
/linux/arch/i386/kernel/vm86.c中),把当前的vm86_regs结构中的信息全部保
存起来,再执行函数do_signal()(定义在文件/linux/arch/i386/kernel/signal.
c中),根据各种处理信号来设置当前任务的状态。若不是虚拟8086模式,则直接执
行do_siganl()函数即可。
7.      调用宏"RESTORE_ALL"使进程离开核心态,恢复各寄存器值并返回。
三.系统调用实例分析:sys_exit
        当用户发出一个退出系统命令的时候,Linux就调用系统调用sys_exit。系统调用
sys_exit的主要作用是终止当前正在运行的所有用户的应用程序,保存当前帐号的
各种信息,逐步退出支撑Linux操作系统运行的系统子模块和子系统。这些系统子
模块和子系统是:
1.     删除当前任务的实定时器。
2.      删除信号队列(destroye semaphore arrays),释放信号撤消结构(free
semaphores undo structures)
3. 清空当前任务的kerneld队列。
4. 退出内存管理。
5.      关闭打开的文件。
6.      退出文件系统。
7.      释放当前任务的所有信号(signal)。
8.      退出线程(thread)。
3.1 sys_exit的初始化
        在文件include/asm-i386/unistd.h中,可以找到很多包括sys_exit在内系统调用
的宏定义。sys_exit的宏定义如下:
static inline _syscall1(int,_exit,int,exitcode)

使用第三部分"展开宏定义"的方法将其展开以后,它变成代码如下:
        int _exit(int exitcode)
{               long __res;
__asm__ volatile ("int $0x80"
                                        : "=a" (__res)
: "0" (__NR_exit), "b" ((long)(exitcode)));
if (__res >= 0 )
                                                        return (int) __res;

errno = -__res;
        return -1;
        }
可见,sys_exit是一个只带一个参数exitcode的函数,该参数就是系统退出(
sys_exit)的退出码。_exit通过调用0x80号软中断带参数:系统调用号(
_NR_exit,即在sys_call_table中的偏移量)和退出码(exitcode)的方法,来达到
调用sys_exit的目的。
3.2 系统调用sys_exit的流程
系统调用sys_exit的处理函数定义在文件kernel/exit.c中。
        sys_exit的函数体很简单,只是调用了函数do_exit:
                do_exit((error_code&0xff)<<8);
(error_code&0xff)<<8的作用就是将error_code的低8位移到高8位中,低8位用
0填补,此数将作为参数传给函数do_exit。
        下面来看看函数do_exit是怎样做的。
        首先,do_exit判断表示是否有正在处理的中断服务全局变量intr_count是否为1
,如果为1,表明当前还有中断正在处理,执行intr_count=0,停止处理中断。
        接着,do_exit要为关闭系统,逐步退出一些运行操作系统所必须的模块。
1.     执行函数acct_process()。(该函数定义在kernel/sys.c中)
再该函数中,保存当前帐号的各种状态。
2.      把当前任务的标记记为退出:
current->flags |= PF_EXITING;
向所有的进程宣布,现在系统要退出了,以便一些调度处理函数得知这一消息(通
过检测该标记)。
    3. 删除当前的实定时器:
        del_timer(&current->real_timer);
4. 删除信号队列(destroye semaphore arrays),释放信号撤消结构(free
semaphores undo structures)
Sem_exit();(定义在ipc/sem.c文件中)
在该函数中,增加调整值(semval)给信号,再释放撤消结构(free undo
structures)。由于某些信号(semaphore)可能已经过时或无效了,直到信号数
组(semaphore array)被删除了以后,撤消结构(undo structures)才被释放。具
体做法如下:
a.     如果当前进程正在睡眠状况(需要一信号(semaphore)来唤醒),将进程当前
指向所需信号(semaphore)的指针置空。
b.     在当前的信号撤消链表(struct sem_undo)里查找a中提到的那信号(
semaphore),找到以后,调整该信号(已在信号撤消链表中注册过的)的内容。

c.     由于有可能有一个队列的进程在等该信号,故须更新整个操作系统的数组。
5.      清空当前任务的kerneld队列:kerneld_exit();
6.      退出内存管理系统:
__exit_mm(current);(函数定义在kernel/exit.c文件中)
具体做法如下:
a.     将cache,tlb,page里的内容全部回写。
b.     退出内存影射。
c.     释放页表(page table)。
7.      把当前任务所打开的文件都关闭,释放文件指针。
__exit_files(current);
8.      退出文件系统:
__exit_fs(current);
    9. 释放当前任务的所有信号(signal):
__exit_sighand(current);
    10.释放当前线程数据:
__exit_thread();
    11.  向外广播退出:
 exit_notify();
        先将当前任务的状态(state)设为TASK_ZOMBIE,退出码为code(即传进
来的参数)。再调用exit_notify()函数。在exit_notify()中,
        作为我们执行上述过程后,退出系统的结果,我们的进程组们应该变成孤立的了
。如果它们已经停止工作了,给它们发"SIGHUP"和"SIGCONT"信号。接着,通知它
们的父进程,本进程已经被kill了。
            接下去是一循环,该循环主要做以下两件事:
         使初始进程(init)继承所有子进程。
        检查是否有遗漏:还有进程组不是孤立的。若有,处理方法同上。
        最后,调用函数disassciate_ctty(int)(定义在 drivers/char/tty_io.
c中)。只有当参数是1时,才是被exit_notify()调用的。在该函数中,将当前
tty进程组kill掉,所有进程对应的tty成员赋NULL。
   12.将当前任务的用户数目减一:
        (*current->exec_domain->use_count)--;
        (*current->binfmt->use_count)--;
   13.继续调度:
        schedule();

    在调用完sys_exit后,先判断返回值_res是否为非负数。若是,则说明该次调
用是成功的,返回_res即可。若_res为负数,则说明该次调用过程中存在着一个或
一些错误,将错误值赋给一全局变量errno:
        errno = -_res;
再返回-1,指明有错误存在,可以让专门处理这些错误的函数根据errno的值知道
这次调用到底出错在什么地方,是哪种类型的错,以便进行错误处理。





本文分析的Linux kernel版本为2.0.34
目前最新版本为2.0.36,2.1.133,2.2.0

  参考资料
(1)   《计算机操作系统》  汤子瀛
(2)   《操作系统讲义》  李善平
(3)   Linux Kernel Hackers' Guide

--

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.43.46]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店