引入系统调用

简介: 引入系统调用

引言

  • 回顾前面章节,我们成功的实现了多任务并行执行,然而这些任务都是一直循环执行,那么系统中的所有任务都是一直不停的执行的吗?

思考

  • 任务显然不是必须一直执行的,那么任务执行结束后,又返回到哪里呢?
  • 回顾前面所讲的恢复上下文示意图中 ③ eip 寄存器保存任务入口地址,然后通过 iret 指令跳转到任务入口执行

  • 很显然,在跳转前,我们并没有将跳转前程序执行地址入栈保存,所以 iret 跳转后是程序没办法返回的
  • 可见,当前我们已实现的方案是通过 iret 直接跳转到入口函数执行,并非函数调用,因此无法返回

如何实现返回

  • 为什么需要任务返回?
  • 任务结束后,我们需要释放任务占用资源,而如何知道任务执行结束呢?答案就是通过任务返回
  • 想要破局实现返回,我们可以构造出来一个函数调用,借助函数调用来实现返回
  • 听上去很抽象,别着急,我们一点一点具体实现
  • 先在 TASK 结构体中新增 TASK_FUNC task_entry 成员
typedef struct TASK
{
    U32             id;                 // 任务 id
    U08*            name;               // 任务名称
    U08*            stack_addr;         // 任务栈基址
    U16             stack_size;         // 任务栈大小
    REG             reg;                // 任务上下文,即任务执行状态下所有寄存器的值
    TASK_FUNC       task_entry;         // 任务函数入口
} TASK;
  • 在 “task.c” 中新增一个函数 TaskEntry
static void TaskEntry(void)
{
    if(current_task)
    {
        current_task->task_entry();
    }
    printk("%s end!!!\n", current_task->name); // 调试
    // 任务销毁工作
    while(1);
}
  • TaskEntry 函数具体是怎么使用的呢?见 TaskCreat 函数
E_RET TaskCreat(TASK* task, TASK_FUNC pFunc, U08* stackAddr, U16 stackSize, U08* name)
{
    ...
    // task->reg.eip = (U32)pFunc;
    task->reg.eip = (U32)TaskEntry;
    task->task_entry = pFunc;
    ...
}
  • 现在的程序逻辑就变成了所有任务的入口都是 TaskEntry 函数,在 TaskEntry 函数中再借助
current_task->task_entry(); 跳转到各自任务函数执行
  • 测试一下,让 TASK D 任务函数 TaskDFunc 循环几次后就跳出循环,测试代码见:task.ctask.hmain.c
void TaskDFunc(void)    // 任务执行函数
{
    static U32 count = 0;
    while(1)
    {
        if(count++ % 100000 == 0)
        {
            static U32 j = 0;
            asm volatile("cli");
            SetCursorPos(0, 12);
            printk("TASK D: %x\n", j++); 
            asm volatile("sti");
        }  
        if(count > 1000000) // 跳出循环
            break;
    }
}
  • 测试结果,TASK D 执行结束,其它任务仍在运行

引入系统调用

  • 回到 TaskEntry 函数,前面的实验中我们并没有具体实现任务销毁工作,仅仅只是写了个注释,然后 while(1) 循环
  • 在实现销毁任务前还需要深入思考一下,在什么情况下才能销毁任务呢?答案:0 特权级,也就是内核态才能执行销毁任务操作
  • 思考:TaskEntry 函数执行的特权级是多少? 0 还是 3
  • 显然,TaskEntry 是任务入口,那么其必定工作在 3 特权级,那么,其不具备销毁任务的操作权限
  • 问题又来了,怎么跳转到 0 特权级呢?
  • 答案,哈哈,终于说到点子上了,那就是通过中断跳转到 0 特权级,我们可以使用 0x80 号软中断实现跳转,于是,引入了系统调用

实现第一个系统调用

  • 系统调用是任务与内核之间的交互接口
  • 涉及特权级的转换(DPL3 --> DPL0)
  • 接下来我们将通过 0x80 号中断(软中断)实现系统调用
  • 代码见:syscall.csyscall.htask.cinterrupt.asm
  • 创建 “syscall.c” 和 “syscall.h” 文件,其用于系统调用相关代码,这个不用再多说了
  • 在 TaskEntry 函数中添加如下代码触发 0x80 号软中断

asmvolatile("int $0x80\n");

  • 不过,需要注意的是,在 “interrupt.asm” 的中断入口 Int0x80_Entry 也需要保存并恢复上下文
extern Int0x80Handle
Int0x80_Entry:
    ; 保存上下文, ss esp eflags cs eip 这 5 个寄存器已被 CPU 自动入栈保存
    pushad                      ; 保存通用寄存器
    push ds
    push es
    push fs 
    push gs
    mov esp, KERNEL_STACK       ; 重新指定栈顶 esp 到内核栈,以供接下来的逻辑功能代码部分使用 
    call Int0x80Handle          ; 中断逻辑功能
    mov esp, [current_reg]      ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置
    ; 恢复上下文 
    pop gs
    pop fs
    pop es
    pop ds   
    popad                       ; 恢复通用寄存器
    iret
  • 保存上下文和恢复上下文的代码在 Int0x20_Entry 中断入口已经写过一遍了,本着代码不重复的原则,使用汇编宏优化一下代码
%macro BeginISR 0
    ; 保存上下文, ss esp eflags cs eip 这 5 个寄存器已被 CPU 自动入栈保存
    pushad                      ; 保存通用寄存器
    push ds
    push es
    push fs 
    push gs
    mov esp, KERNEL_STACK       ; 重新指定栈顶 esp 到内核栈,以供接下来的逻辑功能代码部分使用 
%endmacro
%macro EndISR 0
    mov esp, [current_reg]      ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置
    ; 恢复上下文 
    pop gs
    pop fs
    pop es
    pop ds   
    popad                       ; 恢复通用寄存器
    iret
%endmacro
Int0x20_Entry:
    BeginISR
    call Int0x20Handle          ; 中断逻辑功能
    EndISR
...
Int0x80_Entry:
    BeginISR
    call Int0x80Handle          ; 中断逻辑功能
    EndISR
  • 中断都已经触发,然而对应的中断服务程序还没实现,我们把其放到 “syscall.c” 文件中
void Int0x80Handle(U16 ax)
{
    SetCursorPos(0, 14);
    printk("Int0x80Handle\n");
}
  • 万事具备,编译运行看看结果
  • 很不幸,程序崩溃了, bochs 提示如下:

interrupt(): soft_int && (gate.dpl < CPL)

  • 一看就是特权级的问题,查看一下 “loader.asm”, 发现其中中断描述符表 IDT 的属性特权级为 DA_DPL0,应改为 DA_DPL3
  • 再次编译运行,这回终于成功实现打印

  • 别着急,这还仅仅是调通了 0x80 号软中断,想要区分不同的系统调用就必须给 Int0x80Handle 函数传参,TaskEntry 函数中触发 0x80 号中断的代码就要改成如下方式,利用 ax 寄存器传参
asm volatile(
        "movw $0, %%ax\n"
        "int $0x80\n"
        : : : "ax" // 告诉 gcc 编译器,ax 寄存器被内嵌汇编使用,需要 gcc 自动添加保护和恢复操作(入栈和出栈)
    );
  • 根据 C与汇编混合编程 中的 C 调用约定,汇编作为主调者,要从右到左入栈,函数调用后还要负责恢复栈,于是 Int0x80_Entry 中断入口处代码如下
Int0x80_Entry:
    BeginISR
    push ax
    call Int0x80Handle          ; 中断逻辑功能
    add esp, 4
    EndISR
  • 中断服务程序逻辑代码 Int0x80Handle 中再以 ax 为参数执行不同的函数调用
void Int0x80Handle(U16 ax)
{
    if(0 == ax)
    {
        SetCursorPos(0, 14);
        printk("Kill Task\n");
    }
}
  • 嗯~,到最后我们都没有真正实现销毁任务代码,哈哈,只以打印字符串 "Kill Task" 替代,这是因为目前我们的主要目的是实现系统调用。关于任务销毁的具体实现,待后面章节实现
  • 最后看一下代码执行效果

目录
相关文章
|
5月前
|
缓存 Linux 编译器
C/C++ 函数调用以及Linux中系统调用 开销介绍:介绍C/C函数调用以及Linux中系统调用的开销情况
C/C++ 函数调用以及Linux中系统调用 开销介绍:介绍C/C函数调用以及Linux中系统调用的开销情况
64 0
|
10月前
|
存储 API Windows
8.1 Windows驱动开发:内核文件读写系列函数
在应用层下的文件操作只需要调用微软应用层下的`API`函数及`C库`标准函数即可,而如果在内核中读写文件则应用层的API显然是无法被使用的,内核层需要使用内核专有API,某些应用层下的API只需要增加Zw开头即可在内核中使用,例如本章要讲解的文件与目录操作相关函数,多数ARK反内核工具都具有对文件的管理功能,实现对文件或目录的基本操作功能也是非常有必要的。
8.1 Windows驱动开发:内核文件读写系列函数
Linux驱动程序开发用户态和内核态 模块机制
Linux驱动程序开发用户态和内核态 模块机制
|
存储 API Windows
驱动开发:内核中进程与句柄互转
在内核开发中,经常需要进行进程和句柄之间的互相转换。进程通常由一个唯一的进程标识符(PID)来标识,而句柄是指对内核对象的引用。在Windows内核中,`EProcess`结构表示一个进程,而HANDLE是一个句柄。为了实现进程与句柄之间的转换,我们需要使用一些内核函数。对于进程PID和句柄的互相转换,可以使用函数如`OpenProcess`和`GetProcessId`。OpenProcess函数接受一个PID作为参数,并返回一个句柄。GetProcessId函数接受一个句柄作为参数,并返回该进程的PID。
|
存储 API
驱动开发:内核文件读写系列函数
在应用层下的文件操作只需要调用微软应用层下的`API`函数及`C库`标准函数即可,而如果在内核中读写文件则应用层的API显然是无法被使用的,内核层需要使用内核专有API,某些应用层下的API只需要增加Zw开头即可在内核中使用,例如本章要讲解的文件与目录操作相关函数,多数ARK反内核工具都具有对文件的管理功能,实现对文件或目录的基本操作功能也是非常有必要的。
|
缓存 Linux API
系统编程之文件IO(七)——0,1,2三个文件描述符与库函数和系统调用的区别
系统编程之文件IO(七)——0,1,2三个文件描述符与库函数和系统调用的区别
106 0
系统编程之文件IO(七)——0,1,2三个文件描述符与库函数和系统调用的区别
|
Unix 物联网 Linux
系统调用的概念|学习笔记
快速学习系统调用的概念
系统调用的概念|学习笔记
驱动开发:内核中枚举进线程与模块
内核枚举进程使用`PspCidTable` 这个未公开的函数,它能最大的好处是能得到进程的EPROCESS地址,由于是未公开的函数,所以我们需要变相的调用这个函数,通过`PsLookupProcessByProcessId`函数查到进程的EPROCESS,如果`PsLookupProcessByProcessId`返回失败,则证明此进程不存在,如果返回成功则把EPROCESS、PID、PPID、进程名等通过DbgPrint打印到屏幕上。
驱动开发:内核中枚举进线程与模块
驱动开发:内核通过PEB得到进程参数
PEB结构`(Process Envirorment Block Structure)`其中文名是进程环境块信息,进程环境块内部包含了进程运行的详细参数信息,每一个进程在运行后都会存在一个特有的PEB结构,通过附加进程并遍历这段结构即可得到非常多的有用信息。
驱动开发:内核通过PEB得到进程参数
|
缓存 Linux C语言
库函数与系统调用之间的区别--扩展知识点1
库函数与系统调用之间的区别--扩展知识点1
190 0