系统调用是用户程序和linux内核交互的接口,linux的系统
调用有下面三种方式:
在x86与x86_64的系统中,都可以使用int $0x80指令来执行系统调用,参数使用如下:
x86_64引入了一个新指令syscall来执行系统调用,参数使用如下:
正常调用库函数( man 3 execve ),传参方式见 x86 调用约定
查看调用号:头文件 /usr/include/asm/unistd.h 内容如下,所以32位和64位的调用号可以分别在unistd_32.h和unistd_64.h中找到
查看参数: man 2 execve
以 execve 为例, man 2 execve 查看其接口如下: int execve(const char *pathname, char *const argv[], char *const envp[])
下面汇编实现了 execve("/bin/sh", 0, 0) :
32位:
64位:
作者 雷镇 本文是Linux系统调用系列文章的第一篇 对Linux系统调用的定义 基本原理 使用方法和注意事项大概作了一个介绍 以便读者对Linux系统调用建立一个大致的印象 什么是系统调用?Linux内核中设置了一组用于实现各种系统功能的子程序 称为系统调用 用户可以通过系统调用命令在自己的应用程序中调用它们 从某种角度来看 系统调用和普通的函数调用非常相似 区别仅仅在于 系统调用由 *** 作系统核心提供 运行于核心态 而普通的函数调用由函数库或用户自己提供 运行于用户态 二者在使用方式上也有相似之处 在下面将会提到 随Linux核心还提供了一些C语言函数库 这些库对系统调用进行了一些包装和扩展 因为这些库函数与系统调用的关系非常紧密 所以习惯上把这些函数也称为系统调用 Linux *** 有多少个系统调用?这个问题可不太好回答 就算让Linus Torvaldz本人也不见得一下子就能说清楚 在 版内核中 狭义上的系统调用共有 个 你可以在 内核源码目录>/include/a *** i /unistd h中找到它们的原本 也可以通过命令 man syscalls 察看它们的目录(man pages的版本一般比较老 可能有很多最新的调用都没有包含在内) 广义上的系统调用 也就是以库函数的形式实现的那些 它们的个数从来没有人统计过 这是一件吃力不讨好的活 新内核不断地在推出 每一个新内核中函数数目的变化根本就没有人在乎 至少连内核的修改者本人都不在乎 因为他们从来没有发布过一个此类的声明 随本文一起有一份经过整理的列表 它不可能非常全面 但常见的系统调用基本都已经包含在内 那里面只有不多的一部分是你平时用得到的 本专栏将会有选择的对它们进行介绍 为什么要用系统调用?实际上 很多已经被我们习以为常的C语言标准函数 在Linux平台上的实现都是靠系统调用完成的 所以如果想对系统底层的原理作深入的了解 掌握各种系统调用是初步的要求 进一步 若想成为一名Linux下编程高手 也就是我们常说的Hacker 其标志之一也是能对各种系统调用有透彻的了解 即使除去上面的原因 在平常的编程中你也会发现 在很多情况下 系统调用是实现你的想法的简洁有效的途径 所以有可能的话应该尽量多掌握一些系统调用 这会对你的程序设计过程带来意想不到的帮助 系统调用是怎么工作的?一般的 进程是不能访问内核的 它不能访问内核所占内存空间也不能调用内核函数 CPU硬件决定了这些(这就是为什么它被称作 保护模式 ) 系统调用是这些规则的一个例外 其原理是进程先用适当的值填充寄存器 然后调用一个特殊的指令 这个指令会跳到一个事先定义的内核中的一个位置(当然 这个位置是用户进程可读但是不可写的) 在Intel CPU中 这个由中断 x 实现 硬件知道一旦你跳到这个位置 你就不是在限制模式下运行的用户 而是作为 *** 作系统的内核 所以你就可以为所欲为 进程可以跳转到的内核位置叫做sysem_call 这个过程检查系统调用号 这个号码告诉内核进程请求哪种服务 然后 它查看系统调用表(sys_call_table)找到所调用的内核函数入口地址 接着 就调用函数 等返回后 做一些系统检查 最后返回到进程(或到其他进程 如果这个进程时间用尽) 如果你希望读这段代码 它在 内核源码目录>/kernel/entry S Entry(system_call)的下一行 如何使用系统调用?先来看一个例子 #include /*定义宏_syscall */#include /*定义类型time_t*/_syscall (time_t time time_t * tloc) /*宏 展开后得到time()函数的原型*/main(){ time_t the_timethe_time=time((time_t *) )/*调用time系统调用*/ printf( The time is %ld the_time)} 系统调用time返回从格林尼治时间 年 月 日 : 开始到现在的秒数 这是最标准的系统调用的形式 宏_syscall ()展开来得到一个函数原型 稍后我会作详细解释 但事实上 如果把程序改成下面的样子 程序也可以运行得同样的结果 #includemain(){ time_t the_timethe_time=time((time_t *) )/*调用time系统调用*/ printf( The time is %ld the_time)} 这是因为在time h中实际上已经用库函数的形式实现了time这个系统调用 替我们省掉了调用_syscall 宏展开得到函数原型这一步 大多数系统调用都在各种C语言函数库中有所实现 所以在一般情况下 我们都可以像调用普通的库函数那样调用系统调用 只在极个别的情况下 我们才有机会用到_syscall*()这几个宏 _syscall*()是什么?在unistd h里定义了 个宏 分别是 _syscall (type name)_syscall (type name type arg )_syscall (type name type arg type arg )_syscall (type name type arg type arg type arg )_syscall (type name type arg type arg type arg type arg )_syscall (type name type arg type arg type arg type arg type arg )_syscall (type name type arg type arg type arg type arg type arg type arg ) 它们看起来似乎不太像宏 但其实质和 #define MAXSIZE 里面的MAXSIZE没有任何区别 它们的作用是形成相应的系统调用函数原型 供我们在程序中调用 我们很容易就能发现规律 _syscall后面的数字和typeN argN的数目一样多 事实上 _syscall后面跟的数字指明了展开后形成函数的参数的个数 让我们看一个实例 就是刚刚用过的time系统调用 _syscall (time_t time time_t * tloc) 展开后的情形是这样 time_t time(time_t * tloc){ long __res__a *** __ volatile( int $ x : =a (__res) : ( ) b ((long)(tloc)))do { if ((unsigned long)(__res) >= (unsigned long)( )) { errno = (__res)__res = } return (time_t) (__res)} while ( ) } 可以看出 _syscall (time_t time time_t * tloc)展开成一个名为time的函数 原参数time_t就是函数的返回类型 原参数time_t *和tloc分别构成新函数的参数 事实上 程序中用到的time函数的原型就是它 errno是什么?为防止和正常的返回值混淆 系统调用并不直接返回错误码 而是将错误码放入一个名为errno的全局变量中 如果一个系统调用失败 你可以读出errno的值来确定问题所在 errno不同数值所代表的错误消息定义在errno h中 你也可以通过命令 man errno 来察看它们 需要注意的是 errno的值只在函数发生错误时设置 如果函数不发生错误 errno的值就无定义 并不会被置为 另外 在处理errno前最好先把它的值存入另一个变量 因为在错误处理过程中 即使像printf()这样的函数出错时也会改变errno的值 系统调用兼容性好吗?很遗憾 答案是 不好 但这决不意味着你的程序会三天两头的导致系统崩溃 因为系统调用是Linux的内核提供的 所以它们工作起来非常稳定 对于此点无需丝毫怀疑 在绝大多数的情况下 系统调用要比你自己编写的代码可靠而高效的多 但是 在Linux的各版本内核之间 系统调用的兼容性表现得并不像想象那么好 这是由Linux本身的性质决定的 Linux是一群程序设计高手利用业余时间开发出来的 他们中间的大部分人没有把Linux当成一个严肃的商业软件 (现在的情况有些不同了 随着Linux商业公司和以Linux为生的人的增长 不少人的脑筋发生了变化 )结果就是 如果新的方案在效率和兼容性上发生了矛盾 他们往往舍弃兼容性而追求效率 就这样 如果他们认为某个系统调用实现的比较糟糕 他们就会毫不犹豫的作出修改 有些时候甚至连接口也一起改掉了 更可怕的是 很多时候 他们对自己的修改连个招呼也不打 在任何文档里都找不到关于修改的提示 这样 每当新内核推出的时候 很可能都会悄悄的更新一些系统调用 用户编制的应用程序也会跟着出错 说到这里 你是不是感觉前途一片昏暗呢?呵呵 不用太紧张 如前面所说 随着越来越多的人把Linux当成自己的饭碗 不兼容的情况也越来越罕见 从 版本以后的Linux内核已经非常稳定了 不过尽管如此 你还是有必要在每个新内核推出之后 对自己的应用程序进行兼容性测试 以防止意外的发生 该如何学习使用Linux系统调用呢?你可以用 man 系统调用名称 的命令来查看各条系统调用的介绍 但这首先要求你要有不错的英语基础 其次还得有一定的程序设计和系统编程的功底 man pages不会涉及太多的应用细节 因为它只是一个手册而非教程 如果man pages所提 lishixinzhi/Article/program/Oracle/201311/17062一、Linux0.11下添加系统调用:\x0d\x0a\x0d\x0a我在bochs2.2.1中对linux0.11内核添加了一个新的系统调用,步骤如下: \x0d\x0a1./usr/src/linux/include/unistd.h中添加:#define __NR_mytest 87 \x0d\x0a然后在下面声明函数原型:int mytest()\x0d\x0a2./usr/src/linux/include/linux/sys.h中添加:extern int sys_mytest()\x0d\x0a然后在sys_call_table中最后加上sys_mytest; \x0d\x0a3.在/usr/src/linux/kernel/sys.c中添加函数实现如下: \x0d\x0aint sys_mytest(){ \x0d\x0aprintk("This is a test!")\x0d\x0areturn 123\x0d\x0a} \x0d\x0a4.在/usr/src/linux/kernel/system_call.s中对系统调用号加1(原来是86改成了87) \x0d\x0a5.然后到/usr/src/linux目录下编译内核make cleanmake Image \x0d\x0a6. cp /usr/src/linux/include/unistd.h /usr/include/unistd.h \x0d\x0a7. reset bochs \x0d\x0a8. 在/usr/root中生成test.c文件如下: \x0d\x0a#define __LIBRARY__ \x0d\x0a#include \x0d\x0a_syscall0(int,mytest) \x0d\x0aint main(){ \x0d\x0aint a\x0d\x0aa = mytest()\x0d\x0aprintf("%d", a)\x0d\x0areturn 0\x0d\x0a} \x0d\x0a9.然后gcc test.c编译之后运行a.out,前面所有步骤都通过,但是每次调用都是返回-1,然后我查过errno为1(表示 *** 作不允许),就不知道为什么了? \x0d\x0a系统知道的高手们能够告知一下,不胜感激!这个问题困扰我很久了! \x0d\x0a\x0d\x0a二、新Linux内核添加系统调用\x0d\x0a\x0d\x0a如何在Linux系统中添加新的系统调用\x0d\x0a系统调用是应用程序和 *** 作系统内核之间的功能接口。其主要目的是使得用户可以使用 *** 作系统提供的有关设备管理、输入/输入系统、文件系统和进程控制、通信以及存储管理等方面的功能,而不必了解系统程序的内部结构和有关硬件细节,从而起到减轻用户负担和保护系统以及提高资源利用率的作用。\x0d\x0a\x0d\x0aLinux *** 作系统作为自由软件的代表,它优良的性能使得它的应用日益广泛,不仅得到专业人士的肯定,而且商业化的应用也是如火如荼。在Linux中,大部分的系统调用包含在Linux的libc库中,通过标准的C函数调用方法可以调用这些系统调用。那么,对Linux的发烧友来说,如何在Linux中增加新的系统调用呢? \x0d\x0a1 Linux系统调用机制\x0d\x0a\x0d\x0a在Linux系统中,系统调用是作为一种异常类型实现的。它将执行相应的机器代码指令来产生异常信号。产生中断或异常的重要效果是系统自动将用户态切换为核心态来对它进行处理。这就是说,执行系统调用异常指令时,自动地将系统切换为核心态,并安排异常处理程序的执行。Linux用来实现系统调用异常的实际指令是:\x0d\x0a\x0d\x0aInt $0x80\x0d\x0a\x0d\x0a这一指令使用中断/异常向量号128(即16进制的80)将控制权转移给内核。为达到在使用系统调用时不必用机器指令编程,在标准的C语言库中为每一系统调用提供了一段短的子程序,完成机器代码的编程工作。事实上,机器代码段非常简短。它所要做的工作只是将送给系统调用的参数加载到CPU寄存器中,接着执行int $0x80指令。然后运行系统调用,系统调用的返回值将送入CPU的一个寄存器中,标准的库子程序取得这一返回值,并将它送回用户程序。\x0d\x0a\x0d\x0a为使系统调用的执行成为一项简单的任务,Linux提供了一组预处理宏指令。它们可以用在程序中。这些宏指令取一定的参数,然后扩展为调用指定的系统调用的函数。\x0d\x0a\x0d\x0a这些宏指令具有类似下面的名称格式:\x0d\x0a\x0d\x0a_syscallN(parameters)\x0d\x0a\x0d\x0a其中N是系统调用所需的参数数目,而parameters则用一组参数代替。这些参数使宏指令完成适合于特定的系统调用的扩展。例如,为了建立调用setuid()系统调用的函数,应该使用:\x0d\x0a\x0d\x0a_syscall1( int, setuid, uid_t, uid )\x0d\x0a\x0d\x0asyscallN( )宏指令的第1个参数int说明产生的函数的返回值的类型是整型,第2个参数setuid说明产生的函数的名称。后面是系统调用所需要的每个参数。这一宏指令后面还有两个参数uid_t和uid分别用来指定参数的类型和名称。\x0d\x0a\x0d\x0a另外,用作系统调用的参数的数据类型有一个限制,它们的容量不能超过四个字节。这是因为执行int $0x80指令进行系统调用时,所有的参数值都存在32位的CPU寄存器中。使用CPU寄存器传递参数带来的另一个限制是可以传送给系统调用的参数的数目。这个限制是最多可以传递5个参数。所以Linux一共定义了6个不同的_syscallN()宏指令,从_syscall0()、_syscall1()直到_syscall5()。\x0d\x0a\x0d\x0a一旦_syscallN()宏指令用特定系统调用的相应参数进行了扩展,得到的结果是一个与系统调用同名的函数,它可以在用户程序中执行这一系统调用。\x0d\x0a2 添加新的系统调用 \x0d\x0a如果用户在Linux中添加新的系统调用,应该遵循几个步骤才能添加成功,下面几个步骤详细说明了添加系统调用的相关内容。\x0d\x0a\x0d\x0a(1) 添加源代码\x0d\x0a\x0d\x0a第一个任务是编写加到内核中的源程序,即将要加到一个内核文件中去的一个函数,该函数的名称应该是新的系统调用名称前面加上sys_标志。假设新加的系统调用为mycall(int number),在/usr/src/linux/kernel/sys.c文件中添加源代码,如下所示:\x0d\x0aasmlinkage int sys_mycall(int number) \x0d\x0a{ \x0d\x0areturn number\x0d\x0a}\x0d\x0a作为一个最简单的例子,我们新加的系统调用仅仅返回一个整型值。\x0d\x0a\x0d\x0a(2) 连接新的系统调用\x0d\x0a\x0d\x0a添加新的系统调用后,下一个任务是使Linux内核的其余部分知道该程序的存在。为了从已有的内核程序中增加到新的函数的连接,需要编辑两个文件。\x0d\x0a\x0d\x0a在我们所用的Linux内核版本(RedHat 6.0,内核为2.2.5-15)中,第一个要修改的文件是:\x0d\x0a\x0d\x0a/usr/src/linux/include/asm-i386/unistd.h\x0d\x0a\x0d\x0a该文件中包含了系统调用清单,用来给每个系统调用分配一个唯一的号码。文件中每一行的格式如下:\x0d\x0a\x0d\x0a#define __NR_name NNN\x0d\x0a\x0d\x0a其中,name用系统调用名称代替,而NNN则是该系统调用对应的号码。应该将新的系统调用名称加到清单的最后,并给它分配号码序列中下一个可用的系统调用号。我们的系统调用如下:\x0d\x0a\x0d\x0a#define __NR_mycall 191\x0d\x0a\x0d\x0a系统调用号为191,之所以系统调用号是191,是因为Linux-2.2内核自身的系统调用号码已经用到190。\x0d\x0a\x0d\x0a第二个要修改的文件是:\x0d\x0a\x0d\x0a/usr/src/linux/arch/i386/kernel/entry.S\x0d\x0a\x0d\x0a该文件中有类似如下的清单:\x0d\x0a.long SYMBOL_NAME()\x0d\x0a\x0d\x0a该清单用来对sys_call_table[]数组进行初始化。该数组包含指向内核中每个系统调用的指针。这样就在数组中增加了新的内核函数的指针。我们在清单最后添加一行:\x0d\x0a.long SYMBOL_NAME(sys_mycall)\x0d\x0a\x0d\x0a(3) 重建新的Linux内核\x0d\x0a\x0d\x0a为使新的系统调用生效,需要重建Linux的内核。这需要以超级用户身份登录。\x0d\x0a#pwd \x0d\x0a/usr/src/linux \x0d\x0a#\x0d\x0a\x0d\x0a超级用户在当前工作目录(/usr/src/linux)下,才可以重建内核。\x0d\x0a\x0d\x0a#make config \x0d\x0a#make dep \x0d\x0a#make clearn \x0d\x0a#make bzImage\x0d\x0a\x0d\x0a编译完毕后,系统生成一可用于安装的、压缩的内核映象文件:\x0d\x0a\x0d\x0a/usr/src/linux/arch/i386/boot/bzImage \x0d\x0a(4) 用新的内核启动系统 \x0d\x0a要使用新的系统调用,需要用重建的新内核重新引导系统。为此,需要修改/etc/lilo.conf文件,在我们的系统中,该文件内容如下:\x0d\x0a\x0d\x0aboot=/dev/hda \x0d\x0amap=/boot/map \x0d\x0ainstall=/boot/boot.b \x0d\x0aprompt \x0d\x0atimeout=50 \x0d\x0a\x0d\x0aimage=/boot/vmlinuz-2.2.5-15 \x0d\x0alabel=linux \x0d\x0aroot=/dev/hdb1 \x0d\x0a read-only \x0d\x0a\x0d\x0aother=/dev/hda1 \x0d\x0alabel=dos \x0d\x0atable=/dev/had\x0d\x0a\x0d\x0a首先编辑该文件,添加新的引导内核:\x0d\x0aimage=/boot/bzImage-new \x0d\x0alabel=linux-new \x0d\x0aroot=/dev/hdb1 \x0d\x0aread-only\x0d\x0a\x0d\x0a添加完毕,该文件内容如下所示:\x0d\x0aboot=/dev/hda \x0d\x0amap=/boot/map \x0d\x0ainstall=/boot/boot.b \x0d\x0aprompt \x0d\x0atimeout=50 \x0d\x0a\x0d\x0aimage=/boot/bzImage-new \x0d\x0alabel=linux-new \x0d\x0aroot=/dev/hdb1 \x0d\x0aread-only \x0d\x0a\x0d\x0aimage=/boot/vmlinuz-2.2.5-15 \x0d\x0alabel=linux \x0d\x0aroot=/dev/hdb1 \x0d\x0aread-only \x0d\x0a\x0d\x0aother=/dev/hda1 \x0d\x0alabel=dos \x0d\x0atable=/dev/hda\x0d\x0a\x0d\x0a这样,新的内核映象bzImage-new成为缺省的引导内核。为了使用新的lilo.conf配置文件,还应执行下面的命令:\x0d\x0a#cp /usr/src/linux/arch/i386/boot/zImage /boot/bzImage-new\x0d\x0a\x0d\x0a其次配置lilo:\x0d\x0a\x0d\x0a# /sbin/lilo\x0d\x0a\x0d\x0a现在,当重新引导系统时,在boot:提示符后面有三种选择:linux-new 、linux、dos,新内核成为缺省的引导内核。\x0d\x0a至此,新的Linux内核已经建立,新添加的系统调用已成为 *** 作系统的一部分,重新启动Linux,用户就可以在应用程序中使用该系统调用了。\x0d\x0a\x0d\x0a(5)使用新的系统调用\x0d\x0a\x0d\x0a在应用程序中使用新添加的系统调用mycall。同样为实验目的,我们写了一个简单的例子xtdy.c。\x0d\x0a\x0d\x0a/* xtdy.c */ \x0d\x0a#include \x0d\x0a_syscall1(int,mycall,int,ret) \x0d\x0amain() \x0d\x0a{ \x0d\x0aprintf("%d \n",mycall(100))\x0d\x0a}\x0d\x0a编译该程序:\x0d\x0a# cc -o xtdy xtdy.c\x0d\x0a执行:\x0d\x0a# xtdy\x0d\x0a结果:\x0d\x0a# 100\x0d\x0a注意,由于使用了系统调用,编译和执行程序时,用户都应该是超级用户身份。
评论列表(0条)