LINUX内核分析第四周学习总结——扒开应用系统的三层皮(上)
张忻(原创作品转载请注明出处)
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、知识概要
(一)用户态、内核态和中断处理过程
(二)系统调用概述
系统调用概述和系统调用的三层皮
(三)使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
使用库函数API获取系统当前时间
C代码中嵌入汇编代码的方法(复习)
使用C代码中嵌入汇编代码触发系统调用获取系统当前时间
二、学习笔记
(一)用户态、内核态和中断处理过程
- 一般现代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和系统调用
应用编程接口(application program interface, API) 和系统调用是不同的
- API只是一个函数定义
- 系统调用通过软中断向内核发出一个明确的请求
Libc库定义的一些API引用了封装例程 (wrapper routine,唯一目的就是发布系统调用)
- 一般每个系统调用对应一个封装例程
- 库再用这些封装例程定义出给用户的API
不是每个API都对应一个特定的系统调用。
- API可能直接提供用户态的服务。如,一些数学函数
- 一个单独的API可能调用几个系统调用
- 不同的API可能调用了同一个系统调用
返回值
- 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
- -1在多数情况下表示内核不能满足进程的请求
- Libc中定义的errno变量包含特定的出错码
3.应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系
系统调用的三层皮:xyz、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 <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <stdio.h> 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 <stdio.h> #include <sys/types.h> #include <sys/syscall.h> #include <errno.h> 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.总结
对“系统调用的工作机制”的理解。
可以通过库函数API使用系统调用或者用汇编方式触发系统调用。
参考资料:http://www.linuxidc.com/Linux/2014-12/110238.htm