程涵
原创博客
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
【使用库函数API和C代码中嵌入汇编代码触发同一个系统调用】
知识点整理
一、用户态、内核态和中断
处理过程
1. 通过库函数完成系统调用:库函数将系统调用封装起来。
2. 用户态与内核态
- 内核态:一般现代CPU有几种指令执行级别。在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别对应着内核态。
- 用户态:在相应的低级别执行状态下,代码的掌控范围有限,只能在对应级别允许的范围内活动。
3. 区分用户态与内核态的方法
- cs寄存器的最低两位表明了当前代码特权级
- CPU每条指令的读取都是通过cs:eip这两个寄存器
4. 中断处理
- 中断处理是从用户态到内核态的主要方式(系统调用是中断的一种特殊形式)
- 系统调用只是一种特殊的中断
- 寄存器上下文,从用户态切换到内核态时:必须保存用户态的寄存器上下文,同时将内核态的寄存器相应的值放入当前CPU
- 中断/int指令会在堆栈上保存一些寄存器的值:如用户态栈顶地址、当前的状态字、当时cs:eip的值(当前中断程序的入口)
5. 保护现场与恢复现场
-
保护现场:进入中断程序,保存需要用到的寄存器的数据(中断发生后的第一件事)
#define SAVE_ALL //将其他寄存器的值push到内核堆栈中
-
恢复现场:退出中断程序,恢复保存寄存器的数据(中断处理结束前最后一件事)
#RESTORE_ALL //将用户态保存的寄存器pop到当前CPU中
iret指令:iret指令与中断信号(包括int指令)发生时的CPU的动作相反
6. 中断处理的完整过程
-
第一步
interrupt(ex:int 0x80)-save //int 0x80指系统调用
cs:eip/ss:esp/eflags(current)to kernel stack,then //中断将cs:eip、ss:esp(当前堆栈段栈顶)、eflags(当前标志寄存器)保存到内核堆栈中
load cs:eip(entry of a specific ISR)and //将当前中断信号相关联的中断服务入口加载到cs:eip
ss:esp(point to kernel stack). //同时将当前指向内核信息的的堆栈段和esp也加载到CPU中 -
第二步
SAVE_ALL
-... //内核代码,完成中断服务,(完成中断服务后可能)发生进程调度
//如果发生了进程调度,则当前的状态都会暂时保存在系统中。当其他进程调度切换回当前进程时,则接着执行RESTORE_ALL -
第三步
RESTORE_ALL
-
第四步
iret -pop cs:eip/ss:esp/eflags from kernel stack
二、系统调用概述和系统调用的三层皮
1. 系统调用概述
系统调用是操作系统为用户态进程与硬件设备进行交互提供的一组接口。
意义:
- 把用户从底层的硬件编程中解放出来
- 极大的提高了系统的安全性
- 使用户程序具有可移植性(用户程序与具体硬件被抽象的接口替代,没有非常紧密的关系)
2. API和系统调用
- 应用程序接口(API)与系统调用不同
- API只是一个函数定义
- 系统调用通过软件中断trap向内核发出一个明确的请求
- Libc库定义的一些API引用了封装例程(唯一目的是发布系统调用,直接调用函数就可以出发系统调用)
- 一般每个系统调用对应一个封装例程
- 库再用这些封装例程定义出给用户的API
- 不是每个API都对应一个特定的系统调用
- API可能直接提供用户态的服务,如一些数学函数
- 一个单独的API可能调用几个系统调用
- 不同的API可能调用了同一个系统调用
- 返回值
- 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
- -1在多数情况下表示内核不能满足进程的请求
- Libc中定义的errno变量包含特定的出错码
3. 系统调用的三层皮
xyz(API)、system_ call(中断向量)、sys_xyz(中断向量对应的中断服务程序)
4. 系统调用程序及服务例程
- 当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数
- 在Linux中是通过执行int $0x80来执行系统调用的, 这条汇编指令产生向量为128的编程异常
- Intel Pentium II中引入了sysenter指令(快速系统调用),2.6已经支持
- (系统调用号将xyz和sys_xyz关联起来)
- 传参:内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数
- 使用eax寄存器
5. 参数传递
- 系统调用也需要输入输出参数,例如
- 实际的值
- 用户态进程地址空间的变量的地址
- 甚至是包含指向用户态函数的指针的数据结构的地址
- system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号
- 一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即_NRfork)。
- 这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
- 进入sys_call之后,立即将eax的值压入内核堆栈
- 寄存器传递参数具有如下限制:
- 每个参数的长度不能超过寄存器的长度,即32位
- 在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp)
- 超过6个则将某一个寄存器作为一个指针指向一块内存,进入内存态后可以访问所有地址空间,可以通过那块内存传递数据
三、使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
1.使用库函数API获取系统当前时间
time.c
#include<stdio.h>
#include<time.h> int main()
{
time_t tt;
struct tm *t;//构造一个结构体,方便读取
tt = time(NULL);//time系统调用
t = localtime(&tt);
printf("time:%d:%d:%d:%d:%d:%d\n", t->tm_year+, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
return ;
}
2.使用C代码中嵌入汇编代码触发系统调用获取系统当前时间
time_asm.c
#include<stdio.h>
#include<time.h> int main()
{
time_t tt;
struct tm *t;
asm volatile(
"mov $0,%%ebx\n\t" # 把ebx清零,相当于传参数
"mov $0xd,%%eax\n\t" # 把0xd放入eax中,即系统调用号13,指time
"int $0x80\n\t"
"mov %%eax,%0\n\t" # 返回值是在eax中,%0指tt,把返回值放到tt中去。
: "=m" (tt)
);
t = localtime(&tt);
printf("time:%d:%d:%d:%d:%d:%d\n", t->tm_year+, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
return ;
}
用户态向内核态:
1.传递了一个系统调用号 - eax
2.传递了参数 - ebx
实验过程及截图
要求:
- 选择一个系统调用(13号系统调用time除外),系统调用列表参见http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl
- 参考视频中的方式使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用
我选择的是24号getuid。
getuid.c
#include <unistd.h>
#include <stdio.h> int main()
{
pid_t uu;
uu=getuid();
printf("uu = %d \n", uu);
return ;
}
getuid_asm.c
#include <unistd.h>
#include <stdio.h> int main()
{ pid_t uu;
uu = getuid();
asm volatile(
"mov $0x24,%%eax\n\t"
"int $0x80\n\t"
"mov %%eax,%0\n\t"
:"=m"(uu)
);
printf("uu = %d \n",uu);
return ; }
执行结果:
分析过程:
我们使用int 0x80触发中断,然后中断处理程序保存现场,我们的进程陷入内核态。
系统调用的工作机制是:当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数,由API、中断向量和中断处理程序协调完成。
而在系统调用的过程中,系统调用号使用eax寄存器传递参数。
寄存器在传递参数时的限制:
1)每个参数的长度不能超过寄存器的长度,即32位。
2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp),若超过6个,就将某一个寄存器作为指针,指向一块内存,在进入内核态之后,可以访问所有的地址空间,通过内存来传递参数。