#Linux第四周学习总结——扒开系统调用的三层皮(上)
Linux第四周学习总结——扒开系统调用的三层皮(上)
一、用户态、内核态和中断
系统调用通过库函数。
1.用户态和内核态
区分(不同的指令执行级别):
- 用户态:在相应的低执行状态下,代码的掌控范围受到限制,只能在对应级别允许的范围内活动
- 内核态:在高执行级别下,代码可以执行特权指令,访问任意的物理地址。
Intel x86 CPU有四种不同的执行级别0—3,Linux只是用了期中的0级和3级分别表示内核态和用户态。
注意逻辑地址和物理地址的区别。逻辑地址是进程地址空间里面的
2.中断
中断处理是从用户态进入内核态主要的方式
系统调用是只是一种特殊的中断
中断指令或向量发生之后发生中断处理程序,刚一开始执行SAVE ALL,即把其他的寄存器的值push到内核堆栈上去,结束之后,把用户态的寄存器的值在popl出来,最后iret,对应着中断信号(int指令),发生时的CPU动作相反。
- 保护现场就是进入中断程序,保存需要用到的寄存器的数据;
- 恢复现场就是退出中断程序,恢复保存寄存器的数据。
中断处理的完整过程:
理解:中断信号(int指令)完成:保存cs:eip的值、当前堆栈段栈顶和当前标志,同时加载了当前中断信号或是系统调用的相关联的中断服务入口到cs:eip里面,把当前对战段和esp也加载到CPU里面。
SAVE ALL完成后若没有发生调度,则接着执行RESTORE_ALL;若发生进程调度,则当前的状态会暂时的保存在系统里面,当下一次发生进程调度切换到当前进程时再接着执行完毕。
二、系统调用概述
1.系统调用的意义:
操作系统为用户态进程与硬件设备进行交互提供的一组接口。可以把用户从底层的硬件变成中解放出来,极大的提高了系统的安全性,使用户程序具有可移植性。
2.API应用程序编程接口:
只是一个函数定义,系统调用通过软中断向内核发出一个明确的请求。
Libc库定义的一些API引用了一些封装例程,目的是发布系统调用,让程序员写代码的时候可以通过函数调用而非汇编指令触发一个系统调用。
3.返回值:
- 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用;
- -1在多数情况下表示内核不能满足进程的请求;
- Libc中定义的errno变量包含特定的出错码。
4.系统调用的三个层次
应用程序编程接口里封装了一个系统调用,int 0x80中断向量,对应着system_call内核代码的起点,执行完中断服务程序,返回。
系统调用的三个层次依次是:xyz函数(API)、system_ call(中断向量)和 sys_ xyz(中断服务程序)。
5.传参
内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数,使用eax寄存器传递。
系统调用号将xyz和sys_xyz关联起来了。
system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号。具体过程:
1.一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即__NR_fork)。
2.这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
3.进入sys_call之后,立即将eax的值压入内核堆栈。
寄存器传递参数具有如下限制:
- 每个参数的长度不能超过寄存器的长度,即32位
- 在系统调用号( eax)之外,参数的个数不能超过6个( ebx,ecx, edx, esi, edi, ebp)
- 参数个数超过6个则把某一个寄存器作为指针指向一块内存,进入内核态之后可以访问所有的地址空间,通过那块内存传递参数。
三、使用库函数API和C代码中嵌入汇编代码触发系统调用
1. 内嵌汇编语法:
__asm__(
汇编语句模板:
输入部分:
输出部分:
破坏描述部分:);
这部分内容前几周已经讲过,这里不再重复。
2. 用汇编方式出发系统调用获取系统当前时间
汇编代码分析:
#include <stdio.h>
#include <time.h>
int main()
{
time_t tt;//int型数值
struct tm *t;
asm volatile(
"mov $0,%%ebx\n\t"//将ebx寄存器清零,系统调用传递第一个参数使用ebx,这里是null
"mov $0xd,%%eax\n\t"//将0xd放入eax中,0xd为13,传递系统调用号13
"int $0x80\n\t"
"mov %%eax,$0\n\t"//通过eax这个寄存器返回系统调用值,和普通函数一样
:"=m"(tt)
);
t = localtime(&tt);
printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900,t->tm_mon,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
return 0;
}
用户态进程向内核态传递了系统调用号。
四、实验——使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用
本次实验我选择了24号系统调用,获取当前用户uid(用户ID)。
实验过程:
使用库函数API方式:
使用C代码中嵌入汇编代码方式:
编译运行:
汇编代码调用系统调用的工作过程分析:
-
首先将ebx寄存器清零,表示无参数传入;
-
然后将0x18放入eax,表示需要调用的系统调用号24;
-
执行int 0x80来执行系统调用;
-
之后eax寄存器保存了返回值,将它赋值给输出uid变量。
完成整个汇编代码的系统调用。
总结:
在Linux系统中是通过激活0x80中断来触发系统调用的,需要调用的系统调用号实现赋值给eax存储器,如果有传入参数可赋值给ebx寄存器,如果多于1个则按顺序赋值给ebx、ecx、edx、esi、edi、ebp,如果超过6个则通过指针变量指向另一片堆栈区,如果无参数传入则赋值为0。
虽然Intel X86 CPU有4种执行级别0~3,但是在Linux系统中仅使用了0和3级,分别表示内核态和用户态。一些涉及底层、硬件、核心的操作必须在内核态下才允许执行,为操作系统程序和驱动程序专享,普通程序仅能执行在用户态下。如果普通程序需要涉及内核态的操作,就需要通过系统调用来实现。这样做的好处是屏蔽平台相关操作降低了软件开发难度,增强了系统安全性,使程序具有更好的移植性(Linux系统及其他Unix系统遵循统一标准,系统调用基本一样)。
另外,我想问一下老师,为什么我的API方法运行结果最后会多出来一个%?
参考资料:
http://www.iteedu.com/os/linux/linuxprgm/linuxcfunctions/user/getuid.php