#####################################
作者:张卓
原创作品转载请注明出处:《Linux操作系统分析》MOOC课程 http://www.xuetangx.com/courses/course-v1:ustcX+USTC001+_/about
#####################################
一 系统调用概述
操作系统为用户态进程与硬件设备进行交互提供了一组接口-系统调用
● 把用户从底层的硬件编程中解放出来
● 极大的提高了系统的安全性
● 使用用户程序具有可移植性
操作系统提供的API和系统调用的关系
应用编程接口(application program interace,API)和系统调用是不同的
● API只是一个函数定义
● 系统调用通过软中断向内核发生一个明确的请求
Libc库定义的一些API引用了封装例程(wrapper routine,唯一目的就是发布系统调用)
● 一般每个系统调用对应一个封装例程
● 库再用这些例程定义出给用户的API
不是每个API都对应一个特定的系统调用
● API可能直接提供用户态的服务,如一些数学函数
● 一个单独的API可能调用几个系统调用
● 不同的API可能调用同一个系统调用
返回值
● 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
● -1在大多数的情况下表示内核不能满足进程的请求
● Libc中定义的errno变量包含特定的出错码
二 系统调用的执行过程分析
在 Linux 平台下有两种方式来使用系统调用:利用封装后的 C 库(libc)或者通过汇编直接调用。其中通过汇编语言来直接调用系统调用,是最高效地使用 Linux 内核服务的方法,因为最终生成的程序不需要与任何库进行链接,而是直接和内核通信。
下面我将借助 实验楼的环境详细分析使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用的过程。
1 #include
2 #include
3 #include
4 #include
5
6 int main(int argc, const char *argv[])
7 {
8 int fd = 0;
9 int ret = 0;
10 char buf[100] = {0};
11 char *filename = "1569.txt";
12
13 fd = open(filename, O_RDONLY);
14 if( 0 > fd){
15 perror("Failed to open");
16 exit(1);
17 }
18
19 ret = read(fd, buf, 50);
20 if(ret < 0){
21 perror("Failed to read");
22 close(fd);
23 exit(1);
24 }
25 printf("%s\n",buf);
26
27 return 0;
28 }
这简单的程序用库函数的方式使用了两个系统调用:5 和 3
通过简单的编译执行,可以得到如下的结果:
接下来,我们用GCC内嵌汇编的方式来执行系统调用:
1 #include
2 #include
3 #include
4 #include
5
6 int main(int argc, const char *argv[])
7 {
8 int fd = 0;
9 int ret = 0;
10 char buf[100] = {0};
11 char *filename = "1569.txt";
12
13 //fd = open("1569.txt", O_RDONLY);
14 asm volatile(
15 "movl %1,%%ebx\n\t" /* 用ebx保存第一个参数 */
16 "movl %2,%%ecx\n\t" /* 用ecx保存第二个参数 */
17 "movl $0x5,%%eax\n\t" /* 用eax传递系统调用号,5表示sys_open */
18 "int $0x80\n\t" /* 系统通过 int $0x80这条指令产生一个软中断,执行系统调用 */
19 "movl %%eax, %0\n\t" /* 和其它函数一样,返回值也是保存在eax寄存器之中 */
20 :"=m"(fd)
21 :"b"(filename),"c"(O_RDONLY)
22 );
23 if( 0 > fd){
24 perror("Failed to open");
25 exit(1);
26 }
27
28 //ret = read(fd, buf, 50);
29 asm volatile(
30 "movl %1,%%ebx;"
31 "movl %2,%%ecx;"
32 "movl %3,%%edx;"
33 "movl $0x3,%%eax;"
34 "int $0x80;"
35 "movl %%eax, %0;"
36 :"=m"(ret)
37 :"b"(fd),"c"(buf),"d"(50)
38 );
39 if(ret < 0){
40 perror("Failed to read");
41 close(fd);
42 exit(1);
43 }
44 printf("%s\n",buf);
45
46 return 0;
47 }
48
和 DOS 一样,Linux 下的系统调用也是通过中断(int 0x80)来实现的。在执行 int 80 指令时,寄存器 eax 中存放的是系统调用的功能号,而传给系统调用的参数则必须按顺序放到寄存器 ebx,ecx,edx,esi,edi 中,当系统调用完成之后,返回值可以在寄存器 eax 中获得。所有的系统调用功能号都可以在文件 /usr/include/bits/syscall.h 中找到。
通过下面的命令编译,然后执行:
gcc read_asm.c -o read_asm -m32
可以看到执行结果和上面的一样:
三 总结
从上面实验的内容我们得出,系统调用的执行过程,可以用下面的一个图表示出来:
用户进程通过调用系统提供的API,在API中执行类似与上面内嵌汇编的程序;进而执行system call 处理程序,在system call处理程序中执行真正的系统提供的服务程序,执行完毕返回退出。系统调用号将API和system call 紧密联系在一起。在这个过程中涉及到了,系统从用户态切换到内核态执行程序的过程,从而可以说明系统调用也是一种特殊的中断。中断向量0x80对应system call。
本博客参考: Linux 汇编语言开发指南
系统调用列表参见 http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl