您的位置:首页 > 运维架构 > Linux

Linux内核分析:使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用

2015-03-28 22:41 1186 查看

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

第一部分:基础知识和系统调用概述

1.1用户态、内核态和中断处理过程

一般现代CPU都有几种不同的指令级别,在高执行级别下,代码可以执行特权指令,访问任意物理地址,这种CPU执行级别就对应着内核态。

Intel x86 cpu 有四种不同的执行级别0-3,Linux只使用了其中的0级和3级,分别来表示内核态(0级)和用户态(3级)。

中断处理是从用户态进入内核态的主要方式。

系统调用只是一种特殊的中断。

1.2 寄存器上下文——从用户态切换到内核态时,必须保存用户态的寄存器上下文

中断/int指令会在堆栈上保存一些寄存器的值,如cs:eip;ss:esp;eflags。

中断发生后第一件事就是保存现场(保存在内核栈上),中断处理结束前最后一件事就是恢复现场。

例如:

发生中断

Save cs:eip/ss:esp/eflags 至内核栈

然后修改cs:eip,使其指向entry of a specific ISR

修改ss:esp 使其指向内核栈

SAVE_ALL

······//内核代码,完成中断服务,可能发生进程调度

RESTORE_ALL

Iret(中断返回)

Pop cs:eip/ss:esp/eflags(从内核栈上)

1.3 系统调用概述

操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用。

应用程序接口(API)和系统调用是不同的,API只是一个函数的定义,而系统调用通过软中断向内核发出一个明确的请求。API可能封装了一个或多个系统调用,也可能没有封装。一般一个系统调用对应一个封装例程,库(如libc)再用这些例程定义出给用户的API。

系统调用的三层皮:API 中断向量 中断服务程序

进程需传递一个名为“系统调用号”的参数来指明需要哪个系统调用,传递方式为将要传递的系统调用号存入eax寄存器中。若还需要其他参数,可以通过ebx,ecx,edx,esi,edi,ebp来传递。若超过6个参数,则将其中一个寄存器变为一个指针,指向一边内存区域。

第二部分:实验过程

2.1实验内容

选择一个系统调用(13号系统调用time除外),系统调用列表参见http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl

参考视频中的方式使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用,推荐在实验楼Linux虚拟机环境下完成实验。

2.2 选择的系统调用介绍

我选择了78号系统调用,sys_gettimeofday,它对应的API是gettimeofday,它用于Linux中的计时,使用C语言编写程序需要获得当前精确时间(1970年1月1日到现在的时间),或者为执行计时,可以使用gettimeofday()函数。它的调用格式是:

#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
int settimeofday(const struct timeval *tv , const struct timezone *tz);


结构timeval的定义为:

strut timeval {long tv_sec; /* 秒数 */long tv_usec; /* 微秒数 */};


可以看出,使用这种方式计时,精度可达微秒,也就是10-6秒。进行计时的时候,我们需要前后调用两次gettimeofday,然后计算中间的差值:

gettimeofday( &start, NULL );
foo();
gettimeofday( &end, NULL );
timeuse = 1000000 * ( end.tv_sec - start.tv_sec ) + end.tv_usec - start.tv_usec;
timeuse /= 1000000;


(参考:http://blog.chinaunix.net/uid-22150747-id-189280.html

那么在直接使用系统调用sys_gettimeofday,只需要将78存入eax,将第二个参数NULL存入ebx,将struct timeval类型的结构体变量地址存入ecx,通过中断指令执行这个系统调用即可。

2.3 实现代码

2.3.1API方式实现代码

//gettimeofday.c

#include <time.h>
#include <stdio.h>
#include <sys/time.h>
int main()
{

struct timeval start,end;//用这两个结构体变量保存开始和结束时间
double Dstart,Dend,Dtime;//用于计算和保存最后结果
Dstart=Dend=Dtime=0;
gettimeofday(&start,NULL);//获取当前时间
sleep(1);
gettimeofday(&end,NULL);//获取当前时间

Dstart=((double)start.tv_sec*1000000+(double)start.tv_usec);
Dend=((double)end.tv_sec*1000000+(double)end.tv_usec);
Dtime=Dend-Dstart;//相减得到sleep函数运行的时间
Dtime=Dtime/1000000;//将结果转换成以秒为单位。
printf("Dtime=%lf\n",Dtime);

return 0;
}


2.3.2 系统调用方式实现代码

//gettimeofday_asm.c

#include <time.h>
#include <stdio.h>
#include <sys/time.h>
int main()
{
struct timeval start;
struct timeval end;
double Dstart=0;
double Dend=0;
double Dtime=0;
asm(
"mov $0,%%ecx\n\t"
"mov %0,%%ebx\n\t"
"mov $0x4e,%%eax\n\t"
"int $0x80\n\t"
:
:"d" (&start)
);
sleep(1);
asm(
"mov $0,%%ecx\n\t"
"mov %0,%%ebx\n\t"
"mov $0x4e,%%eax\n\t"
"int $0x80\n\t"
:
:"d" (&end)
);
Dstart=((double)start.tv_sec*1000000+(double)start.tv_usec);
Dend=((double)end.tv_sec*1000000+(double)end.tv_usec);
Dtime=Dend-Dstart;
Dtime=Dtime/1000000;
printf("Dtime=%lf\n",Dtime);
return 0;
}


2.4 实验结果截图



从图中可以看出,用两种方式得到的时间几乎是相同的,只有略微差别。

2.5 实验分析

在用系统调用实现过程中,关键代码如下:

asm(
"mov $0,%%ecx\n\t"
"mov %0,%%ebx\n\t"
"mov $0x4e,%%eax\n\t"
"int $0x80\n\t"
:
:"d" (&start)
);


第一句:

"mov $0,%%ecx\n\t"


压gettimeofday的第二个参数,这里使用NULL,就是0。

第二句:

"mov %0,%%ebx\n\t"


压gettimeofday的第一个参数,也就是start变量的地址存入ebx。

第三句:

"mov $0x4e,%%eax\n\t"


将系统调用号78,其十六进制就是0x4e,存入eax。

第四句:

"int $0x80\n\t"


使用中断指令,进入内核态,执行系统调用。

最后两行:

:
:"d" (&start)


&start是输入变量,所以放在第二个冒号后面,并使用“d”先将其存入edx寄存器中。这样在执行第二句的时候,实际就是把edx的内容存入ebx中。

第三部分:自己对“系统调用的工作机制”的理解

系统调用的作用是为了在保证系统安全和稳定的前提下,给用户提供各种与硬件设备进行交互的接口。这样用户进程大部分时间都在用户态下运行,只是需要进行与硬件交互或其他需要进入内核态的情况时,才通过中断的方式,进入内核态,然后在内核态下由中断服务程序完成所需功能后,又通过中断返回,切回用户态模式,继续执行用户进程。这就很好的保存了系统内核的安全和稳定。

用户进程一般会使用一些API函数,这些函数中调用了一个或多个系统调用,它通过一个系统调用号来辨别具体调用了哪一个系统调用。传递方式是使用eax寄存器。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐