系统调用初探

1. 系统调用概述

        

用户态与内核态

  • 用户态是指程序在相应的低级别执行状态下,代码的掌控范围会受到限制;只能在对应级别允许的范围内活动;
  • 内核态是指操作系统在高执行级别下,程序代码可以执行特权指令,访问任意的物理地址;
  • 内核态与用户态的区别不是说用户程序的运行状态如何如何,主要区别在于 CPU/OS 的控制权限得以提升或下降;
  • 用户态与内核态切换的触发方式:系统调用、异常、中断;

系统调用与Shell

  • 为了实现内核与用户的交互,计算机提供了两种方式:系统调用和 Shell ;
  • 系统调用是操作系统为用户态进程与硬件设备进行交互提供的一组接口,是为了方便程序员编程用;
  • Shell 则与编程无关,涉及到的是 Shell 脚本编程(命令的集合),是为了方便人机交互而设计的;
  • 当然,Shell 命令可能暗地里调用的还是系统调用;

库函数API与系统调用


        应用编程接口(Application Programming Interface)与系统调用(System Call)是不同的。API 只是函数定义,而系统调用则是通过软中断向内核发出一个明确的请求。系统调用是在内核态完成的,而以普通函数为代表的 API 是在用户态下完成的。 


系统调用的工作流程

系统调用初探_第1张图片

应用程序、封装例程、系统调用处理程序及 系统调用服务例程之间的关系

2. 方法论


        深入系统调用的话,就需要我们在C代码中嵌入汇编语言来触发系统调用,也就是说我们需要通过库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用。那么如何做到呢?我的方法是,C 语言的编程代码我们可以顺利地编程得到,但是 C 代码中嵌入汇编的方式可能无法顺利地做到,或者做到的过程中会出现问题,我们就需要一条条语句来做,并且每条语句做好了就立马进行调试,使得最后得到的结果无限接近于用 C 代码写出的程序。


3. 程序目标


        这次实验所编写的C 程序是文件拷贝程序,将当前目录下文件名为 “src_file” 的文件内容拷贝到当前目录的另一个文件 “dest_file” 中去(文件不存在则创建)。


4. 编码过程


C代码实现

#include 
#include 
#include 

#define BUFFER_SIZE	    1024		/* 每次读写缓存大小,影响运行效率 */
#define OFFSET		    10240		/* 拷贝的数据大小 */
#define SRC_FILE_NAME	    "src_file"	        /* 源文件名 */
#define DEST_FILE_NAME	    "dest_file"	        /* 目标文件名 */

int main()
{
	int src_fd, dest_fd;
	unsigned char buff[BUFFER_SIZE];
	int buff_len;

	/* 以只读的方式打开源文件 */
	src_fd = open(SRC_FILE_NAME, O_RDONLY);

	/* 以只读的方式打开目标文件,若此文件不存在则创建,访问权限为644 */
	dest_fd = open(DEST_FILE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
	
	if (src_fd < 0 || dest_fd < 0)
	{
		printf("Open File Error!\n");
		exit(1);
	}

	/* 将源文件的读写指针移到最后10KB的起始位置 */
	lseek(src_fd, -OFFSET, SEEK_END);

	/* 读取源文件的最后10KB数据并写到目标文件中,每次读写1KB */
	while ((buff_len = read(src_fd, buff, sizeof(buff))) > 0)
	{
		write(dest_fd, buff, buff_len);
	}

	close(dest_fd);
	close(src_fd);

	return 0;
}


实验截图


系统调用初探_第2张图片

file.c 文件的编写


系统调用初探_第3张图片

编辑 src_file 文件的内容


系统调用初探_第4张图片

编译 file.c; 运行 file 可执行文件; 用 cat 命令查看 dest_file 文件内容


        从实验代码及截图可以看出程序的效果,成功地实现了预设的目标——“当前目录下文件名为 “src_file” 的文件内容拷贝到当前目录的另一个文件 “dest_file” 中去(文件不存在则创建)”。


C嵌入汇编实现


直白的方式:

#include 
#include 
#include 
#include 
#include 

#define BUFFER_SIZE 1024
#define OFFSET      10240

int main()
{
    int src_fd, dest_fd ;
    unsigned char buff[BUFFER_SIZE] ;
    int buff_len ;

    char *src_file_name = "src-asm_file" ;
    char *dest_file_name = "dest-asm_file" ;

    //src_file = open(SRC_FILE_NAME, O_RDONLY) ;
    asm volatile (
            "movl %1, %%ebx\n\t"
            "movl $0x0000, %%ecx\n\t"	//O_RDONLY
            "movl $0x5, %%eax\n\t"	//set system call numbers $0x05-->open
            "int $0x80\n\t"
            "movl %%eax, %0\n\t"
            : "=m" (src_fd)
            : "p" (src_file_name)
            ) ;
	
    //dest_file = open(DEST_FILE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    asm volatile (
            "movl %1, %%ebx\n\t"
            "movl $0x0001, %%ecx\n\t"   //O_WRONLY
            "or $0x0200, %%ecx\n\t"     //O_CREAT
            "movl $0000400, %%edx\n\t"  //S_IRUSR
            "or $0000200, %%edx\n\t"    //S_IRUSR|S_IWUSR
            "or $0000040, %%edx\n\t"    //S_IRGRP
            "or $0000004, %%edx\n\t"    //S_IROTH
            "movl $0x5, %%eax\n\t"      //set system call numbers $0x05-->open
            "int $0x80\n\t"
            "movl %%eax, %0\n\t"
            : "=m" (dest_fd)
            : "p" (dest_file_name)
            ) ;

    if (src_fd < 0 || dest_fd < 0)
    {
	printf("Open File Error!\n");
        exit(1);
    }

    //lseek(src_fd, -OFFSET, SEEK_END);
    asm volatile (
            "movl %0, %%ebx\n\t"
            "movl $-1024, %%ecx\n\t"	//OFFSET
            "movl $2, %%edx\n\t"	//SEEL_END
            "movl $0x13, %%eax\n\t"	//set system call numbers $0x13-->lseek
            "int $0x80\n\t"
            :
            : "m" (src_fd)
            ) ;
	
    do
    {
	//read(src_fd, buff, sizeof(buff))
        asm volatile (
                "movl %1, %%ebx\n\t"
                "movl %2, %%ecx\n\t"
                "movl %3, %%edx\n\t"
                "movl $0x03, %%eax\n\t"	//set system call numbers $0x03-->read
                "int $0x80\n\t"
                "movl %%eax, %0\n\t"
                : "=m" (buff_len)
                : "m" (src_fd), "p" (buff), "i" (sizeof(buff))
                ) ;
		
        if (real_real_len > 0)
        {
	    //write(dest_fd, buff, buff_len);
            asm volatile (
                    "movl %0, %%ebx\n\t"
                    "movl %1, %%ecx\n\t"
                    "movl %2, %%edx\n\t"
                    "movl $0x04, %%eax\n\t"//set system call numbers $0x04-->write
                    "int $0x80\n\t"
                    :
                    : "m" (dest_fd), "p" (buff), "m" (buff_len)
                    ) ;
        }
        else
        {
            break ;
        }
    } while (1) ;

    close(src_fd) ;
    close(dest_fd) ;
    
    return 0 ;
}

        在程序编写的时候,首先碰到的难题就是 open 函数。open 函数的原型是

int open(const char *pathname, int flags);
让我感到下不了手的,就是宏定义中的文件名,宏定义的 SRC_FILE_NAME 参数该如何传给 open 函数呢?难!我只好在 main 函数中声明了两个字符串指针,指向两文件的文件名,嵌入式汇编的时候输入参数就是一个指针,指向文件名字符串常量。而 flags 则相对好办多了,那些宏定义在 fcntl.h 文件中有定义,只需要到里面查找就好了,于是乎我就把O_RDONLY, O_WRONLY, ... 都找到然后一一放到寄存器中去。

        lseek 函数中的参数,OFFSET 我也直接用立即数的形式存放到寄存器中去了,参数 SEEK_END 也找到相应的值存放到寄存器中去。

        而循环则又重新写了一遍,因为先要读取一片段的数据到缓冲区;然后判断读取是否成功;成功了的话,然后再将缓冲区中的数据写到目标文件中去;不成功则退出循环。于是乎,照着这个逻辑写了一遍。

        也就是在重写 read 函数的这个时候,我才意识到前面写的代码都可以更好。我在思考怎么把 sizeof(buf) 这个参数传给 read 函数时,我原先想申明个变量 int buf_size = (int)sizeof(buff) ; 然后把 buf_size 以内存值的方式存放到寄存器,但是又觉得麻烦。重新审视 C 语言才明白,buf_size 也只不过是个内存地址,赋值操作只不过是把值/变量中的值存放到 buf_size 地址对应的内存空间中去罢了。而 sizeof(buff) 本身就是个立即数!然后又发现,既然

"m" (buff_size)

可以传给寄存器,那么为什么立即数不能传给寄存器呢?

"i" (sizeof(buff))

这时候,暮然回首,才发现圆括号里面的 C 语言参数是可以被解析的,是可以识别出来的!那么也就是说,前面的 O_RDONLY, O_WRONLY, OFFSET, SEEK_END, ... 什么乱七八糟的都是可以直接放到括号里面去的!这样程序的可读性,易理解程度不是大大提高了吗?天才!

        于是乎,就有了下面的程序,这样子的程序只要有点 C 编码基础和 AT&T 汇编基础的人,读起来都不会太难。同时我想到了,既然已经把 open、read、write、lseek 等系统调用 API 用 C 嵌入式汇编的方式实现了,那为什么不更疯狂一点儿把 exit、close 等都实现呢?


增强可读性

#include 
#include 
#include 
#include 
#include 

#define BUFFER_SIZE 1024
#define OFFSET      10240

int main()
{
    int src_fd, dest_fd ;
    unsigned char buff[BUFFER_SIZE] ;
    int buff_len ;

    char *src_file_name = "src-asm_file" ;
    char *dest_file_name = "dest-asm_file" ;

    asm volatile (
            "movl %1, %%ebx\n\t"
            "movl %2, %%ecx\n\t"
            "movl $0x5, %%eax\n\t"
            "int $0x80\n\t"
            "movl %%eax, %0\n\t"
            : "=m" (src_fd)
            : "p" (src_file_name), "i" (O_RDONLY)
            : "eax", "ebx", "ecx"
            ) ;

    asm volatile (
            "movl %1, %%ebx\n\t"
            "movl %2, %%ecx\n\t"
            "movl %3, %%edx\n\t"
            "movl $0x5, %%eax\n\t"
            "int $0x80\n\t"
            "movl %%eax, %0"
            : "=m" (dest_fd)
            : "p" (dest_file_name), "i" (O_WRONLY|O_CREAT), 
                "i" (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
            : "eax", "ebx", "ecx", "edx"
            ) ;

    if (src_fd < 0 || dest_fd < 0)
    {
        printf("Open File Error!\n") ;
        asm volatile (
                "movl $1, %ebx\n\t"
                "movl $0x01, %eax\n\t"
                "int $0x80\n\t"
                ) ;
    }

    asm volatile (
            "movl %0, %%ebx\n\t"
            "movl %1, %%ecx\n\t"
            "movl %2, %%edx\n\t"
            "movl $0x13, %%eax\n\t"
            "int $0x80\n\t"
            :
            : "m" (src_fd), "i" (-OFFSET), "i" (SEEK_END)
            : "eax", "ebx", "ecx", "edx"
            ) ;

     
    do
    {
        asm volatile (
                "movl %1, %%ebx\n\t"
                "movl %2, %%ecx\n\t"
                "movl %3, %%edx\n\t"
                "movl $0x03, %%eax\n\t"
                "int $0x80\n\t"
                "movl %%eax, %0\n\t"
                : "=m" (buff_len)
                : "m" (src_fd), "p" (buff), "i" (sizeof(buff))
                : "eax", "ebx", "ecx", "edx"
                ) ;
        if (buff_len > 0)
        {
            asm volatile (
                    "movl %0, %%ebx\n\t"
                    "movl %1, %%ecx\n\t"
                    "movl %2, %%edx\n\t"
                    "movl $0x04, %%eax\n\t"
                    "int $0x80\n\t"
                    :
                    : "m" (dest_fd), "p" (buff), "m" (buff_len)
                    : "eax", "ebx", "ecx", "edx"
                    ) ;
        }
        else
        {
            break ;
        }
    } while (1) ;

    asm volatile (
            "movl %0, %%ebx\n\t"
            "movl $0x06, %%eax\n\t"
            "int $0x80\n\t"
            :
            : "m" (src_fd)
            : "eax", "ebx"
            ) ;

    asm volatile (
            "movl %0, %%ebx\n\t"
            "movl $0x06, %%eax\n\t"
            "int $0x80\n\t"
            :
            : "m" (dest_fd)
            : "eax", "ebx"
            ) ;
    
    return 0 ;
}


可以看到,上面的这个程序并没有 C 程序的那么简洁,一个 main 函数那么长,而且都是汇编,简直吓死人!有很多代码片段都是可服用的,比如关闭文件的汇编代码段:

asm volatile (
            "movl %0, %%ebx\n\t"
            "movl $0x06, %%eax\n\t"
            "int $0x80\n\t"
            :
            : "m" (src_fd)
            : "eax", "ebx"
            ) ;

    asm volatile (
            "movl %0, %%ebx\n\t"
            "movl $0x06, %%eax\n\t"
            "int $0x80\n\t"
            :
            : "m" (dest_fd)
            : "eax", "ebx"
            ) ;
    
    return 0 ;

        我们把文件描述符放到输入参数部分的好处除了程序可读性增强之外,还增强了程序的可复用性。除了文件描述符(输入参数)不一样,汇编代码段完全相同!
我们可以封装一个函数来包装,只需要把文件描述符传进去就好了,如:
void xyz(int fd)
{
    ...
}

        但是,等一下!我们的这个函数怎么这么眼熟?不就是 close(int fd) 本身嘛!我们绕了一大圈又回来了,但是也是有经验的不是吗?在试图提高程序可读性的同时试图提高程序到可复用性,我们大致知道了这些系统调用是怎么被封装的。

实验截图


系统调用初探_第5张图片

编辑 file-asm.c 文件


系统调用初探_第6张图片

编辑 src-asm_file 文件内容


系统调用初探_第7张图片

编译 file-asm.c; 运行 file-asm 可执行文件; 用 cat 命令查看 dest_file 文件内容

        可以看出,程序实现的非常成功,我们用 cat 命令查看“dest-asm_file”时,终端显示出了 Yeates 的诗When You Are Old. 效果跟调用系统调用 API 时效果相同——“将当前目录下文件名为 “src-asm_file” 的文件内容拷贝到当前目录的另一个文件 “des-asmt_file” 中去(文件不存在则创建)”。


5. 总结


        实验过程颇具挑战性,需要一点点尝试、学习、理解,才能写出最后看起来没有问题的程序。库函数 API(这里特指那些系统调用的API)最终会调用系统调用,如 xyz(); 通过 glic 库中的封装例程来触发软中断,调用系统调用;而系统调用处理程序 system_call 则会调用系统调用服务例程 sys_xyz() 最终执行相应的内核级操作;系统中断服务例程处理完毕后返回到系统调用处理程序 ret_from_sys_call ,而此时正是进程调度的时机,因为此时正处于内核态,如果不进行进程调度的话则会返回到 glibc 库的封装例程中去(用户态);glibc 库中的封装例程结束,用户程序进行下一步操作。


陈金雷+原创作品转载请注明出处+《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000


你可能感兴趣的:(Linux内核分析)