20189220 余超《Linux内核原理与分析》第五周作业

扒开系统调用的三层皮?(上)

第4章的基础知识

  • Linux系统调用的三层机制:xyz()(API函数)、system_call(系统调用处理入口) 、 sys_xyz()(系统调用内核处理函数)。
  • 32位的X86机器上在用户态的时候只能访问0x00000000—0xbfffffff的地址空间,而在内核态下可以访问所有的地址空间。
  • 中断,系统调用是最常用的两种方式,从用户态切换到内核态。
  • int指令触发中断机制会在堆栈上保存一些寄存器的值,会保存用户态栈顶地址,当时的状态字,当时的CS:EIP的值。
  • 中断发生后的第一件事就是保存现场,中断结束的最后一件事就是恢复现场。
  • 系统调用的意义可以把用户从底层的硬件编程中解放出来,极大的提高了系统的安全性,使用户程序具有可移植性。
  • API(应用程序编程接口)是函数定义,一个API可以对应多个系统调用,他们之间是多对多的关系。
  • 使用EAX寄存器传递一个名为系统调用号的参数。
  • Linux操作系统中采用了0和3两个特权级别,分别对应内核态和用户态。
  • 在Linux中,系统调用是用户空间访问内核的惟一手段;除异常和中断外,它们是内核惟一的合法入口。   而kernel留给用户层的接口其实就只有一个:软中断(int 0x80)。用户态通过它来陷入内核态,完成系统调用。为了方便使用,kernel与用户层之间又增加了API与一些库(例如libc库)来封装这一过程。因此,目前的资料里大部分都写,调用一个系统调用有三种方法:
           (1)通过 glibc 提供的库函数
           (2)使用 syscall 函数直接调用
           (3)通过 int 0x80指令陷入
  • API/libc与系统调用的关系:既然API与libc对软中断进行了封装,那我们先一起看看它们之间的具体关系。一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用一一对应。一个API定义了一组应用程序使用的编程接口。从程序员的角度看,系统调用无关紧要,他们只需要跟API打交道就可以了。相反,内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用不是内核所关心的。

系统调用实现机制

与调用函数一样,系统调用也需要输入输出参数。每个系统调用至少有一个参数,即系统调用号(由eax传递),其他参数依次由ebx、ecx、edx、esi、edi、ebp传入。 由于使用寄存器传递参数,因此对参数的长度做了限制:
(1)每个参数的长度不能超过寄存器的长度,即32位
(2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx、ecx、edx、esi、edi、ebp)如果超过六个,可以传入一个地址,地址所在地存放多个参数
系统调用的三个层次依次是:xyz函数(API)、system_ call(中断向量)和 sys_ xyz(中断服务程序)。

触发一个系统调用的三种方式

1.API函数的方式
首先我选取的是20号的getpid系统调用

编译的结果如下:

2.嵌入式汇编方式

编译运行完的结果

3.调用库函数syscall

编译完运行的结果

4.用gdb进行调试看API的调用是否就是像我们之前分析的对系统调用的封装那样执行的
在getpid处设置断点,运行到断点处。

使用ni命令,对汇编指令逐条运行,前面的清理寄存器的值此处先不讨论,直接一直ni运行到传递系统调用号处(箭头所指的为下一条将要运行的指令, 利用info r命令来查看当前寄存器的值,系统调用号还未传入,eax还为0)。

ni一下,再次查看,发现系统调用号已经传入,确实是利用eax的。

再ni一下,此时该系统调用的实际工作已经完成了,结果(也就是pid)保存在eax中。

实验分析

  • getpid是一种函数,功能是取得进程识别码。
  • 函数原型:旧的原型为pid_t getpid(void);,推荐使用int _getpid( void );这种形式。注意,函数名第一个字符是下划线。
  • 函数说明:getpid函数用来取得目前进程的进程ID,许多程序利用取到的此值来建立临时文件,以避免临时文件相同带来的问题。
  • 返回值:目前进程的进程ID。
  • 在Linux系统中是通过激活0x80中断来触发系统调用的,需要调用的系统调用号实现赋值给eax存储器,如果有传入参数可赋值给ebx寄存器,如果多于1个则按顺序赋值给ebx、ecx、edx、esi、edi、ebp,如果超过6个则通过指针变量指向另一片堆栈区,如果无参数传入则赋值为0。
    汇编代码的分析

#include <stdio.h>
#include<unistd.h>
int main()
{
    pid_t pid;
    asm volatile(
        "movl $0,%%ebx
	"//将ebx寄存器清零,系统调用传递第一个参数使用ebx,这里是null
        "movl $0x14,%%eax
	"//将0xd放入eax中,0x14为20,传递系统调用号20
        "int $0x80
	"
        "movl %%eax,$0
	"//通过eax这个寄存器返回系统调用值,和普通函数一样
        :"=m"(pid)
    );
    printf("this process's pid is : %u
",pid);
    return 0;
}

遇到的问题
1.我最开始在做书上给的38号系统调用的例子的时候,出现了下面的问题

后面再网上搜索资料可以得知是因为在64位系统下去编译32位的目标文件,这样是非法的。必须用”-m32”强制用32位ABI去编译,即可编译通过。

2.我在改写syscall函数的的时候,出现了下面的问题

最后发现是因为缺少必要的头文件,添加上 1 #include <unistd.h>2 #include <sys/syscall.h>3 #include <sys/types.h>即可

本章总结

  • 在Linux中我们可以通过三种方式也进行系统的调用分为:API方式,C代码中嵌入式汇编语言,和库函数syscall。API方法实现系统调用实现非常便捷,只需知道函数原型即可。
  • 经过这次实验,我们可以看到:
          (1)要查看系统资源(如pid等),只有处于内核态(0级)的时候才可以。
           (2)用户要从用户态(3级)切换到内核态(0级),就要通过软中断(int 0x80)来进行系统调用。
           (3)通过eax传递系统调用号,然后由system_call交给system_service完成工作。
           (4)system_service完成工作后,结果又由eax传递给用户态堆栈。
    我对API系统调用的理解:
  • 总之用户调用API函数,系统调用号和参数保存到 eax,ebx ,等寄存器中,通过 0x80 中断向量触发中断陷入内核态,中断服务程序根据系统调用号调用并执行对应的系统调用函数,执行完毕后将结果存放的 eax 中并返回给程序,程序返回用户态。
原文地址:https://www.cnblogs.com/yuchao123/p/9917955.html