LINUX内核分析第四周学习总结——扒开应用系统的三层皮(上)
黄韧(原创作品转载请注明出处)
《Linux内核分析》MOOC课程
【知识点梳理】
(一)用户态、内核态和中断处理过程
1.内核态:在高的执行级别下,代码可以执行特权指令,访问任意的物理地址,这时的CPU就对应内核态
2.用户态:在低级别的指令状态下,代码 只能在级别允许的特定范围内活动。在日常操作下,执行系统调用的方式是通过库函数,库函数封装系统调用,为用户提供接口以便直接使用。
3.在Linux下0级表示内核态,3级表示用户态。
4.内核态cs:eip的值是任意的,即可以访问所有的地址空间。用户态只能访问其中的一部分内存地址。
5.中断处理是从用户态进入内核态的主要方式,系统调用是一种特殊的中断。中断/int指令会在堆栈上保存用户态的寄存器上下文,其中包括用户态栈顶地址、当时的状态字、cs:eip的值,以及内核态的栈顶地址、当时的状态字、中断处理程序入口。中断处理结束前的最后一件事就是恢复现场,退出中断程序,恢复保存寄存器的数据。
例:
interrupt(ex:int 0X80)//发生系统调用 save
cs:eip/ss:esp/eflags(current)to kernel stack //保存cs:eip的值,保存当前堆栈段寄存器当前栈顶和标志位寄存器 load cs:eip(entry of a specific ISR)andss:eip(point to kenerl stack) //把当前的中断信号或系统调用相关中断服务例程入口加载到cs:eip中,把当前的堆栈段和esp加载到CPU
SAVE_ALL//保存现场 ...//内核代码,完成中断服务,发生进程调度
RESTORE_ALL//恢复现场
iret - pop cs:eip/ss:esp/eflags from kernel stack//iret对应相反的中断指令
- 一般现代CPU都有几种不同的指令执行级别。
- 在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态。
- 而在相应的低级别执行状态下,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动。
- 举例:Intel x86CPU有四种不同的执行级别0-3,Linux只使用了其中的0 3级分别表示内核态和用户态
- cs寄存器的最低两位表明了当前代码的特权级。
- CPU每条指令的读取都是通过cs:eip这两个寄存器:其中cs是代码段选择寄存器,eip是偏移量寄存器。
- 上述判断由硬件完成。
- 一般来说在Linux中,地址空间是一个显著地标志:0xc0000000以上的地址空间只能在内核态下访问,都可以访问0x00000000-0xbfffffff的地址空间在两种状态下。注意:这里说的地址空间是逻辑地址而不是物理地址。
- 寄存器上下文 从用户态切换到内核态时,必须要保存用户态的寄存器上下文。
- 中断/int指令会在堆栈上保存一些寄存器的值。如:用户态栈顶地址、当时的状态字、当时的cs:eip的值。
中断发生后的第一件事就是保存现场,结束前最后一件事是恢复现场。
- 保护现场就是进入中断程序 保存需要用到的寄存器的数据。
- 恢复现场就是推出中断程序 恢复保存寄存器的数据。
中断处理的完整过程
- interrupt(ex:int 0x80)-save
- SAVE_ALL
- RESTORE_ALL
- iret-pop cs:eip/ss:esp/eflags from kernel stack
(二)系统调用概述
系统调用概述和系统调用的三层皮
1.系统调用的意义
操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用
- 把用户从底层的硬件编程中解放出来
- 极大的提高了系统的安全性
- 使用户程序具有可移植性
2.API和系统调用
-
API是一个系统调用封装成的一个函数定义
-
系统调用通过软中断向内核发出一个明确的请求
-
Libc库定义的一些API引用了封装例程,目的是发布系统调用,让程序员写代码的时候可以通过函数调用而非汇编指令触发一个系统调用
-
一般每个系统调用对应一个封装例程,库再用这些封装例程定义出给用户的API
应用编程接口(application program interface, API) 和系统调用是不同的
不是每个API都对应一个特定的系统调用。
- API可能直接提供用户态的服务。如,一些数学函数
- 一个单独的API可能调用几个系统调用
- 不同的API可能调用了同一个系统调用
返回值
- 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
- -1在多数情况下表示内核不能满足进程的请求
- Libc中定义的errno变量包含特定的出错码
3.应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系
系统调用的三层皮:xyz(API)、system_call(中断向量)、sys_xyz(中断服务程序)
(1)系统调用程序及服务例程
当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。
- 在Linux中是通过执行int $0x80来执行系统调用的, 这条汇编指令产生向量为128的编程异常
- Intel Pentium II中引入了sysenter指令(快速系统调 用),2.6已经支持(本课程不考虑这个)
传参:
- 内核实现了很多不同的系统调用, 进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数(系统调用号将xyz和sys_xyz关联起来了)
- 使用eax寄存器
(2)参数传递
系统调用也需要输入输出参数,例如- 实际的值
- 用户态进程地址空间的变量的地址
- 甚至是包含指向用户态函数的指针的数据结构的地址
system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数 ,即由eax传递的系统调用号
- 一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值 置为2(即__NR_fork)。
- 这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
- 进入sys_call之后,立即将eax的值压入内核堆栈
- 寄存器传递参数具有如下限制: 1)每个参数的长度不能超过寄存器的长度,即32位 2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx, ecx,edx,esi,edi,ebp) 3)超过6个怎么办?把某一个寄存器作为一个指针,指向某一块内存。
(三)使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
1.使用库函数API获取系统当前时间
2.C代码中嵌入汇编代码的方法(复习)
3.使用C代码中嵌入汇编代码触发系统调用获取系统当前时间
三、作业
1.实验过程
分析汇编代码调用系统调用的工作过程,特别是参数的传递的方式等。
(1)通过库函数chomd函数改变文件的权限为只读
代码如下:
#include#include #include #include int main(){ int rc; rc = chmod("/etc/passwd", 0444); if (rc == -1) fprintf(stderr, "chmod failed, errno = %d\n", errno); else printf("chmod success!\n"); return 0;}
在普通用户下编译运用,输出结果为:
上面系统调用返回的值为-1,说明系统调用失败,错误码为1,
即无权限进行该操作,我们以普通用户权限是无法修改 /etc/passwd 文件的属性的,结果正确。
(2)使用C代码中嵌入汇编代码触发系统调用改变文件的权限为只读
代码如下:
#include#include #include #include int main(){ long rc; char *file_name = "/etc/passwd"; unsigned short mode = 0444; asm( "int $0x80" : "=a" (rc) : "0" (SYS_chmod), "b" ((long)file_name), "c" ((long)mode) ); if ((unsigned long)rc >= (unsigned long)-132) { errno = -rc; rc = -1; } if (rc == -1) fprintf(stderr, "chmode failed, errno = %d\n", errno); else printf("success!\n"); return 0;}
如果 eax 寄存器存放的返回值(存放在变量 rc 中)在 -1~-132 之间,就必须要解释为出错码(在/usr/include/asm-generic/errno.h
文件中定义的最大出错码为 132),这时,将错误码写入 errno 中,置系统调用返回值为 -1;否则返回的是 eax 中的值。
结果如图:
上面程序在 32位Linux下以普通用户权限编译运行结果与前面两个相同。
2.总结