Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🐧 Linux Kernel Comments
    • Chapter 2 - 微型计算机组成结构

      • Chapter 2 - 微型计算机组成结构
    • Chapter 3 - 内核编程语言和环境

      • Chapter 3 - 内核编程语言和环境
    • Chapter 4 - 80X86 保护模式及其编程

      • Chapter 4.1-4.2 - 80X86 系统寄存器和系统指令 & 保护模式内存管理
      • Chapter 4.3 - 分段机制
      • Chapter 4.4 - 分页机制
      • Chapter 4.5 - 保护
      • Chapter 4.6 - 中断和异常处理
      • Chapter 4.7 - 任务管理
      • Chapter 4.8 - 保护模式编程初始化
      • Chapter 4.9 - 一个简单的多任务内核实例
    • Chapter 5 - Linux 内核体系结构

      • Chapter 5.1-5.2 - Linux 内核模式 & 体系结构
      • Chapter 5.3 - Linux 内核对内存的管理和使用
      • Chapter 5.4-5.6 - 中断机制 & 系统调用 & 系统时间和定时
      • Chapter 5.7-5.9 - Linux 进程控制 & 堆栈使用 & 文件系统
    • Chapter 6 - 引导启动程序 (boot)

      • Chapter 6 - 引导启动程序 (boot)
    • Chapter 7 - 初始化程序 (init)

      • Chapter 7 - 初始化程序 (init)
    • Chapter 8 - 内核代码

      • Chapter 8.1 - 内核代码总体功能
      • Chapter 8.2 - asm.s 程序
      • Chapter 8.3 - traps.c 程序
      • Chapter 8.4 - sys_call.s 程序
      • Chapter 8.5 - mktime.c 程序
      • Chapter 8.6 - sched.c 程序
      • Chapter 8.7 - signal.c 程序
      • Chapter 8.8 - exit.c 程序
      • Chapter 8.9 - fork.c 程序
      • Chapter 8.10 - sys.c 程序
    • Chapter 9 - 块设备驱动程序

      • Chapter 9.1 - 块设备驱动程序 总体功能
      • Chapter 9.2 - blk.h 文件
      • Chapter 9.3 - hd.c 程序
      • Chapter 9.4 - ll_rw_blk.c 程序
      • Chapter 9.5 - ramdisk.c 程序
    • Chapter 10 - 字符设备驱动程序

      • Chapter 10.1 - 字符设备驱动程序 总体功能
      • Chapter 10.2 - keyboard.S 程序
      • Chapter 10.3 - console.c 程序
      • Chapter 10.4 - serial.c 程序
      • Chapter 10.5 - rs_io.s 程序
      • Chapter 10.6 - tty_io.c 程序
      • Chapter 10.7 - tty_ioctl.c 程序
    • Chapter 12 - 文件系统

      • Chapter 12.1 - 文件系统 总体功能
      • Chapter 12.2 - buffer.c 程序
      • Chapter 12.3 - bitmap.c 程序
      • Chapter 12.4 - truncate.c 程序
      • Chapter 12.5 - inode.c 程序
      • Chapter 12.6 - super.c 程序
      • Chapter 12.7 - namei.c 程序
      • Chapter 12.9 - block_dev.c 程序
      • Chapter 12.10 - file_dev.c 程序
      • Chapter 12.11 - pipe.c 程序
      • Chapter 12.12 - char_dev.c 程序
      • Chapter 12.13 - read_write.c 程序
      • Chapter 12.14 - open.c 程序
      • Chapter 12.15 - exec.c 程序
      • Chapter 12.16 - stat.c 程序
      • Chapter 12.17 - fcntl.c 程序
      • Chapter 12.18 - ioctl.c 程序
      • Chapter 12.19 - select.c 程序

Chapter 8.4 - sys_call.s 程序

Created by : Mr Dk.

2019 / 08 / 16 16:18

Ningbo, Zhejiang, China


8.4 sys_call.s 程序

实现了 系统调用中断 INT 0x80 的入口处理过程以及 信号检测处理。

此外还给出了两个系统功能的底层接口:

  • sys_execve
  • sys_fork

还有处理过程类似的中断处理程序:

  • INT 16:协处理器出错
  • INT 7:设备不存在
  • INT 32:时钟中断
  • INT 46:硬盘中断
  • INT 38:软盘中断

8.4.1 功能描述

在 Linux 中,用户使用系统调用中断 INT 0x80 和寄存器 eax 中的功能号,来使用内核提供的各种功能服务。所有的系统调用服务的 C 函数实现分布在整个内核代码中,内核把它们按照功能号顺序排成一张 函数指针表,在 INT 0x80 的处理过程中,根据 eax 中的功能号,调用指针表中对应的服务代码。

另外,该程序中还包含对其它几个中断的入口处理代码:

  • 软中断 (system_call, coprocessor_error, device_not_available) 将一些参数压入堆栈,为调用的 C 函数处理程序做准备 (作为参数)
    • 系统调用参数 - EBX、ECX、EDX
    • 调用 C 函数作相应处理
    • 处理返回后检测当前任务的信号位图,对优先级最高的信号进行处理并复位
  • 硬件中断
    • 向 8259A 发送 结束硬件中断控制字 EOI
    • 调用相应的 C 函数处理程序
    • 对于 时钟中断,也需要对当前任务的信号位图进行检测处理

8.4.1.1 中断调用入口处理过程

一个接口程序,每个调用的实际功能通过调用相应的 C 函数完成。首先,检查 eax 中的功能号是否在有效范围内,然后将一些用到的寄存器保存到堆栈上 - Linux 内核将 DS、ES 用于内核数据段,将 FS 用于用户程序数据段。通过 sys_call_table 调用相应系统调用的 C 函数进行进程处理,C 函数返回后,将返回值压入堆栈保存起来。

查看当前进程的状态。如果由于调用过程使进程从就绪态变为其它状态,或进程时间片已经用完,则调用调度函数 schedule() 调度进程。执行调度函数之前已经把 ret_from_sys_call 入栈;调度执行后,最终会回到 ret_from_sys_call 处继续执行。

我的理解:此时,当前进程的系统调用已经做完或正在等待被处理或时间片用完,因此进程不能马上被执行,需要调度别的进程执行;而最终,CPU 的使用权总会回到当前进程 (比如等待的事件已经完成)。应当从系统调用返回 (即 ret_from_sys_call) 处开始继续执行。

ret_from_sys_call 主要做系统调用的后处理工作:

  • 判断当前进程是否为 0,若是,则直接退出此次系统调用
  • 根据代码段描述符和使用的堆栈判断该进程是否为用户进程;若是内核进程,则立刻退出系统调用
  • 处理调用系统调用的进程的信号;若信号位图表明该进程接收到信号,则调用信号处理函数 do_signal()

最终,恢复保存的寄存器内容,退出中断处理过程,返回调用程序。若有信号,则首先返回到相应的信号处理函数中,再返回调用系统调用的进程。

8.4.1.2 系统调用参数传递方法

Linux 0.12 系统中,使用寄存器 EBX、ECX、EDX 传递参数,可以传递 指针。在系统调用运行过程中,段寄存器 DS、ES 指向内核数据空间,FS 指向用户数据空间。Linux 内核可以通过 FS 寄存器执行内核数据空间和用户数据空间的数据复制工作

  • get_fs_byte()
  • put_fs_byte()

优势:进入中断处理程序,进行堆栈切换时,这些传递参数的寄存器也被自动放在了内核态堆栈上。因此内核不用进行特殊处理。

8.4.2 代码注释

系统调用号不在合法范围的处理方式:

ENOSYS = 38 # 系统调用号出错码

.align 2
bad_sys_call:
    pushl $-ENOSYS # 相当于在 eax 中放置出错码
    jmp ret_from_sys_call

系统调用中断处理过程:

# INT 0x80 - 系统调用入口点
.align 2
_system_call:
    push %ds # 保存原寄存器值
    push %es
    push %fs
    pushl %eax # 保存功能号
    
    push %edx # 系统调用参数
    push %ecx
    push %ebx
    
    movl $0x10, %edx # 内核数据空间
    mov %dx, %ds
    mov %dx, %es
    movl $0x17, %edx # 用户数据空间
    mov %dx, %fs
    
    cmpl _NR_syscalls, %eax
    jae bad_sys_call # 调用号超出范围
    
    # 调用地址为 [_sys_call_table + %eax * 4]
    call _sys_call_table(, %eax, 4) # 调用对应功能的 C 函数
    pushl %eax # 系统调用返回值入栈

接下来查看当前程序的运行状态:

  • 不在就绪状态 → 执行调度程序
  • 处在就绪状态,但时间片已用完 → 执行调度程序

重新执行调度程序的入口:

  • 调度程序返回时从 ret_from_sys_call 处继续执行。
.align 2
reschedule:
    pushl $ret_from_sys_call # 返回地址入栈
    jmp _schedule

这里显然需要访问任务的 task_struct 结构体。为了方便访问结构体中的各个字段,程序中预先定义了各字段在结构体中的偏移:

state = 0
counter = 4
priority = 8
signal = 12
sigaction = 16
blocked = (33*16)

接下来是中断处理过程中的代码:

2:
    movl _current, %eax # current 指针,指向当前任务
    cmpl $0, state(%eax) # 进程状态
    jne reschedule
    cmpl $0, counter(%eax) # 进程时间片
    jne reschedule

接下来,可能会调度到其它进程运行。当系统调用的 C 函数返回,并重新获得 CPU 使用权时,或本程序中的其它中断服务程序退出时,都会执行接下来的代码,对信号进行识别处理。此时,堆栈的布局是这样的:

    30(%esp) - %oldss
    2C(%esp) - %oldesp
    28(%esp) - %eflags
    24(%esp) - %cs
    20(%esp) - %eip
    1C(%esp) - %ds
    18(%esp) - %es
    14(%esp) - %fs
    10(%esp) - original %eax
     C(%esp) - %edx
     8(%esp) - %ecx
     4(%esp) - %ebx
     0(%esp) - %eax

因此,为了便于通过 esp 指针访问堆栈中的数据,代码中定义了堆栈中各字段的偏移量:

EAX = 0x00
EBX = 0x04
ECX = 0x08
EDX = 0x0C
ORIG_EAX = 0x10
FS = 0x14
ES = 0x18
DS = 0x1C
EIP = 0x20
CS = 0x24
EFLAGS = 0x28
OLDESP = 0x2C
OLDSS = 0x30

下面的代码中:

  • 标号 ret_from_sys_call 进行信号处理
  • 标号 3 用于退出中断
ret_from_sys_call:
    movl _current, %eax
    cmpl _task, %eax # task[0] 不处理信号
    je 3f # 直接退出中断
    
    cmpw $0x0f, CS(%esp) # 调用系统调用程序的代码段 RPL 是否为 3
    jne 3f # 不是用户代码,直接退出中断
    cmpw $0x17, OLDSS(%esp) # 调用系统调用程序的堆栈段是否是位于用户堆栈
    jne 3f # 不在用户堆栈,直接退出中断
    # 任务在内核态执行时不可抢占
    # 因此不对任务进行信号处理,直接退出中断
    
    # 处理信号
    movl signal(%eax), %ebx # ebx 中放置当前任务的信号 bitmap
    movl blocked(%eax), %ecx # ecx 中放置阻塞信号 bitmap
    notl %ecx
    andl %ebx, %ecx
    bsfl %ecx, %ecx # 从低位开始扫描信号 bitmap 是否有 1
    je 3f # 没有信号,则直接退出中断
    btrl %ecx, %ebx # 复位信号
    movl %ebx, signal(%eax) # 重新保存信号 bitmap
    
    incl %ecx # 将信号调整为 1 开始的数 (1-32)
    pushl %ecx # 信号值入栈,作为 do_signal 的参数
    call _do_signal # 调用信号处理的 C 函数
    popl %ecx # 弹出入栈的信号值
    test %eax, %eax # 返回值不为 0
    jne 2b # 切换进程 / 处理更多信号
    
3:
    popl %eax # 含有之前入栈的系统调用返回值
    popl %ebx
    popl %ecx
    popl %edx
    addl $4, %esp
    pop %fs
    pop %es
    pop %ds
    iret

接下来是一些软硬件中断的处理过程:

# INT 16 - 处理器错误中断
# 协处理器检测到自己发生错误,通过 ERROR 引脚通知 CPU
# CPU 跳转执行 C 函数 math_error()
# 返回后跳转到标号 ret_from_sys_call 继续执行
.align 2
_coprocessor_error:
    push %ds
    push %es
    push %fs
    pushl $-1 # 不是系统调用,否则应保存功能号
    pushl %edx
    pushl %ecx
    pushl %ebx
    pushl %eax
    movl $0x10, %eax # 内核数据段
    mov %ax, %ds
    mov %ax, %es
    movl $0x17, %eax # 局部数据段
    mov %ax, %fs
    pushl $ret_from_sys_call # 调用返回地址入栈
    jmp _math_error # 执行 math_error()
# INT 7 - 设备或协处理器不存在
# INT 32 - INT 0x20 - 时钟中断处理程序
.align 2
_timer_interrupt:
    push %ds
    push %es
    push %fs
    pushl $-1
    pushl %edx
    pushl %ecx
    pushl %ebx
    pushl %eax
    movl $0x10, %eax
    mov %ax, %ds
    mov %ax, %es
    movl $0x17, %eax
    mov %ax, %fs
    incl _jiffies # 增加一次系统滴答
    
    # 8259A 没有采用自动结束中断,所以需要发送指令结束中断
    movb $0x20, %al
    outb %al, $0x20
    
    # 从堆栈中取出执行系统调用代码的 CS 寄存器
    # 取其中的 CPL 特权级,并压入堆栈,作为 do_timer 的参数
    # do_timer() 执行任务切换、即使
    movl CS(%esp), %eax
    andl $3, %eax
    pushl %eax
    call _do_timer
    addl $4, %esp # 将压入堆栈的参数忽略
    jmp ret_from_sys_call
# INT 46 - INT 0x2E
# 硬盘中断处理程序
# INT 38 - INT 0x26
# 软盘中断处理程序
# INT 39 - INT 0x27
# 并行接口中断处理程序

两个系统调用:

# sys_execve() 系统调用
.align 2
_sys_execve:
    lea EIP(%esp), %eax # eax 指向保存用户程序 EIP 处
    pushl %eax
    call _do_execve
    addl $4, %esp # 丢弃栈中参数
    ret
# sys_fork() 系统调用
.align 2
_sys_fork:
    call _find_empty_process # 为新进程取得进程号
    testl %eax, %eax
    js 1f # 进程号为负数,退出
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process
    addl $20, %esp # 丢弃所有栈中参数
1:
    ret
Edit this page on GitHub
Prev
Chapter 8.3 - traps.c 程序
Next
Chapter 8.5 - mktime.c 程序