分析 system_call 中断处理过程

Zzzzxp / 2025-02-21 / 原文

张晓攀+原创作品转载请注明出处+《Linux内核分析》MOOC课程https://mooc.study.163.com/course/1000029000

实验五——分析 system_call 中断处理过程

在上一次实验中我选择的4号系统调用write

一、打开shell并使用命令启动内核进入menu程序

cd ~/LinuxKernel/

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

进入menu程序后,可以看到该系统中仅有3个命令:helpversionquit

二、在该系统中添加两条命令:writewrite-asm

writewrite-asm这两个函数是系统调用sys_write的C语言版本和汇编版本)

打开test.c文件可以看到main函数如下:

  • MenuConfig("version", "XXX V1.0(Menu program v1.0 inside)", NULL); - 配置菜单项。这个函数的第一个参数是命令名称"version",第二个参数是命令的描述或帮助信息,"XXX V1.0(Menu program v1.0 inside)",第三个参数是执行这个命令时的函数指针,这里是NULL,表示这个命令不会执行任何操作,只显示信息。

  • MenuConfig("quit", "Quit from XXX", Quit); - 另一项菜单配置,设置了一个退出命令"quit",描述信息是"Quit from XXX",对应的函数指针为Quit`,用于退出菜单系统。

  • ExecuteMenu();` - 执行菜单,进入到菜单系统中并等待用户的输入指令。

main函数中添加write和和write-asm

并给出WriteWrite_asm函数:

  • write(0, argv[i], strlen(argv[i]));:调用 write 函数,将 argv[i] 的内容写到文件描述符 0(标准输入/输出可能被重定向),字符串长度由 strlen(argv[i]) 决定。

  • write(0, "\n", 1);:写入一个换行符 \n

  • mov $4, %%eax:将系统调用号 4sys_write)放入 eax`,表示要执行写操作。

  • mov $1, %%ebx:将文件描述符 1(标准输出)放入 ebx

  • mov %2, %%ecx:将 str 的地址放入 ecx,指定要写入的字符串。

  • mov %3, %%edx:将字符串长度 len 放入 edx

  • int $0x80:触发中断 0x80,执行系统调用。

再次输入help可以看到以上两条命令已经添加:

三、分析系统调用的过程,从 system_call 开始到 iret 结束之间的整个过程

在 Linux 系统中,系统调用的过程通常是通过软件中断来完成的。以 x86 架构的 Linux 为例,系统调用的核心过程是通过 int 0x80 中断来触发内核中的 system_call 函数。以下是从 system_call 开始到 iret 返回的整个过程详细分析:

1. 触发系统调用:int 0x80

用户态程序执行 int 0x80 指令,触发系统调用中断 0x80,这会导致处理器进入内核态并跳转到系统调用入口,即 system_call 函数。以下是一些重要的准备工作:

  • 系统调用号:用户程序在 eax 寄存器中设置系统调用号,用于指定执行哪个系统调用(例如,sys_write 的系统调用号为 4)。
  • 参数:用户程序在 ebxecxedxesiedi 等寄存器中设置参数,这些寄存器将被 system_call 读取。

2. system_call 函数入口

系统调用的入口通常会执行以下操作:

  • 保存上下文system_call 首先将用户态的寄存器上下文(例如 eaxebxecxedx 等寄存器)保存到内核栈,以便系统调用完成后恢复用户态的状态。
  • 检查系统调用号system_call 会验证 eax 中的系统调用号是否合法。如果无效,则可能返回错误(例如 -ENOSYS 表示系统调用不存在)。

3. 调用相应的系统调用处理函数

根据系统调用号 eaxsystem_call 会从系统调用表中找到对应的系统调用处理函数并跳转执行。例如,如果系统调用号是 4(即 sys_write),则会执行 sys_write 函数。

  • 系统调用表(sys_call_table):这是一个函数指针数组,系统调用号用于索引该数组,从而找到相应的系统调用处理函数。例如 sys_writesys_read 等系统调用都在 sys_call_table 中有对应的函数指针。
  • 执行系统调用逻辑:系统调用处理函数(如 sys_write)执行相应的内核逻辑,通常涉及与硬件交互、文件操作、内存管理等。

4. 返回系统调用结果

系统调用处理函数执行完毕后,将返回一个结果值。通常情况下,返回值存放在 eax 寄存器中。

  • 错误处理:如果系统调用失败,内核会在 eax 中设置一个负数的错误码,例如 -EINVAL 表示无效参数。

5. 恢复用户态上下文

system_call 函数返回用户态之前,内核需要恢复用户态的寄存器上下文,以确保系统调用返回时用户态的执行环境不受影响。

  • 恢复寄存器:从内核栈中取出保存的寄存器值,恢复到各个寄存器。
  • 清理内核栈:清理在进入内核态时保存的上下文信息。

6. 返回用户态:iret

系统调用完成后,处理器会执行 iret 指令返回到用户态。iret 指令会从栈中弹出用户态的 CS:EIPEFLAGSSS:ESP 等寄存器,恢复用户态的指令指针、标志位和栈指针。

  • 切换回用户态iret 会将处理器的状态切换回用户态,并开始执行用户程序中系统调用之后的指令。
  • 返回结果:在用户程序中,系统调用的返回值将保存在 eax 寄存器中,这样用户程序可以通过检查 eax 的值来知道系统调用是否成功,以及获取到的结果。

四、总结

整个系统调用过程包括以下关键步骤:

  1. 用户态程序通过 int 0x80 触发系统调用中断。
  2. system_call 入口函数保存上下文,并根据系统调用号跳转到对应的系统调用处理函数。
  3. 系统调用处理函数执行内核逻辑并返回结果。
  4. system_call 恢复用户态上下文。
  5. iret 返回用户态,并让用户程序继续执行系统调用后的指令。

这个过程确保了系统调用的安全性和稳定性,使用户程序可以请求内核提供的服务而不会直接操作系统资源。