怎么用?(实践性) 以x86体系下linux内核为例
正如《系统调用一》里所讲,执行系统调用只有3个步骤:选系统调用号,传入系统调用参数,执行特权指令。只要正确的执行这3个步骤便可以完成对所有系统调用的执行。
3个步骤简单明了,但实现却纷繁复杂。本文以下内容将以原始,进阶,高阶,三个阶段去了解怎么调用系统调用。三个阶段都能完成系统调用的执行,但抽象程度一个比一个高,实际中的使用程度也一个比一个高。
开始之前
首先,必须明白只使用C语言是无法完成系统调用的执行的。C语言无法明确的设置寄存器,且无法执行特权指令。而这两部分是执行系统调用必须的。所以需要借助更底层的语言—汇编。而本文是以linux系统为例的,所以使用Gnu C的嵌入式汇编功能。如果对嵌入式汇编有疑惑可自行查找资料查看。
原始(一)
当需要执行某个系统调用最原始也是最直观的是使用嵌入式汇编直接调用。
/*
* 执行系统调用write 与系统调用exit
*/
int main(int argc,char *argv[])
{
int ret=0;
char *str="hello world \n";
int status=0;
//完成write系统调用
asm("int $0x80"
:"=a"(ret)
: "a"(4),"b"(1),"c"(str),"d"(13)
);
//使用汇编解释以上代码
//movl $4,%eax
//movl $1,%ebx
//movl str,%ecx 将str的值赋给ecx str为局部变量 实际中不能如此书写
//movl $13,%edx
//int $0x80
//mov %eax,ret 将eax的值赋给ret ret为局部变量 实际中不能如此书写
//完成exit系统调用
asm("int $0x80"
:"=a"(ret)
:"a"(1),"b"(status)
);
//使用汇编解释以上代码
//movl $1,%eax
//movl status,%ebx 将status的值赋给ebx status为局部变量 实际中不能如此书写
//int $0x80
//mov %eax,ret 将eax的值赋给ret ret为局部变量 实际中不能如此书写
return 0;
}
代码执行了2个系统调用,系统调用write以及exit ,输出 “hello world \n”后退出程序。
原始(二)
直接使用嵌入式汇编,虽然能够完成对系统调用的执行,但存在许多问题。
嵌入式汇编代码不易阅读,不易修改,易出错,太依赖机器,从x86体系移植到其他体系下,嵌入式汇编代码都需要修改。
所以glibc提供了syscall函数,封装了系统调用过程。
int syscall(int number,...);
number为系统调用号,而省略号表示参数可变。
不同系统调用的参数不同,系统调用存在几个参数,便写入几个参数。
若执行成功则返回0或正数,失败则返回-1,并将错误号放入全局变量errno中。
使用syscall重写以上示例
int main(int argc,char *argv[])
{
int ret=0;
char *str="hello world \n";
int status=0;
//完成write系统调用
ret=syscall(4,1,str,13);
//完成exit系统调用
ret=syscall(1,status);
return 0;
}
不同体系下都可以调用syscall函数来完成系统调用的执行,syscall相对于程序员来说是与系统无关的。
进阶
不管是syscall还是嵌入式汇编都需要我们记住每个系统调用对应的系统调用号以及对应的参数类型与个数。这极其困难,也极易出错。所以系统开发者将每个系统调用封装成一个函数。
《系统调用(一)》中写到系统调用本质上是函数列表,而用函数封装系统调用其实还原了系统调用原本的样子。
如系统调用write可以封装成int write(int fd,char *buf,int size)函数。当需要调用write系统调用直接查看write函数,马上可以获取参数类型以及个数。
封装可以借助于嵌入式汇编或syscall
//封装write系统调用
//借助于嵌入式汇编
int write(int fd,char *buf,int size)
{
int ret;
asm("int $0x80"
:"=a"(ret)
: "a"(4),"b"(fd),"c"(buf),"d"(size)
);
return ret;
}
//借助于syscall
int write(int fd,char *buf,int size)
{
int ret;
ret=syscall(4,fd,buf,size);
return ret;
}
封装后重写上面示例
int main(int argc,char *argv[])
{
int ret=0;
char *str="hello world \n";
int status=0;
//完成write系统调用
ret=write(1,str,13);
//完成exit系统调用
ret=exit(status);
return 0;
}
linux内核中使用一种方式:嵌入式汇编加上宏完成系统调用的封装。如感兴趣可参考系统调用的封装
高阶
实际开发中使用最多的方式是调用glibc中对系统调用的封装。
glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库,即运行时库。glibc 为程序员提供丰富的 API(Application Programming Interface),除了例如字符串处理、数学运算等用户态服务之外,最重要的是封装了操作系统提供的系统服务,即系统调用的封装。许多系统调用在glibc中都有同名的封装函数。一般这些函数的语义与系统调用的语义是一致的,但也有些进行了改变,比如系统调用brk与glibc提供的brk的语义是不一致的(主要是返回值进行了改变)。也存在一些系统调用是glibc未提供的,比如_llseek,glibc提供了lseek64对其进行了封装。glibc不提供一些系统调用,主要出于移植的考虑,比如_llseek在64位中并不存在,所以用lseek64函数替代了它。
对于不存在或者语义不同的系统调用可以使用上面的方式执行系统调用。