首页 > 其他分享 >系统调用三层机制

系统调用三层机制

时间:2024-12-05 13:10:26浏览次数:7  
标签:调用 系统 system call 内核 time 机制 三层

用户态和内核态

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自动生成

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

ENTRY(system_call) SAVE_ALL cmpl $(nr_syscalls),%eax 否-> syscall_badsys 是 call*sys_call_table(,%eax,4) movl %eax,PT_EAX(%esp) syscall_exit 否-> restore_all -> iret 是 syscall_exit_work work_pending work_resched restore_all iret

流程图中涉及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

标签:调用,系统,system,call,内核,time,机制,三层
From: https://www.cnblogs.com/sgqmax/p/18588322

相关文章

  • 如何使用js去调用vscode-js-debugger的方法去调试网页?
    ......
  • 使用 Rust 调用 YOLOv3 模型进行物体检测
    环境准备在Rust中,我们可以通过调用C库或使用绑定来加载YOLOv3模型。为了简单起见,我们将使用Rust的opencvcrate进行图像处理,并借助YOLOv3模型进行推理。安装依赖:Rust环境:首先,确保你已经安装了Rust开发环境,使用以下命令安装Rust:bashcurl--proto'=https'......
  • 技术团队远景的三层进化:从底线思维到业务赋能
    在当今快速变化的商业环境中,技术团队不仅是企业的后台支持,更是驱动业务创新和增长的核心动力。那么,如何构建一个具有前瞻性的技术团队远景呢?我的观点是三层进化模型:底线思维、支撑业务、业务赋能。下面让我们一起来探讨这三个层次,以及如何通过可量化的指标来实现和衡量它们。第......
  • 【一文读懂】SPI机制之JAVA的SPI实现详解
    ......
  • 元类,类,实例调用__call__方法
    实例可以调用双下划线call方法;类也可以调用双下划线call方法,但二者有大的差别;实例调用双下划线call方法是在实例及类内部查找,如果没有,直接报错TypeError类调用双下划线call方法会在实例中查找元类(type)内部具有__call__方法1. 类调用触发元类的__call__:• 类的调用行为(MyClas......
  • 使用 C++ 调用 YOLOv3 模型进行物体检测
    环境准备首先,确保你已经安装了以下工具:OpenCV:用于图像处理。Darknet:用于YOLO模型的推理。C++编译器:如g++。2.安装Darknet克隆Darknet仓库并进入目录:bashgitclonehttps://github.com/pjreddie/darknet.gitcddarknet使用Makefile编译Darknet(如果使用GPU......
  • 接口调用频率太高,如何做好接口的限制呢?
    前端限制接口调用频率主要为了防止用户误操作或恶意攻击导致服务器压力过大。以下几种方法可以实现:1.节流(Throttling):原理:在一定时间内,只允许一个请求发出,忽略后续的请求直到时间窗口结束。适用场景:按钮点击提交、搜索框输入联想等。实现方式:functionthrottle(fu......
  • 【SpringBoot】SpringBoot优雅停机机制
    文章目录背景1.什么是优雅停机?2.SpringBoot优雅停机的基础实现2.1立即停机模式2.2优雅停机模式3.核心机制解析3.1.启用ShutdownHook3.2.自定义资源释放逻辑3.3.超时机制4.优雅停机的实际应用场景4.1.服务更新4.2.流量调控4.3.订单处理5.优雅停机可能失......
  • 使用 PHP 调用 YOLO 模型进行物体检测
    环境准备首先,需要安装以下工具:Darknet:一个开源的深度学习框架,支持YOLO等物体检测模型。PHP:确保你已安装PHP环境。OpenCV:用于图像处理,确保PHP支持图像处理扩展。2.安装Darknet首先,克隆Darknet并编译它:bashgitclonehttps://github.com/pjreddie/darknet.gitc......
  • 物体检测(YOLO)示例:使用 Rust 调用 Python 进行物体检测
    在本示例中,我们将展示如何使用Rust调用Python脚本来完成YOLO物体检测任务。我们将通过Rust的std::process::Command调用Python脚本,并传递输入图像以进行检测。环境准备首先,您需要安装以下工具:更多内容访问ttocr.com或联系1436423940Rust语言:从Rust官网安装R......