操作系统 1.5-系统调用的实现
不能直接跳转到内核执行代码
首先,我们提出一个系统调用的只管实现:应用程序能否直接访问操作系统内核中的数据,如特定的字符串。
答案是否定的。这种直接访问的不安全性,因为这段内核数据包含着系统信息,直接访问可能泄露重要的系统信息,如root用户的密码,以及其他用户的敏感数据。
因此,不能随意调用内核数据和账户,以确保系统的安全。
怎么做保证不能直接跳转到内核
总的来讲,就是将内核程序和用户程序隔离!!!
当执行一段代码的时候,系统会判断当前程序执行在什么态(哪层环)?
区分内核态和用户态:,一种处理器“硬件设计”。
由于CS:IP是当前指令所以用CS的最低两位来表示:0是内核态,3是用户态
在操作系统中,特权级是一个重要的概念,用于区分不同级别的代码执行权限。特权级确保了操作系统的稳定性和安全性,防止低权限的代码访问或修改高权限的资源。特权级通常分为多个级别,例如在x86架构中,常见的是0到3的四个级别,其中0级是最高特权级,通常用于操作系统内核,而3级是最低特权级,用于用户程序。
当前特权级(CPL)和目标特权级(DPL)
-
当前特权级(CPL):表示当前执行代码的特权级。在x86架构中,CPL由代码段寄存器(CS)的最低两位表示。CPL决定了当前执行代码可以访问的资源和执行的操作。
-
目标特权级(DPL):表示目标代码段或门描述符的特权级。当一个程序尝试进入一个新的代码段时,操作系统会检查该代码段的DPL,以确定是否允许当前CPL的代码进入该段。
在指令执行中,如果CPL低于或等于目标代码段的DPL,那么跳转或访问是允许的。如果CPL高于DPL,那么跳转或访问将被阻止,以防止低权限代码访问高权限资源。
CS(代码段)和IP(指令指针)
-
代码段寄存器(CS):用于存储当前执行代码段的段地址。CS不仅用于定位代码在内存中的位置,还包含了当前代码的特权级信息(CPL)。
-
指令指针(IP):用于存储下一条指令的地址。IP与CS结合,形成了程序计数器(PC),PC指向了当前执行指令的内存地址。
当程序执行跳转指令时,新的IP值会被加载到IP寄存器中,同时,如果跳转目标的代码段与当前代码段不同,CS寄存器也会被更新为新代码段的段地址。这样,CS和IP共同构成了PC,指向了新的执行位置。
在操作系统中,CS的值不仅决定了代码的内存位置,还决定了执行该代码的特权级。操作系统通过控制CS和IP的值,来控制程序的执行流程和访问权限,从而确保系统的安全性和稳定性。
流程分析
-
系统初始化:
-
在操作系统启动时(如通过head.S等引导程序),会初始化全局描述符表(GDT)。
-
GDT中会定义用户态和内核态的段描述符,内核态的段描述符的特权级(DPL)设置为0。
-
-
用户程序执行:
-
用户程序开始执行时,其代码段(CS)的特权级(CPL)被设置为3(用户态)。
-
在用户态下,程序只能访问用户态的资源,不能直接访问内核态的资源。
-
-
系统调用:
-
当用户程序需要执行系统调用时,它会执行一条特殊的中断指令(如
int 0x80
)。 -
这条指令会触发从用户态到内核态的转换,CPL从3变为0。
-
-
中断处理:
-
中断指令会使得CPU查找中断描述符表(IDT)中的相应中断处理程序。
-
IDT中的中断处理程序的DPL允许从用户态调用(例如,DPL为0)。
-
中断处理程序开始执行,此时程序处于内核态(CPL为0),可以访问所有资源。
-
-
执行系统调用:
-
在内核态中,操作系统会根据系统调用号(通常在EAX寄存器中)查找系统调用表(如syscall table)。
-
找到对应的系统调用处理函数,并执行相应的内核操作。
-
-
返回用户态:
-
系统调用完成后,需要从内核态返回用户态。
-
CPU会将CPL从0恢复为3,继续执行用户程序。
-
-
访问控制:
-
在整个过程中,操作系统通过检查CPL和DPL来控制对资源的访问。
-
如果CPL高于或等于DPL,则允许访问;如果CPL低于DPL,则拒绝访问,防止用户态程序非法访问内核态资源。
-
硬件提供了主动进入内核的方法
硬件提供了一种机制,允许用户程序主动进入内核执行系统调用,这种方法就是中断。在计算机体系结构中,中断是一种硬件特性,它允许CPU在执行过程中被外部事件(如I/O操作完成、计时器到期或用户程序的特定指令)所打断,从而转而执行一个预先定义的中断处理程序。
以下是详细解释硬件如何提供主动进入内核的方法:
-
中断描述符表(IDT):
-
硬件通过中断描述符表(IDT)来管理中断。IDT是一个数据结构,它包含了中断处理函数的地址和相关信息。
-
当一个中断发生时,CPU会查找IDT来确定应该执行哪个中断处理程序。
-
-
中断指令(如
int 0x80
):-
用户程序通过执行特定的中断指令来触发系统调用。例如,在x86架构中,
int 0x80
是一个用于Linux系统调用的中断指令。 -
当执行这条指令时,CPU会识别这是一个中断请求,并根据IDT中的信息跳转到相应的中断处理程序。
-
-
特权级检查:
-
在x86架构中,每个段描述符(包括代码段和数据段)都有一个特权级(DPL)字段,它定义了访问该段所需的特权级。
-
当前特权级(CPL)由代码段寄存器(CS)的最低两位表示,它表示当前执行代码的特权级。
-
只有当CPL小于或等于DPL时,才能访问该段。这是硬件提供的保护机制,防止低特权级的代码访问高特权级的资源。
-
-
从用户态到内核态的转换:
-
当一个系统调用被触发时,如果CPL高于目标段的DPL,硬件会阻止访问。但如果CPL小于或等于DPL,硬件会允许访问,并开始执行中断处理程序。
-
在执行中断处理程序时,硬件会自动将CPL设置为0(内核态),从而允许访问内核态的资源。
-
-
中断处理程序:
-
中断处理程序是操作系统内核的一部分,它负责处理特定的中断请求。
-
在系统调用的情况下,中断处理程序会根据传递的系统调用号(通常存储在寄存器中)来确定要执行哪个系统调用,并执行相应的内核函数。
-
-
返回用户态:
-
一旦系统调用完成,中断处理程序会将控制权返回给用户程序。硬件会自动将CPL恢复为用户态的值,通常是3。
-
通过这种方式,硬件提供了一种安全的方法,允许用户程序主动请求操作系统内核的服务,同时确保了内核的安全性和稳定性。这种机制是操作系统能够提供系统调用功能的基础。
系统调用的实现过程
总体讲述
这张幻灯片展示了系统调用的实现过程,以 printf
函数为例,说明了从应用程序到操作系统内核的调用链。以下是详细的解释:
-
应用程序层:
-
应用程序直接调用
printf
函数来输出格式化的文本。
-
-
C函数库层:
-
printf
函数实际上是C函数库中的一个函数,它负责处理格式化字符串并准备要输出的数据。 -
当
printf
需要将数据写入标准输出时,它会调用C函数库中的write
函数。
-
-
操作系统内核层:
-
C函数库中的
write
函数通过系统调用来请求操作系统内核的服务。 -
系统调用是通过中断指令(如
int 0x80
)实现的,这使得程序能够从用户态切换到内核态。
-
-
系统调用的实现:
-
在Linux系统中,系统调用是通过
syscall
宏来实现的,该宏在unistd.h
头文件中定义。 -
例如,
_syscall3
宏用于生成系统调用的代码,它接受函数类型、函数名和参数列表。 -
在
write.c
文件中,_syscall3
宏被用来定义write
函数的系统调用版本,该函数调用内核中的write
系统调用。
-
-
最终的系统调用:
-
最终,这些库函数和宏展开成包含
int
指令的汇编代码,这些代码在执行时会触发中断,从而进入操作系统内核。 -
在内核中,相应的系统调用处理程序(如内核中的
write
函数)会被执行,以完成实际的写操作。
-
这个过程展示了从用户空间到内核空间的转换,以及如何通过系统调用来请求操作系统服务。这是操作系统提供给应用程序的一种安全、可控的访问内核功能的方式。
write函数详解
图片 1:
// 在 linux/include/unistd.h 中
#define _syscall3(type, name, atype, a, btype, b, ctype, c) \
type name(atype a, btype b, ctype c) \
{ \
long __res; \
__asm__ volatile("int 0x80" : "=a" (__res) : "0" (__NR_##name), \
"b" ((long)(a)), "c" ((long)(b)), "d" ((long)(c))); \
if (__res >= 0) return (type)__res; errno=-__res; return -1; \
}
#define __NR_write 4 // 系统调用号,放在 eax 中
图片 2:
// 在 linux/include/asm/system.h 中
#define set_system_gate(n, addr) \
_set_gate(&idt[n], 15, 3, addr); // idt 是中断向量表基址
#define _set_gate(gate_addr, type, dpl, addr) \
__asm__("movw %%dx,%%ax\n\t" "movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" "movl %%edx,%2": \
:"i"((short)(0x8000+(dpl<<13)+type<<8))), "o"(*((char*)(gate_addr))), \
"o"(*(4+(char*)(gate_addr))), "d"((char*)(addr)), "a"(0x00080000))
图片 3:
// 在 linux/kernel/system_call.s 中
nr_system_calls=72
.globl _system_call
_system_call: cmpl $nr_system_calls-1, %eax
ja bad_sys_call
push %ds push %es push %fs
pushl %edx pushl %ecx pushl %ebx // 调用的参数
movl $0x10, %edx mov %dx, %ds mov %dx, %es // 内核数据
movl $0x17, %edx mov %dx, %fs // fs 可以找到用户数据
call _sys_call_table(, %eax, 4) // a(, %eax, 4) = a + 4*%eax
pushl %eax // 返回值压栈,留着 ret_from_sys_call 时用
// 其他代码 ...
ret_from_sys_call: popl %eax, 其他 pop, iret
图片 4:
// 在 include/linux/sys.h 中
fn_ptr sys_call_table[] = {sys_setup, sys_exit, sys_fork, sys_read, sys_write, ...};
// 在 include/linux/sched.h 中
typedef int (fn_ptr*)();
总结每张图片的内容
图片 1:
-
定义了
_syscall3
宏,用于生成具有三个参数的系统调用包装函数。 -
使用内联汇编触发
int 0x80
中断,将系统调用号放入eax
,参数放入ebx
、ecx
和edx
。 -
__NR_write
宏定义了write
系统调用的编号为4
。
图片 2:
-
定义了
set_system_gate
和_set_gate
宏,用于设置中断描述符表(IDT)中的中断门。 -
set_system_gate
用于设置特定中断号(如0x80
)的中断处理函数地址。
图片 3:
-
定义了
system_call
中断处理程序,处理int 0x80
中断。 -
保存寄存器状态,设置内核数据段和用户数据段寄存器。
-
调用
_sys_call_table
函数表,根据eax
中的系统调用号调用相应的处理函数。 -
处理完成后,返回用户空间。
图片 4:
-
定义了
sys_call_table
全局函数数组,包含所有系统调用的处理函数。 -
sys_write
对应的数组下标为4
,与__NR_write
一致。
一起起到的作用
这些代码片段共同实现了 Linux 系统中用户空间程序通过系统调用来请求内核服务的完整过程:
用户空间到内核空间的转换:
用户程序通过 _syscall3
宏生成的包装函数(如 write
)触发 int 0x80
中断,进入内核空间。
中断处理:
system_call
中断处理程序被调用,保存寄存器状态,设置内核数据段和用户数据段寄存器。
系统调用分发:
根据 eax
中的系统调用号,从 sys_call_table
函数表中查找并调用相应的处理函数(如 sys_write
)。
参数传递和返回值处理:
参数通过寄存器传递给系统调用处理函数,返回值通过 eax
返回给用户空间程序。
中断门设置:
set_system_gate
宏用于初始化 IDT 中的中断门,将 int 0x80
中断的处理函数地址设置到 IDT 中。
通过这些步骤,用户空间程序可以安全地请求内核执行特定的操作,如文件写入,同时确保了操作的安全性和错误处理的一致性。这种机制是现代操作系统中用户空间与内核空间交互的基础。
执行总结代码
这张图展示了用户空间程序如何通过系统调用与内核空间进行交互的过程。我们可以分步骤来解释图中的代码和流程:
用户空间代码
-
main() 函数:
main()
{
eax = 72;
int 0x80;
}
-
eax = 72;
:将寄存器eax
设置为 72。在 Linux 系统中,eax
寄存器通常用于存放系统调用号。这里的 72 对应于whoami
系统调用。 -
int 0x80;
:触发一个软件中断,进入内核模式,执行系统调用。int 0x80
是 x86 架构下常用的系统调用中断指令。
-
whoami() 函数:
whoami()
{
printf(100, 8);
}
-
printf(100, 8);
:调用printf
函数,打印地址 100 处的字符串,长度为 8 个字符。这里的地址 100 对应字符串"lizhijun"
。
内核空间代码
-
_system_call:
_system_call:
call sys_whoami
//sys_call_table + eax*4
-
call sys_whoami
:调用sys_whoami
函数。这里通过eax
寄存器中的系统调用号(72)来查找系统调用表sys_call_table
中对应的系统调用函数。
-
sys_whoami() 函数:
sys_whoami()
{
printk(100, 8);
}
-
printk(100, 8);
:在内核中打印地址 100 处的字符串,长度为 8 个字符。这里的地址 100 也对应字符串"lizhijun"
。
总结
-
用户空间:
main
函数通过设置eax
寄存器为 72 并触发int 0x80
中断,请求执行whoami
系统调用。 -
内核空间:内核通过
eax
寄存器中的系统调用号找到对应的sys_whoami
函数,并执行该函数,最终在内核中打印字符串"lizhijun"
。
总结
为什么不能直接跳转到内核执行代码?
-
内核空间与用户空间的隔离:
-
原因:内核空间包含系统的核心数据和功能,直接访问可能导致安全问题(如泄露敏感信息)和稳定性问题(如用户程序错误地修改内核数据)。
-
实现方式:通过硬件支持的特权级(如 x86 架构中的环 0 和环 3)和软件机制(如段选择子和页表保护)来隔离内核空间和用户空间。
-
-
特权级的作用:
-
CPL(Current Privilege Level):当前执行代码的特权级,由段选择子的低两位决定。
-
DPL(Descriptor Privilege Level):目标代码段的特权级,定义在段描述符中。
-
规则:如果 CPL ≤ DPL,允许访问;否则,访问被拒绝。
-
-
CS(代码段)和 IP(指令指针):
-
决定程序的执行位置和权限。
-
通过控制 CS 和 IP,操作系统可以限制程序的执行范围和权限。
-
硬件如何支持安全进入内核?
-
中断机制:
-
原理:硬件提供了一种机制,允许用户程序通过特定的中断(如
int 0x80
)主动进入内核空间。 -
作用:用户程序通过中断请求内核服务,而内核通过中断处理程序提供服务,同时保证安全性和隔离性。
-
-
中断门(Interrupt Gate):
-
定义在 IDT(中断描述符表)中,用于控制中断的访问权限。
-
通过设置中断门的 DPL,可以限制哪些特权级可以触发特定的中断。
-
系统调用的实现过程
1. 用户空间的系统调用请求
-
用户程序通过系统调用接口(如
write
函数)请求内核服务。 -
示例代码:
#define _syscall3(type, name, atype, a, btype, b, ctype, c) \ type name(atype a, btype b, ctype c) \ { \ long __res; \ __asm__ volatile("int 0x80" : "=a" (__res) : "0" (__NR_##name), \ "b" ((long)(a)), "c" ((long)(b)), "d" ((long)(c))); \ if (__res >= 0) return (type)__res; errno=-__res; return -1; \ }
-
通过
int 0x80
触发中断,进入内核空间。
2. 内核空间的中断处理
-
中断处理程序
_system_call
被触发。 -
示例代码:
_system_call: cmpl $nr_system_calls-1, %eax ja bad_sys_call push %ds push %es push %fs pushl %edx pushl %ecx pushl %ebx movl $0x10, %edx mov %dx, %ds mov %dx, %es movl $0x17, %edx mov %dx, %fs call _sys_call_table(, %eax, 4) pushl %eax ret_from_sys_call: popl %eax iret
-
保存用户空间的寄存器状态。
-
根据
eax
中的系统调用号,从sys_call_table
中查找对应的处理函数。
3. 系统调用表和处理函数
-
系统调用表
sys_call_table
包含所有系统调用的处理函数指针。 -
示例代码:
fn_ptr sys_call_table[] = {sys_setup, sys_exit, sys_fork, sys_read, sys_write, ...};
-
内核调用对应的系统调用处理函数(如
sys_write
)。
4. 参数传递和返回值
-
参数通过寄存器传递给系统调用处理函数。
-
返回值通过
eax
返回到用户空间。