第四周 扒开系统调用的三层皮(上)
- 用户态,内核态和中断
和系统调用打交道的方式:通过库函数,把系统调用给封装起来
用户态vs内核态:
一般现代CPU都有几种不同的指令执行级别
在高级别的状态下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别对应着内核态
在相应的低级别执行状态下,代码的掌控范围会受到限制,只能在对应级别允许的范围内活动
为什么有权限级别的划分:为了防止系统崩溃以及恶意代码的入侵,通过划分权限级别来让系统更稳定
举例:Intel x86 CPU有四种不同的执行级别0-3,Linux只使用了其中的0级和3级分别来表示内核态和用户态
区分:在Linux中,地址空间是一个显著的标志,0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问
(地址空间指的是逻辑地址而不是物理地址,逻辑地址:进程的地址空间里边的)
中断处理是从用户态进入内核态主要的方式
系统调用只是一种特殊的中断
当用户态切换到内核态时,必须保存用户态的寄存器上下文
中断/int指令会在堆栈上保存一些寄存器的值,如:用户态栈顶地址,当时的状态字,当时的cs:eip的值
中断发生后第一件事:保存现场(进入中断程序,保存需要用到的寄存器的数据)
SAVE_ALL:把其他的寄存器的值给push到内核堆栈里边去
中断处理结束前最后一件事:恢复现场(退出中断程序,恢复保存寄存器的数据)
RESTOTRE_ALL:把用户态保存的寄存器再popl出来
Iret指令与中断信号(包括int指令)发生时的CPU做的动作相反
- 系统调用概述
系统调用概述:
系统调用的意义
API和系统调用
应用程序,封装程序,系统调用处理程序及系统调用服务例程之间的关系
系统调用的三层皮: xyz(API), system_call(中断向量对应的中断服务程序), sys_xyz (不同种类的服务程序)
系统调用的服务历程:
系统调用的参数传递方法:
系统调用也需要输入输出参数,例如
实际的值
用户态进程地址空间的变量的地址
包含指向用户态函数的指针的数据结构的地址
- 使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
使用库函数API获取当前系统时间
代码:
time.c
#include <stdio.h>
#include <time.h>
int main()
{
time_t tt;//int型数值
struct tm *t;
tt = time(NULL);
t = localtime(&tt);//强制类型转换,便于输出
printf("time:%d:%d:%d:%d:%d:%d:\n",t->tm_year+1960,t->tm_mon,t->tm_mda,t->tm_hour,t->tm_min,t->tm_sec);
return 0;
}
通过gcc time.c -o time -m32编译后打印出当前系统的时间
用汇编方式处罚系统调用获取当前系统时间
代码:
time_asm.c
#include <stdio.h>
#include <time.h>
int main()
{
time_t tt;//int型数值
struct tm *t;
asm volatile(
"mov $0,%%ebx\n\t"//系统调用传递第一个参数使用ebx,这里是null
"mov $0xd,%%eax\n\t"//传递系统调用号13(16进制即0xd)
"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+1960,t->tm_mon,t->tm_mda,t->tm_hour,t->tm_min,t->tm_sec);
return 0;
}
系统调用返回值使用eax存储
- 实验:使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用
选择24号和47号系统调用,分别获取当前用户uid(用户ID)和gid(组ID),即模拟Linux系统“id”命令。
编写两段代码,分别使用库函数API和C代码中嵌入汇编代码,源码如下:
uidgid.c(使用库函数API方式):
程序中通过调用getuid()和getgid()函数来获取当前执行用户uid和gid
uidgid_asm.c(使用C代码中嵌入汇编代码方式):
内嵌汇编代码版本源码中将原来两行通过API函数获取uid和gid的代码注释掉,用汇编代码替换。
首先将ebx寄存器清零,表示无参数传入。
然后分别将0x18和0x2f(十进制24和47)赋值给eax寄存器,表示需要调用的系统调用号,24为getuid,47为getgid。
执行int 0x80来执行系统调用。
之后eax寄存器保存了返回值,将它分别赋值给输出uid或gid变量。
完成整个汇编代码的系统调用。
分别编译两个源码文件:
分别执行系统id命令以及两个编译好的程序:
上面的截图分别表示普通用户ubuntu和管理员用户root分别执行系统自带命令id,库函数API方式uidgid,内嵌汇编方式uidgid_asm这三种方式运行得到的结果是一样的。
通过实验执行结果可知,程序成功完成了系统调用获取当前用户uid和gid的操作,通过内嵌汇编代码可以清晰的看出调用系统调用的工作过程。
首先将ebx寄存器清零,表示无参数传入。
然后分别将0x18和0x2f(十进制24和47)赋值给eax寄存器,表示需要调用的系统调用号,24为getuid,47为getgid。
执行int 0x80来执行系统调用。
之后eax寄存器保存了返回值,将它分别赋值给输出uid或gid变量。
完成整个汇编代码的系统调用。
在Linux系统中是通过激活0x80中断来触发系统调用的,需要调用的系统调用号实现赋值给eax存储器,如果有传入参数可赋值给ebx寄存器,如果多于1个则按顺序赋值给ebx、ecx、edx、esi、edi、ebp,如果超过6个则通过指针变量指向另一片堆栈区,如果无参数传入则赋值为0。
- 总结
虽然Intel X86 CPU有4种执行级别0~3,但是在Linux系统中仅使用了0和3级,分别表示内核态和用户态。
一些涉及底层、硬件、核心的操作必须在内核态下才允许执行,为操作系统程序和驱动程序专享,普通程序仅能执行在用户态下。如果普通程序需要涉及内核态的操作,就需要通过系统调用来实现。这样做的好处是屏蔽平台相关操作降低了软件开发难度,增强了系统安全性,使程序具有更好的移植性(Linux系统及其他Unix系统遵循统一标准,系统调用基本一样)。