Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

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

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

1. 用户态和内核态

CPU指令执行级别
- 高级别:执行特权指令,访问任意的物理地址——内核态。
- 低级别:代码只能在级别允许的特定范围内活动——用户态。在日常操作下,执行系统调用的方式是通过库函数,库函数封装系统调用,为用户提供接口以便直接使用。
  • Intel x86 CPU有四种不同的执行级别0~3,Linux只用其中的0和3来表示内核态和用户态

  • 区分内核态和用户态:CPU每条指令的读取都是通过cs:eip,cs寄存器最低两位表明了当前代码的特权级。

    • 内核态下可访问所有地址空间
    • 0xc0000000(逻辑地址)以上的空间只能在内核态下访问
    • 0x00000000 ~ 0xbfffffff 内核态和用户态均可访问
  • 用户态转换为内核态的主要方式:中断

2. 中断处理

  • 用户态到内核态的切换:必须保存用户态的寄存器上下文,包括用户态栈顶地址、当时的状态字、cs:eip的值,以及内核态的栈顶地址、当时的状态字、中断处理程序入口。

    • 中断发生后的第一件事:保存现场(SAVE_ALL:保存需要用到的寄存器数据)。
    • 中断处理结束前的最后一件事:恢复现场(RESTORE_ALL:退出中断程序,恢复保存寄存器的数据)。
  • 完整过程:

    Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

二、系统调用

1. 意义

系统调用是操作系统为用户态进程与硬件设备进行交互提供的一组接口
  • 把用户从底层的硬件编程中解放出来
  • 极大的提高了系统的安全性
  • 使用户程序具有可移植性

2. API与系统调用

API:应用程序编程接口,是一个函数定义。
系统调用:通过软中断向内核发出一个明确的请求。
  • 一般每个系统调用对应一个封装例程,库再用这些封装例程定义出用户的API,方便用户使用。所以,API与系统调用不是一一对应的

      - API可以直接提供用户态服务(如:数学函数)
      - 一个单独的API可能调用几个系统调用
      - 不同的API可能调用了同一个系统调用
    
  • 返回值

      - 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
      - -1表示内核不能满足进程请求 
      - Libc中定义errno变量,包含特定出错码
    

3. 系统调用“三层皮”

- API
- 中断向量
- 中断服务程序
  • 当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。

  • Linux中是通过执行int $0x80来执行系统调用,这条汇编指令产生向量为128的编程异常 —— 即中断向量0x80与System_call绑定起来。

  • 系统调用号将函数xyz()和中断服务程序sys_xyz关联起来。

4. 参数传递

  • 内核实现了很多不同的系统调用,进程用系统调用号这个参数指明需要哪个系统调用。

  • system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,使用eax寄存器传递系统调用号。

  • 寄存器传递参数的限制

      - 每个参数的长度不能超过寄存器的长度,即32位
      - 在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp)
      - 超过6个的情况下,使用某一个寄存器作为指针,进入内核态之后可以访问所有的地址空间,通过某一片区域传递参数。
    

三、使用库函数API和C代码中嵌入汇编代码触发同一个系统调用

1. 使用库函数API获取系统当前时间

Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

2. 使用嵌入式汇编代码获取系统当前时间

Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

  • 内嵌汇编在语法上要求先声明输出参数,然后声明输出参数值。

  • 系统调用的参数,按照顺序分别放在ebx、ecx、edx、esi、edi和ebp中。上述代码中time不需要参数,只需要一个输出值,所以ebx在这里是NULL。

  • eax传递系统调用号,这里time是13

  • 执行的效果与上面的是完全一样的:

    Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

我们使用int 0x80触发中断,然后中断处理程序保存现场,我们的进程陷入内核态。

四、实验

参考:系统调用列表

1. 选取了122号系统调用:uname

  • 获取当前内核名称和其它信息

    Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

2. 使用man查看有关uname的说明

  • man 2 uname

    Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

      - char sysname[_UTSNAME_SYSNAME_LENGTH];   //当前操作系统名
      - char nodename[_UTSNAME_NODENAME_LENGTH]; //网络上的名称
      - char release[_UTSNAME_RELEASE_LENGTH];   //当前发布级别
      - char version[_UTSNAME_VERSION_LENGTH];   //当前发布版本
      - char machine[_UTSNAME_MACHINE_LENGTH];   //当前硬件体系类型
    

3. 编写C语言代码并运行

  • Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

4. 编写嵌入式汇编代码

  • Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

5. 运行与比较

  • 在实验楼环境下运行

    Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

  • 可以运行出来,但是sysname是空的,没有搞明白为什么...

  • 同样的环境对比一下C语言代码

    Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

五、总结

1. 系统调用

  • 即便是最简单的程序,在进行输入输出等操作时也会需要调用操作系统所提供的服务,也就是系统调用。
  • Linux下的系统调用是通过中断(int 0x80)来实现的。

2. 传递参数

  • 在执行int 80指令时,寄存器 eax 中存放的是系统调用的功能号,而传给系统调用的参数则必须按顺序放到寄存器 ebx,ecx,edx,esi,edi 中,当系统调用完成之后,返回值可以在寄存器 eax 中获得。
  • Linux 采用的是 C 语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即最后一个参数先入栈,而第一个参数则最后入栈。

六、待解决问题

1. 为什么用嵌入式汇编代码运行的sysname是空?

  • 编写的代码:

    Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

  • 运行结果:

    Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

2. 问题解决

Linux内核分析第四周学习总结:扒开系统调用的三层皮(上)

  • 反思一下其实是我一开始就没有认真看man中的信息,里面明确的写着:

      int uname(struct utsname *buf);
    
  • 返回值是int,传入指针之后系统调用直接将信息写入指针指向位置!