系统调用三层机制

用户态和内核态

intel x86 CPU有4种不同的执行级别,分别为0,1,2,3
按照intel的设想,内核运行在Ring0级别,驱动运行在Ring1和Ring2级别,应用运行在Ring3级别
linux系统中,只使用了0和3两个级别,分别对应内核态和用户态,使用寄存器CS:EIP的指向范围区分

  • 用户态下,只能访问0x00000000~0xBFFFFFFF的地址空间
  • 内核态下,可以指向任意地址,0xC0000000只能在内核态下访问

在32位x86机器上有4GB进程地址空间,MMU负责逻辑地址和物理地址的转换

| |内核空间1GB
| |<- 0xc0000000
| |
| |用户空间3GB
| |<- 0x00000000

中断

int指令触发中断机制(包括系统调用)
中断触发时,会在堆栈上保存寄存器的值,保存用户态栈顶地址、当时的状态字和CS:EIP
同时将内核态的栈顶地址、内核态的状态字载入CPU寄存器中,并将ES:EIP寄存器指向中断处理程序入口(对系统调用来说是system_call

int指令触发后,进入中断处理程序,开始执行内核代码SAVE_ALL保存现场
中断处理程序结束后,执行恢复现场操作,在3.18.6的x86-32内核中,restore_allINTERRUPT_RETURNiret)负责将中断时保存的用户态寄存器值恢复到当前CPU

系统调用

操作系统管理硬件,提高系统安全性,使得用户程序具有可移植性

  1. Linux下系统调用通过触发int 0x80中断完成
    中断保存了用户态CS:EIP的值,及当前堆栈段寄存器的栈顶,将EFLAGS寄存器的值保存到内核堆栈中
    同时将当前的中断信号或系统调用和终端服务例程的入口加载在CS:EIP中,将当前的堆栈段SS:ESP也加载到CPU中

  2. 触发系统调用及参数传递方式
    当Linux通过执行int 0x80触发系统调用时(Intel Pentium II还引入sysenter指令,Linux2.6后支持),进入内核,开始执行中断向量128对应的中断服务例程system_call
    用户态进程需要指明调用哪个系统调用,通过EAX寄存器传递系统调用号参数来区分

除了系统调用号外,系统调用可能需要传递其他参数,由于系统调用从用户态切换到内核态,使用不同的堆栈空间,无法通过压栈的方式传递参数,而是通过寄存器传递参数,参数个数若超过寄存器数量,可将某个寄存器作为指针指向内存,此时可以通过内存传递更多参数

使用库函数libc和C嵌入汇编代码触发同一个系统调用

使用库函数libc

#include<stdio.c>
#include<time.c>
int main(){
    time_t tt; // int
    struct tm* t;
    tt= time(NULL);
    t= localtime(&tt);
    printf("time:%d:%d:%d:%d:%d:%d\n",
            t->tm_year+1900, t->tm_mon, t->tm_mda,
            t->tm_hour, t->tm_min, t->tm_sec);
    return 0;
}

编译gcc -m32 time.c -o time

使用C嵌入汇编代码

#include<stdio.h>
#include<time.h>
int main(){
    time_t tt;
    struct tm* t;
    // 使用汇编代替 time(NULL)
    asm volatile(
        "mov $0, %%ebx\n\t"
        "mov $0xd, %%eax\n\t" // 传递系统调用号 13
        "int $0x80\n\t"       // 触发系统调用
        "mov %%eax, %0\n\t"
        : "=m" (tt)
    );
    t= localtime(&tt);
    printf("time:%d:%d:%d:%d:%d:%d\n",
            t->tm_year+1900, t->tm_mon, t->tm_mda,
            t->tm_hour, t->tm_min, t->tm_sec);

    return 0;
}

编译gcc time-asm.c -o time-asm -m32

含有两个参数的系统调用示例
重命名函数rename(),在内核中对应的系统调用内核处理函数sys_rename(),系统调用号为38
asmlinkage long sys_rename(const char __user* oldname, const char __user *newname);

使用库函数libc

#include<stdio.h>
int main(){
    int ret;
    char* oldname= "hello.c";
    char* newname= "newhello.c";
    ret= rename(oldname, newname);
    if(ret==0){
        printf("renamed successfully\n");
    }else{
        printf("unable to rename the file\n");
    }

    return 0;
}

使用C嵌入汇编代码

#include<stdio.h>
int main(){
    int ret;
    char* oldname= "hello.c";
    char* newname= "newhello.c";

    asm volatile(
        "movl %2, %%ecx\n\t"
        "movl %1, %%ebx\n\t"
        "movl $0x26, %%eax\n\t"
        "int $0x80"
        : "=a" (ret)
        : "b" (oldname), "c" (newname)
    );

    if(ret==0){
        printf("renamed successfully\n");
    }else{
        printf("unable to rename the file\n");
    }

    return 0;
}

EAX用来传递系统调用号,其他参数按顺序赋给EBX,ECX,EDX,ESI,EDI,EBP
将系统调用号38存入EAX寄存器,将oldname存入EBX寄存器,将newname存入ECX寄存器,由于参数是字符串,实际传递的是指针变量
通过执行int $0x80指令来执行系统调用,进入内核态,system_call()根据系统调用号在系统调用列表中查找对应的系统调用内核函数sys_rename(),执行完将结果存入EAX寄存器,再将EAX寄存器的值传给ret

使用gdb跟踪MenuOS系统调用过程

timetime-asm命令集成到MenuOS中

rm -rf menu
git clone https://github.com/mengning/menu.git
make rootfs

通过在test.c:main()中增加两行代码,来给MenuOS增加两个命令

MenuConfig("time");
MenuConfig("time-asm");

使用gdb跟踪系统调用内核函数

调试time命令所用到的系统调用内核处理函数

cd ..
qemu-system-i386 -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -S -s

启动内核后,先启动gdb,再加载内核

(gdb) file vmlinux # cd linux-3.18.6
(gdb) target remote:1234

time()系统调用是系统调用号13对应的内核处理函数,即sys_time

(gdb) b sys_time
(gdb) c

此时,在已经启动的 MenuOS 中执行time-asm命令,程序会停在sys_time

sys_time位于kernel/time/time.c,使用宏实现,所以无法直接看到sys_time

单步执行,会进入get_seconds(),位于kernel/time/timekeeping.c

使用gdb的finish命令将该函数执行完,再单步执行,直到return i,即获取到系统时间

若继续单步调试,会出现cannot find bounds of current function
这里的代码比较特殊,不好调试,因为这时会返回到system_call位置的汇编代码,完成恢复现场并返回到用户态

当执行int 0x80时,实际上会跳转到system_call(),可以直接将断点设在该处
该函数是汇编代码,位于arch/x86/kernel/entry_32.S#490
当执行time-asm命令时,并不能在system_call()处停下,该函数不是一个正常的C函数,gdb不支持跟踪汇编代码

ENTRY(system_call)
system_call还有一个函数原型声明,是一段汇编代码的起点,内部没有遵守函数调用堆栈机制,所有gdb不能跟踪
该段代码是理解Linux运作过程的关键,系统调用作为一种特殊的中断,其执行过程可以类推到其他中断信号触发的中断服务处理过程,下面分析:系统调用time在内核代码中的处理过程
time -> system_call -> sys_time
system_call还涉及一个进程调度时机

中断向量0x80和system_call中断服务程序入口
start_kernel()调用了trap_init(),该函数调用了set_system_trap_gate()

#ifdef CONFIG_X86_32
    set_system_trap_gate(SYSCALL_VECTOR, &system_call);
    set_bit(SYSCALL_VECTOR, used_vectors);
#endif

这是trap_init()中的一段代码,位于arch/x86/kernel/traps.c#838
其中,system_call被声明为一个函数,是汇编代码的入口
通过set_system_trap_gate()绑定中断向量0x80和system_call中断服务程序入口,一旦执行int 0x80则自动跳转到system_call
SYSCALL_VECTOR时系统调用中断向量0x80,位于arch/x86/include/asm/irq_vectors.h#49

#define IA32_SYSCALL_VECTOR 0x80
#ifdef CONFIG_X86_32
#define SYSCALL_VECTOR      0x80
#endif

后面再分析int指令执行或中断信号发生时,CPU的具体行为

system_call汇编代码和系统调用内核处理函数
system_call和其他中断一样,也有保存现场SAVE_CALL和恢复现场restore_all
代码中的sys_call_table是一个系统调用表,EAX寄存器传递系统调用号,在调用时会根据该值调用对应的系统调用内核处理函数,在退出时会进入syscall_exit_work,此时有一个进程调度时机
system_call

ENTRY(system_call)
    RING0_INT_FRAME
    ASM_CLAC
    pushl_cfi %eax        # 保存系统调用号
    SAVE_ALL              # 保存现场,将寄存器值压栈
    GET_THREAD_INFO(%ebp) # ebp用于存放当前进程thread_info结构的地址
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
    jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax # 检查系统调用号,系统调用号应小于NR_syscalls
    jae syscall_badsys    # 不合法,跳转到异常处理
syscall_call:
    call *sys_call_table(, %eax, 4) # 通过系统调用号在表中找到对应的系统调用内核处理函数
    movl %eax, PT_EAX(%esp)         # 保存返回值到栈
syscall_exit:
    testl $_TIF_ALLWORK_MASK,%ecx # 检查是否有任务需要处理
    jne syscall_exit_work         # 若需要,进入syscall_exit_work,这里是最常见的进程调度时机
restore_all:
    TRACE_ITQS_IRET  # 恢复现场
irq_return:
    INTERRUPT_RETURN # iret

在syscall_call中判断当前任务是否需要处理,若需要,进入syscall_exit_work,这里是最常见的进程调度时机
sys_call_table(,%eax,4) 通过系统调用号在表中找到对应的系统调用内核处理函数
系统调用表中每个表项占4字节,所以先将系统调用号(EAX寄存器)乘4,再加上表起始地址,即得到对应的系统调用内核处理函数指针
sys_call_table分派表由一段脚本根据arch/x86/syscalls/syscall_32.tbl自动生成

整体上理解系统调用内核处理过程

Syntax error in textmermaid version 11.4.1

流程图中涉及system_call_exit内部处理,大致过程是需要跳转到work_pending,里面有work_notifysig处理信号,还有work_resched需要重新调度,这里是进程调度时机点call_schedule,调度结束会跳转到restore_all恢复现场返回系统调用到用户态,位于arch/x86/kernel/entry_32.S#593

ENTRY(system_call)
	RING0_INT_FRAME			# can't unwind into user space anyway
	ASM_CLAC
	pushl_cfi %eax			# save orig_eax
	SAVE_ALL
	GET_THREAD_INFO(%ebp)
					# system call tracing in operation / emulation
	testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
	jnz syscall_trace_entry
	cmpl $(NR_syscalls), %eax
	jae syscall_badsys
syscall_call:
	call *sys_call_table(,%eax,4)
syscall_after_call:
	movl %eax,PT_EAX(%esp)		# store the return value
syscall_exit:
	LOCKDEP_SYS_EXIT
	DISABLE_INTERRUPTS(CLBR_ANY)	# make sure we don't miss an interrupt
					# setting need_resched or sigpending
					# between sampling and the iret
	TRACE_IRQS_OFF
	movl TI_flags(%ebp), %ecx
	testl $_TIF_ALLWORK_MASK, %ecx	# current->work
	jne syscall_exit_work

restore_all:
	TRACE_IRQS_IRET
restore_all_notrace:
#ifdef CONFIG_X86_ESPFIX32
	movl PT_EFLAGS(%esp), %eax	# mix EFLAGS, SS and CS
	# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
	# are returning to the kernel.
	# See comments in process.c:copy_thread() for details.
	movb PT_OLDSS(%esp), %ah
	movb PT_CS(%esp), %al
	andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
	cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
	CFI_REMEMBER_STATE
	je ldt_ss			# returning to user-space with LDT SS
#endif
restore_nocheck:
	RESTORE_REGS 4			# skip orig_eax/error_code
irq_return:
	INTERRUPT_RETURN

从系统调用处理过程的入口开始,SAVE_ALL保存现场,然后找到syscall_badsys和sys_call_table
call*sys_call_table(,%eax,4)就是调用了系统调用的内核处理函数,之后restore_all和INTERRUPT_RETURN(iret)用于恢复现场并返回系统调用到用户态,这个过程中可能会执行syscall_exit_work,里面有work_pending,其中的work_notifysig用来处理信号,work_pending还有可能调用schedule

posted @   sgqmax  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示