王康 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
系统调用:操作系统中,程序员通过封装好的库函数来实现系统调用
前提
1,用户态内核态中断:
1,用户态内核态区分:(低级别即用户态)
为什么有权限级别划分?让系统本身更稳定的机制
如何区分?内核态是任意地址x86有4G内存地址空间;用户只能访问到0xbfffffff,c以上只能内核态
逻辑地址经过MMU转化为物理地址。
2,中断处理是从用户态进入内核态的主要方式
硬中断,或者系统调用陷入trap。
系统调用是一种特殊的中断。
同时也保存内核态esp,状态字,内核态当前中断处理程序入口指向system.call函数
iret对应中断信号或者int指令,那边是保存,iret就是恢复
3,中断完整过程
int 0x80是系统调用:1,保存了eip,当前ss堆栈段寄存器esp;eflags标志寄存器保存至内核堆栈
2,同时加载了ISR中断服务程序入口至cseip,然后当前esp指向内核堆栈段信息
3,SAVE_ALL完成后开始中断服务,可能会发生进程调度;
如果发生了进程调度,当前的SAVE_ALL都会暂时保存在系统里边,再切换回来时恢复
4,RESTORE_ALL,iret
2,系统调用概述
也防止了用户直接与操作系统打交道,提高安全性与可移植性(用户程序与具体硬件已经解耦和,被抽象接口替代了)
系统调用封装成了一个函数API,区分是:API只是为了便于系统调用而产生的,通过汇编也可以完成系统调用功能
软中断是通过trap方式有int请求的
xyz()是个API,内部封装了系统调用触发了int 0x80,这个中断向量对应着system_call内核代码入口起点。
服务程序sys_xyz执行完后ret_from_sys_call,这个过程中返回后则是进程调度的实际。
三层皮:
系统调用参数传递:
超过6个就把某一个寄存器做指针指向内存,通过那一块内存来传递(NR应该是NUMBER缩写,即)
eax传递的系统调用号即int 0xxx的这个号至少为一个参数
(eax传入的0Xd即为系统调用号13,int0x80调用system_call)
2,使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
tt是int型数值,需要变为年月日
tm是年月日格式struct
time是系统调用
之后变为tm格式
1,c代码中嵌入汇编代码写法
输出输入部分是参数,return也是一个参数部分
实例:
%%转义 ; 编译器自动帮你把val1的值放入ecx edx寄存器
=m 即写入内存变量
= 意思是操作数在指令中是只写的 ; + 是操作数为读写类型
r是通用寄存器
最后eax是破坏描述部分
3,用汇编方式触发系统调用获取系统当前时间
把ebx请0;eax传入的0Xd即为系统调用号13,int0x80调用system_call
如同之前c语言的time(null)
4,实验部分
方法一:使用API在屏幕上显示“hello world”
这个其实也是C语言经典的入门程序,源代码如下
1. #include "stdio.h"
2. #include "string.h"
3.
4. int main()
5. {
6. char* msg = "Hello World";
7. printf("%s", msg);
8. return 0;
9. }
gedit helloworld.c,新建并打开helloworld.c文件,在其中输入上面的代码,保存退出;
然后使用下面的指令编译链接程序:
1. gcc -o helloworld helloworld.c -m32
接着,运行编译好的程序,
./helloworld
效果如下:
方法二:使用C内嵌汇编代码在屏幕上输出helloworld
Linux中内嵌汇编代码:
1. int main()
2. {
3. char* msg = "Hello World";
4. int len = 11;
5. int result = 0;
6.
7. __asm__ __volatile__("movl %2, %%edx;\n\r" /*传入参数:要显示的字符串长度*/
8. "movl %1, %%ecx;\n\r" /*传入参赛:文件描述符(stdout)*/
9. "movl $1, %%ebx;\n\r" /*传入参数:要显示的字符串*/
10. "movl $4, %%eax;\n\r" /*系统调用号:4 sys_write*/
11. "int $0x80" /*触发系统调用中断*/
12. :"=m"(result) /*输出部分:本例并未使用*/
13. :"m"(msg),"r"(len) /*输入部分:绑定字符串和字符串长度变量*/
14. :"%eax");
15.
16. return 0;
17. }
使用gedit helloworld_asm.c新建文件,并输入上面的代码,使用下面的命令编译
gcc -o helloworld_asm helloworld_asm.c -m32
使用下面的命令运行
./helloworld_asm
运行效果如下
5,总结
即便是最简单的程序,也难免要用到诸如输入、输出以及退出等操作,而要进行这些操作则需要调用操作系统所提供的服务,也就是系统调用。除非你的程序只完成加减乘除等数学运算,否则将很难避免使用系统调用。在 Linux 平台下有两种方式来使用系统调用:利用封装后的 C 库(libc)或者通过汇编直接调用。
Linux 下的系统调用是通过中断(int 0x80)来实现的。在执行 int 80 指令时,寄存器 eax 中存放的是系统调用的功能号,而传给系统调用的参数则必须按顺序放到寄存器 ebx,ecx,edx,esi,edi 中,当系统调用完成之后,返回值可以在寄存器 eax 中获得。
所有的系统调用功能号都可以在文件 /usr/include/bits/syscall.h 中找到,为了便于使用,它们是用 SYS_<name> 这样的宏来定义的,如 SYS_write、SYS_exit 等。例如,经常用到的 write 函数是如下定义的:
ssize_t write(int fd, const void *buf, size_t count);
该函数的功能最终是通过 SYS_write 这一系统调用来实现的。根据上面的约定,参数 fb、buf 和 count 分别存在寄存器 ebx、ecx 和 edx 中,而系统调用号 SYS_write 则放在寄存器 eax 中,当 int 0x80 指令执行完毕后,返回值可以从寄存器 eax 中获得。
或许你已经发现,在进行系统调用时至多只有 5 个寄存器能够用来保存参数,难道所有系统调用的参数个数都不超过 5 吗?当然不是,例如 mmap 函数就有 6 个参数,这些参数最后都需要传递给系统调用 SYS_mmap:
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
当一个系统调用所需的参数个数大于 5 时,执行int 0x80 指令时仍需将系统调用功能号保存在寄存器 eax 中,所不同的只是全部参数应该依次放在一块连续的内存区域里,同时在寄存器 ebx 中保存指向该内存区域的指针。系统调用完成之后,返回值仍将保存在寄存器 eax 中。
由于只是需要一块连续的内存区域来保存系统调用的参数,因此完全可以像普通的函数调用一样使用栈(stack)来传递系统调用所需的参数。但要注意一点,Linux 采用的是 C 语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即最后一个参数先入栈,而第一个参数则最后入栈。如果采用栈来传递系统调用所需的参数,在执行int 0x80 指令时还应该将栈指针的当前值复制到寄存器 ebx中。