Linux系统调用

    1. 方法之四:以功能为中心,各个击破 

        从功能上看,整个Linux系统可看作有一下几个部分组成:
      1. 进程管理机制部分;
      2. 内存管理机制部分; 
      3. 文件系统部分; 
      4. 硬件驱动部分; 
      5. 系统调用部分等;

在这五个功能部件中,系统调用是用户程序或操作调用核心所提供的功能的接口;也是分析Linux内核源码几个很好的入口点之一。

 http://www.yesky.com/20010813/192117_3.shtml

 

与系统调用相关的内容主要有:系统调用总控程序,系统调用向量表sys_call_table,以及各系统调用服务程序。

    1. 保护模式下的初始化过程中,设置并初始化idt,共256个入口,服务程序均为ignore_int, 该服务程序仅打印“Unknown interruptn”。(源码参见/Arch/i386/KERNEL/head.S文件;相关内容可参见 保护模式下的初始化部分)
    2. 在系统初始化完成后运行的第一个内核程序start_kernel中,通过调用 trap_init函数,把各自陷和中断服务程序的入口地址设置到 idt 表中;同时,此函数还通过调用函数set_system_gate 把系统调用总控程序的入口地址挂在中断0x80上。其中:
  • start_kernel的原型为void __init start_kernel(void) ,其源码在文件 init/main.c中;
  • trap_init函数的原型为void __init trap_init(void),定义在arch/i386/kernel/traps.c 中
  • 函数set_system_gate同样定义在arch/i386/kernel/traps.c 中,调用原型为set_system_gate(SYSCALL_VECTOR,&system_call);
  • 其中,SYSCALL_VECTOR是定义在 linux/arch/i386/kernel/irq.h中的一个常量0x80;
  • 而 system_call 即为系统调用总控程序的入口地址;中断总控程序用汇编语言定义在arch/i386/kernel/entry.S中。

 

  • 系统调用向量表sys_call_table, 是一个含有NR_syscalls=256个单元的数组。它的每个单元存放着一个系统调用服务程序的入口地址。该数组定义在/arch/i386/kernel/entry.S中;而NR_syscalls则是一个等于256的宏,定义在include/linux/sys.h中。
  • 各系统调用服务程序则分别定义在各个模块的相应文件中;例如asmlinkage int sys_time(int * tloc)就定义在kerneltime.c中;另外,在kernelsys.c中也有不少服务程序;

 

系统调用个数:

从2.4的190个到2.6的300多个

 

 

eax是寄存器

可见,系统调用的进入课分为“用户程序 系统调用总控程序”“系统调用总控程序各个服务程序”两部分

 

III、系统调用总控程序(system_call)

系统调用总控程序(system_call)可参见arch/i386/kernel/entry.S其执行流程如下图:

 

由以上的分析可知,增加系统调用由于下两种方法 

i.编一个新的服务例程,将它的入口地址加入到sys_call_table的某一项,只要该项的原服务例程是sys_ni_syscall,并且是sys_ni_syscall的作用属于第三种的项,也即Nr 137, Nr 188, Nr 189

 

我觉得,推荐下面这种:

ii.直接增加:

编一个新的服务例程; 

在sys_call_table中添加一个新项, 并把的新增加的服务例程的入口地址加到sys_call_table表中的新项中; 

把增加的 sys_call_table 表项所对应的向量, 在include/asm-386/unistd.h 中进行必要申明,以供用户进程和其他系统进程查询或调用。 

由于在标准的c语言库中没有新系统调用的承接段,所以,在测试程序中,除了要#include ,还要申明如下 _syscall1(int,additionSysCall,int, num)。

 

 下面将对第ii种情况列举一个我曾经实现过了的一个增加系统调用的实例: 

1.)在kernel/sys.c中增加新的系统服务例程如下: 

asmlinkage int sys_addtotal(int numdata) 

{ 

int i=0,enddata=0; 

while(i<=numdata) 

enddata+=i++; 

return enddata; 

} 

该函数有一个 int 型入口参数 numdata , 并返回从 0 到 numdata 的累加值; 当然也可以把系统服务例程放在一个自己定义的文件或其他文件中,只是要在相应文件中作必要的说明; 

2.)把 asmlinkage int sys_addtotal( int) 的入口地址加到sys_call_table表中: 

arch/i386/kernel/entry.S 中的最后几行源代码修改前为: 

... ... 

.long SYMBOL_NAME(sys_sendfile) 

.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */ 

.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */ 

.long SYMBOL_NAME(sys_vfork) /* 190 */ 

.rept NR_syscalls-190 

.long SYMBOL_NAME(sys_ni_syscall) 

.endr 

修改后为: ... ... 

.long SYMBOL_NAME(sys_sendfile) 

.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */ 

.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */ 

.long SYMBOL_NAME(sys_vfork) /* 190 */ 

/* add by I */ 

.long SYMBOL_NAME(sys_addtotal) 

.rept NR_syscalls-191 

.long SYMBOL_NAME(sys_ni_syscall) 

.endr 

3.) 把增加的 sys_call_table 表项所对应的向量,在include/asm-386/unistd.h 中进行必要申明,以供用户进程和其他系统进程查询或调用: 

 

增加后的部分 /usr/src/linux/include/asm-386/unistd.h 文件如下: 

... ... 

#define __NR_sendfile 187 

#define __NR_getpmsg 188 

#define __NR_putpmsg 189 

#define __NR_vfork 190 

/* add by I */ 

#define __NR_addtotal 191 

4.测试程序(test.c)如下: 

#include <unistd.h>

#include <stdio.h>

_syscall1(int,addtotal,int, num) 

main() 

{ 

int i,j; 

do 

printf("Please input a numbern"); 

while(scanf("%d",&i)==EOF); 

if((j=addtotal(i))==-1) 

printf("Error occurred in syscall-addtotal();n"); 

printf("Total from 0 to %d is %d n",i,j); 

} 

对修改后的新的内核进行编译,并引导它作为新的操作系统,运行几个程序后可以发现一切正常;在新的系统下对测试程序进行编译(*注:由于原内核并未提供此系统调用,所以只有在编译后的新内核下,此测试程序才能可能被编译通过),运行情况如下: 

$gcc -o test test.c 

$./test 

Please input a number 

36 

Total from 0 to 36 is 666 

 

在上面的论述中,一共列举了两个内核分析的入口、和三种分析源码的方法:

以程序流程为线索,一线串珠;
以数据结构为基点,触类旁通;
以功能为中心,各个击破。
三种方法各有特点,适合于分析不同部分的代码: 

以程序流程为线索,适合于分析系统的初始化过程:系统引导、实模式下的初始化、保护模式下的初始化三个部分,和分析应用程序的执行流程:从程序的装载,到运行,一直到程序的退出。而流程图则是这种分析方法最合适的表达工具。 

以数据结构为基点、触类旁通,这种方法是分析操作系统源码最常用的和最主要的方法。对分析进程管理,设备管理,内存管理等等都是很有效的。 

以功能为中心、各个击破,是把整个系统分成几个相对独立的功能模块,然后分别对各个功能进行分析。这样带来的一个好处就是,每次只以一个功能为中心,涉及到其他部分的内容,可以看作是其它功能提供的服务,而无需急着追究这种服务的实现细节;这样,在很大程度上减轻了分析的复杂度。 

三种方法,各有其长,只要合理的综合运用这些方法,相信对减轻分析的复杂度还是有所帮组的。

 

 

以下是一个360doc的文档,不能拷贝,只能截图。

 

 

 说一下我的理解。Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。库子程序(除了其中系统调用的部分)是回到了用户态的,只有系统调用是内核态的。内核态、用户态的知识,在另一篇文章中再详细讨论。

 

 

 

 

 

注意标准C库的作用:

 

注意:

所以,一般不会直接调用系统调用的,都是调用标准C库,或者更加抽象的库子程序。

 

虽然理论上,

对于简单的操作,我们可以直接调用系统调用来访问资源,如“人”,对于复杂操作,我们借助于库函数来实现,如“仁”。显然,这样的库函数依据不同的标准也可以有不同的实现版本,如ISO C 标准库,POSIX标准库等。

但是实际上,

 

谁也不想去写复杂的系统调用映射,处理寄存器操作,等等。

 

如果真的想调用,有下面三种方法:

http://www.linuxidc.com/Linux/2014-12/110238.htm

 

系统调用(System Call)是操作系统为在用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互提供的一组接口。当用户进程需要发生系统调用时,CPU 通过软中断切换到内核态开始执行内核系统调用函数。下面介绍Linux 下三种发生系统调用的方法:

 

1. 通过 glibc 提供的库函数

glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库,即运行时库。glibc 为程序员提供丰富的 API(Application Programming Interface),除了例如字符串处理、数学运算等用户态服务之外,最重要的是封装了操作系统提供的系统服务,即系统调用的封装。

 

那么glibc提供的系统调用API与内核特定的系统调用之间的关系是什么呢?

通常情况,每个特定的系统调用对应了至少一个 glibc 封装的库函数,如系统提供的打开文件系统调用 sys_open 对应的是 glibc 中的 open 函数;

其次,glibc 一个单独的 API 可能调用多个系统调用,如 glibc 提供的 printf 函数就会调用如 sys_open、sys_mmap、sys_write、sys_close 等等系统调用;

另外,多个 API 也可能只对应同一个系统调用,如glibc 下实现的 malloccallocfree 等函数用来分配和释放内存,都利用了内核的 sys_brk 的系统调用。

代码如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>

int main()
{
        int rc;

        rc = chmod("/etc/passwd", 0444);
        if (rc == -1)
                fprintf(stderr, "chmod failed, errno = %d\n", errno);
        else
                printf("chmod success!\n");
        return 0;
}

 

 

2. 使用 syscall 直接调用

有点不足是,如果 glibc 没有封装某个内核提供的系统调用时,我就没办法通过上面的方法来调用该系统调用。如我自己通过编译内核增加了一个系统调用,这时 glibc 不可能有你新增系统调用的封装 API,此时我们可以利用 glibc 提供的syscall 函数直接调用。该函数定义在 unistd.h 头文件中,函数原型如下:

long int syscall (long int sysno, ...)

sysno 是系统调用号,每个系统调用都有唯一的系统调用号来标识。在 sys/syscall.h 中有所有可能的系统调用号的宏定义。
... 为剩余可变长的参数,为系统调用所带的参数,根据系统调用的不同,可带0
~5个不等的参数,如果超过特定系统调用能带的参数,多余的参数被忽略。
返回值 该函数返回值为特定系统调用的返回值,在系统调用成功之后你可以将该返回值转化为特定的类型,如果系统调用失败则返回
-1,错误代码存放在 errno 中。

代码如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

int main()
{
        int rc;
        rc = syscall(SYS_chmod, "/etc/passwd", 0444);

        if (rc == -1)
                fprintf(stderr, "chmod failed, errno = %d\n", errno);
        else
                printf("chmod succeess!\n");
        return 0;
}

 

 

3. 通过 int 指令陷入

如果我们知道系统调用的整个过程的话,应该就能知道用户态程序通过软中断指令int 0x80 来陷入内核态(在Intel Pentium II 又引入了sysenter指令),参数的传递是通过寄存器,eax 传递的是系统调用号,ebx、ecx、edx、esi和edi 来依次传递最多五个参数,当系统调用返回时,返回值存放在 eax 中

仍然以上面的修改文件属性为例,将调用系统调用那段写成内联汇编代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <errno.h>

int main()
{
        long rc;
        char *file_name = "/etc/passwd";
        unsigned short mode = 0444;

        asm(
                "int $0x80"
                : "=a" (rc)
                : "0" (SYS_chmod), "b" ((long)file_name), "c" ((long)mode)
        );

        if ((unsigned long)rc >= (unsigned long)-132) {
                errno = -rc;
                rc = -1;
        }

        if (rc == -1)
                fprintf(stderr, "chmode failed, errno = %d\n", errno);
        else
                printf("success!\n");

        return 0;
}

如果 eax 寄存器存放的返回值(存放在变量 rc 中)在 -1~-132 之间,就必须要解释为出错码(在/usr/include/asm-generic/errno.h 文件中定义的最大出错码为 132),这时,将错误码写入 errno 中,置系统调用返回值为 -1;否则返回的是 eax 中的值。

上面程序在 32位Linux下以普通用户权限编译运行结果与前面两个相同!

 

Linux系统调用列表(其实指的是glibc库函数的列表)

http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part1/appendix.html

这个列表以man pages第2节,即系统调用节为蓝本

 

其中有一些函数的作用完全相同,只是参数不同。(可能很多熟悉C++朋友马上就能联想起函数重载,但是别忘了Linux核心是用C语言写的,所以只能取成不同的函数名)。还有一些函数已经过时,被新的更好的函数所代替了(gcc在链接这些函数时会发出警告),但因为兼容的原因还保留着,这些函数我会在前面标上“*”号以示区别。

 

(各给一个示例,具体的看原文吧,对加强对Linux系统的理解,很有好处!)

一、进程控制:

fork 创建一个新进程

clone 按指定条件创建子进程

exit 中止进程

_exit 立即中止当前进程

 

二、文件系统控制

1、文件读写操作

fcntl 文件控制

open 打开文件

 

2、文件系统操作

access 确定文件的可存取性

link 创建链接

 

三、系统控制

ioctl I/O总控制函数

 

四、内存管理

brk 改变数据段空间的分配(malloc, calloc 等库函数,都是用的这个系统调用)

mmap 映射虚拟内存页

 

五、网络管理

getdomainname 取域名

 

六、socket控制

socket 建立socket

 

七、用户管理

getuid 获取用户标识号

 

八、进程间通信

ipc 进程间通信总控制调用

 

1、信号

sigaction 设置对指定信号的处理方法

signal 参见signal

kill 向进程或进程组发信号

 

2、消息

msgctl 消息控制操作

 

3、管道

pipe 创建管道

 

4、信号量

semctl 信号量控制

 

5、共享内存

shmctl 控制共享内存

 

参考资料

Linux man pages

Advanced Programming in the UNIX Environment, W. Richard Stevens, 1993

 

 

 

posted @ 2017-01-08 13:49  blcblc  阅读(1634)  评论(0编辑  收藏  举报