手写简易操作系统(十九)--实现0x80中断

前情提要

上一节我们实现了用户程序,现在的用户程序还是一个函数来模拟的,后面我们会把编译好的用户程序放在硬盘里,通过硬盘加载。

众所周知,用户程序使用系统服务是通过 0x80 中断进行的,也只能通过中断进入高优先级,中断结束再返回用户态,这样就实现了用户程序除非调用系统服务,否则就只能在用户态。系统服务是操作系统准备好的,并不会对操作系统造成危害

一、系统调用实现框架

一个系统功能调用分为两部分,一部分是暴露给用户进程的接口函数,它属于用户空间,此部分只是用户进程使用系统调用的途径,只负责发需求。另一部分是与之对应的内核具体实现,它属于内核空间,此部分完成的是功能需求,就是我们一直所说的系统调用子功能处理函数。

实现思路

1、用中断门实现系统调用,效仿Linux用0x80号中断作为系统调用的入口。
2、在IDT中安装0x80号中断对应的描述符,在该描述符中注册系统调用对应的中断处理例程。
3、建立系统调用子功能表syscall_table,利用eax寄存器中的子功能号在该表中索引相应的处理函数。
4、用宏实现用户空间系统调用接口_syscall,最大支持3个参数的系统调用,故只需要完成_syscall[0-3]。寄存器传递参数,eax为子功能号,ebx保存第1个参数,ecx保存第2个参数,edx保存第3个参数。

1.1、添加0x80中断

// interrupt.c
// 系统调用接口
extern uint32_t syscall_handler(void);/*初始化中断描述符表*/
static void idt_desc_init(void) {put_str("----idt_desc_init begin!\n");for (int i = 0; i < IDT_DESC_CNT; i++) {make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); }/* 单独处理 0x80 system call 系统调用 */make_idt_desc(&idt[0x80], IDT_DESC_ATTR_DPL3, syscall_handler); put_str("----dt_desc_init done!\n");
}

在初始化中断描述符时,初始化 0x80 中断的描述符,其中特权级为3,调用函数为 syscall_handler,这个函数是汇编实现的,我们看一下

;; 0x80 号中断
[bits 32]
extern syscall_table
section .text
global syscall_handler ;0x80的中断处理程序
syscall_handler:; 保存上下文环境push 0             ; 压入0,这里占的位置是err_code,错误码push dspush espush fspush gspushad             ; PUSHAD指令压入32位寄存器,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI push 0x80          ; 压入0x80,这里占的位置是vec_no,中断号; 为系统调用子功能传递参数push edx           ; 系统调用中第3个参数push ecx           ; 系统调用中第2个参数push ebx           ; 系统调用中第1个参数; 调用子功能处理函数call [syscall_table + eax*4]add esp, 12                  ; 跨过上面的三个参数; 将call调用后的返回值存放到eaxmov [esp + 8*4], eaxjmp intr_exit                ; 中断返回

单纯的这样的看是有点看不懂的,我们结合一下调用的汇编

// lib/user/syscall.c
/* 无参数的系统调用 */
#define _syscall0(NUMBER) ({			   \int retval;					           \asm volatile (					       \"int $0x80"						       \: "=a" (retval)					       \: "a" (NUMBER)					       \: "memory"						       \);							       \retval;						       \
})/* 一个参数的系统调用 */
#define _syscall1(NUMBER, ARG1) ({		   \int retval;					           \asm volatile (					       \"int $0x80"						       \: "=a" (retval)					       \: "a" (NUMBER), "b" (ARG1)			   \: "memory"						       \);							       \retval;						       \
})/* 两个参数的系统调用 */
#define _syscall2(NUMBER, ARG1, ARG2) ({   \int retval;						       \asm volatile (					       \"int $0x80"						       \: "=a" (retval)					       \: "a" (NUMBER), "b" (ARG1), "c" (ARG2) \: "memory"						       \);							       \retval;						       \
})/* 三个参数的系统调用 */
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({  \int retval;						            \asm volatile (					            \"int $0x80"					            \: "=a" (retval)					        \: "a" (NUMBER), "b" (ARG1), "c" (ARG2), "d" (ARG3)       \: "memory"					            \);							                \retval;						                \
})

从无参数的系统调用,到三个参数的系统调用。从这里我们就可以串联起来系统的调用过程了。

1.2、实现getpid调用

我们先实现一个系统调用,gitpid(),用来获取当前正在运行的系统调用的pid。

/* 0号调用:返回当前任务的pid */
uint32_t sys_getpid(void) {return running_thread()->pid;
}enum SYSCALL_NR {SYS_GETPID,               // 0号调用:获取当前线程的pid
};/* 初始化系统调用,也就是将syscall_table数组中绑定好确定的函数 */
void syscall_init(void) {put_str("syscall_init begin!\n");syscall_table[SYS_GETPID] = sys_getpid;put_str("syscall_init done!\n");
}

可以看到,这里是将内核中的 sys_getpid 函数地址给了函数数组 syscall_table,由于是0号调用,所以这里也是在数组的0号位置。

然后给用户端留下相应的调用接口‘

/* 返回当前任务pid */
uint32_t getpid() {return _syscall0(SYS_GETPID);
}

用户程序调用这个接口就可以返回用户的进程pid。

我们来分析一下实现过程

1.3、对getpid的分析

首先用户程序执行 _syscall0(SYS_GETPID) 这个宏定义,SYS_GETPID就是子功能号,这里为0。

把这个宏展开

int retval;
asm volatile ("int $0x80"               // 触发系统调用: "=a" (retval)           // 将系统调用的返回值存储在 retval 中: "a" (0)                 // 使用系统调用号 0: "memory"                // 内联汇编可能会影响内存,因此需要指定 memory 操作数
);
return retval;

就是触发系统调用,并将返回值传递给 EAX,这里通过内联汇编将 EAX 存到内存 retval 中,进入中断时传入中断子功能号 0

这样我们就进入中断了,进入中断以后执行 syscall_handler 函数,首先是保存上下文环境

push 0             ; 压入0,这里占的位置是err_code,错误码
push ds
push es
push fs
push gs
pushad             ; PUSHAD指令压入32位寄存器,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI 
push 0x80          ; 压入0x80,这里占的位置是vec_no,中断号

如果有其他参数的话会保存在 EBX,ECX,EDX 中。

push edx           ; 系统调用中第3个参数
push ecx           ; 系统调用中第2个参数
push ebx           ; 系统调用中第1个参数

子功能处理函数保存在 syscall_table 数组中,保存的位置就是子功能号,拿到处理函数的地址,直接跳过去执行。

call [syscall_table + eax*4]

跨过保存在 EBX,ECX,EDX 中的三个参数

add esp,12

现在就剩下一个问题了,我们执行函数拿到的返回值保存在了 EAX,怎么将 EAX 返回呢。这里使用了一种巧妙的方式,中断就得中断返回,中断返回就需要将之前入栈保存的寄存器值再回复,这里我们直接通过这个机会,把栈中EAX的值给他换了,中断返回后EAX中不就是返回值嘛

mov [esp + 8*4], eax
jmp intr_exit

1.4、仿真

可以看到,用户程序成功的获得了自己的pid。这里有个问题就是用户程序还不能自己在控制台输出,因为控制台输出是对硬件的控制,需要0特权级,很简单我们也加一个系统调用。

image-20240329171307966

二、实现用户打印程序

2.1、实现write函数

write是将字符写入到文件中,标准输出也是文件,但是这里我们还没有实现文件系统,所以先暂且直接输出到控制台。

/* 1号调用:把buf中count个字符写到文件描述符fd指向的文件中 */
uint32_t sys_write(char* str) {console_put_str(str);return strlen(str);
}/* 初始化系统调用,也就是将syscall_table数组中绑定好确定的函数 */
void syscall_init(void) {put_str("syscall_init begin!\n");syscall_table[SYS_GETPID] = sys_getpid;syscall_table[SYS_WRITE] = sys_write;put_str("syscall_init done!\n");
}

暴露给用户的接口是

/* 打印字符串str */
uint32_t write(char* str) {return _syscall1(SYS_WRITE, str);
}

这里使用的宏就是带一个参数的宏了,这个宏和上面的分析是一样的。

现在就实现了用户的打印程序,先看一下行不行。

image-20240329172013163

没有任何问题。

2.2、实现printf

在实际使用C语言的过程中,printf函数是可变参数的,这一点以我之前的C语言水平确实是没有理解,按理说,C语言又不能像C++一样实现参数重载,他怎么做到可变参数的,这一点将在这一节说中讲解。

实际上,不管是多少参数,一个函数调用的过程就是先将函数的参数从右到左压栈,我们看一下printf的定义

uint32_t printf(const char* format, ...);

第一个参数一定是一个字符串,这个字符串可以有 % 也可以没有,但是既然我们的 % 个数是和后面的参数个数一致的。那我们就可以自己从栈中找到压入的参数啊。

首先看一个宏

#define va_start(ap, v) ap = (va_list)&v  // 把ap指向第一个固定参数v
#define va_arg(ap, t) *((t*)(ap += 4))	  // ap指向下一个参数并返回其值
#define va_end(ap) ap = NULL		      // 清除ap

单看这个宏定义很难看懂,还是结合实际代码

/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {va_list args;                                                // char* argsva_start(args, format);	       // 使args指向format            // args = (char*) &format;char buf[1024] = { 0 };	       // 用于存储拼接后的字符串vsprintf(buf, format, args);   // 按照format格式输出到bufva_end(args);                  // 销毁指针                    // args = NULLreturn write(buf);             // 写入控制台
}

其中第一行就是申明一个指针,第二行将这个指针指向的传入的字符串的指针。也就是说,这是一个二级指针了。

然后我们看 vsprintf

/* 将参数ap按照格式format输出到字符串str,并返回替换后str长度 */
uint32_t vsprintf(char* str, const char* format, va_list ap) {char* buf_ptr = str;const char* index_ptr = format;char index_char = *index_ptr;int32_t arg_int;char* arg_str;while (index_char) {if (index_char != '%') {*(buf_ptr++) = index_char;index_char = *(++index_ptr);continue;}index_char = *(++index_ptr); // 得到%后面的字符switch (index_char) {case 's':arg_str = va_arg(ap, char*);strcpy(buf_ptr, arg_str);buf_ptr += strlen(arg_str);index_char = *(++index_ptr);break;case 'c':*(buf_ptr++) = va_arg(ap, char);index_char = *(++index_ptr);break;case 'd':arg_int = va_arg(ap, int);if (arg_int < 0) {          // 若是负数, 将其转为正数后,再正数前面输出个负号'-'arg_int = 0 - arg_int;*buf_ptr++ = '-';}itoa(arg_int, &buf_ptr, 10);index_char = *(++index_ptr);break;case 'x':arg_int = va_arg(ap, int);itoa(arg_int, &buf_ptr, 16);index_char = *(++index_ptr); // 跳过格式字符并更新index_charbreak;case 'o':arg_int = va_arg(ap, int);itoa(arg_int, &buf_ptr, 8);index_char = *(++index_ptr); // 跳过格式字符并更新index_charbreak;}}return strlen(str);
}

这个函数对不同的控制字进行了替换,替换成了我们可变参数提供的值。

可以看一下不同进制转换的函数

const char cache[16] = "0123456789ABCDEF";
/* 将整型转换成字符(integer to ascii) */
static void itoa(uint32_t value, char** buf_ptr_addr, uint8_t base) {uint32_t m = value % base; // 求模,最先掉下来的是最低位uint32_t i = value / base; // 取整if (i) {                   // 倍数不为0则递归调用itoa(i, buf_ptr_addr, base);}*((*buf_ptr_addr)++) = cache[m];}

这样就实现了一个可变参数的打印函数,针对用户,既然可以针对用户,那么内核一定是可以的。

/* 格式化输出字符串,内核使用 */
uint32_t printk(const char* format, ...) {va_list args;va_start(args, format);	       // 使args指向formatchar buf[1024] = { 0 };	       // 用于存储拼接后的字符串vsprintf(buf, format, args);   // 按照format格式输出到bufva_end(args);                  // 销毁指针return sys_write(buf);         // 写入控制台
}

2.3、仿真

image-20240329173358582

结束语

本节我们实现了 0x80 中断,并且让用户也可以打印自己的字符串。下一节我们将对我们的内存管理做出改进。也就是实现C语言中 mallocfree 函数。

老规矩,本节的代码地址:https://github.com/lyajpunov/os

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/293840.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CTF wed安全(攻防世界)练习题

一、Training-WWW-Robots 进入网站如图&#xff1a; 翻译&#xff1a;在这个小小的挑战训练中&#xff0c;你将学习Robots exclusion standard。网络爬虫使用robots.txt文件来检查它们是否被允许抓取和索引您的网站或只是其中的一部分。 有时这些文件会暴露目录结构&#xff0c…

Qt6.6添加多媒体模块Multimedia报错问题

问题 QT包含多媒体模块Multimedia时提示未知的模块&#xff1a; error: Project ERROR: Unknown module(s) in QT: multimedia 在帮助文档中只可以找到QMediaPlayer类&#xff0c;但是点进去是空的&#xff0c;这是因为没有安装多媒体模块及对应的帮助文档。 解决 使用在线…

StringBuffer与StringBuilder

1.区别 (1). String : 不可变字符序列. (2). StringBuffer : 可变字符序列.线程安全&#xff0c;但效率低. (3). StringBuilder : 可变字符序列.线程不安全&#xff0c;但效率高. 既然StringBuffer与StringBuilder都是可变字符序列&#xff0c;但二者咋区分开呢&#xff1f…

黑马点评项目笔记 II

基于Stream的消息队列 stream是一种数据类型&#xff0c;可以实现一个功能非常完善的消息队列 key&#xff1a;队列名称 nomkstream&#xff1a;如果队列不存在是否自动创建&#xff0c;默认创建 maxlen/minid&#xff1a;设置消息队列的最大消息数量 *|ID 唯一id&#xff1a;…

200个有趣的HTML前端游戏项目合集(持续更新中)

&#x1f482; 个人网站:【 摸鱼游戏】【神级代码资源网站】【工具大全】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;注册地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…

Docker工作流

1.工作流 开发应用编写Dockerfile构建Docker镜像运行Docker容器测试应用发布镜像到Hub迭代更新镜像 2.开发应用 首先你需要创建一个应用&#xff0c;这个应用可以是后端应用或者前端应用&#xff0c;任何语言都可以。 比如&#xff1a;我使用IDEA 创建一个Java后端应用&…

【C++庖丁解牛】默认成员函数

&#x1f341;你好&#xff0c;我是 RO-BERRY &#x1f4d7; 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f384;感谢你的陪伴与支持 &#xff0c;故事既有了开头&#xff0c;就要画上一个完美的句号&#xff0c;让我们一起加油 目录 前言1. 构造函数1.1 概念…

云手机:实现便携与安全的双赢

随着5G时代的到来&#xff0c;云手机在各大游戏、直播和新媒体营销中扮演越来越重要的角色。它不仅节约了成本&#xff0c;提高了效率&#xff0c;而且在边缘计算和云技术逐渐成熟的背景下&#xff0c;展现出了更大的发展机遇。 云手机的便携性如何&#xff1f; 云手机的便携性…

【嵌入式智能产品开发实战】(十二)—— 政安晨:通过ARM-Linux掌握基本技能【C语言程序的安装运行】

目录 程序的安装 程序安装的本质 在Linux下制作软件安装包 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: 嵌入式智能产品开发实战 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xf…

在 Windows 11 上安装 MongoDB

MongoDB 是一个流行的 NoSQL 数据库&#xff0c;它提供了灵活的数据存储方案&#xff0c;而 MongoDB Compass 则是一个可视化管理工具&#xff0c;可以更轻松地与 MongoDB 数据库交互和管理。在本文中&#xff0c;我们将介绍如何在 Windows 11 上安装 MongoDB&#xff0c;并配置…

Dimitra:基于区块链、AI 等前沿技术重塑传统农业

根据 2023 年联合国粮食及农业组织&#xff08;FAO&#xff09;、国际农业发展基金&#xff08;IFAD&#xff09;等组织联合发布的《世界粮食安全和营养状况》报告显示&#xff0c;目前全球约有 7.35 亿饥饿人口&#xff0c;远高于 2019 年的 6.13 亿&#xff0c;这意味着农业仍…

C++教学——从入门到精通 6.ASCII码与字符型

如何把小写字母转换成大写字母呢&#xff1f; 这个问题问的好&#xff0c;首先我们要新学一个类型——char 这个类型就是字符型 再来说说ASCII码 给大家举几个例子 空格————32 0————48 9————57 A————65 Z————90 a————97 z————122 我们…

HarmonyOS 应用开发之线性容器

线性容器实现能按顺序访问的数据结构&#xff0c;其底层主要通过数组实现&#xff0c;包括ArrayList、Vector、List、LinkedList、Deque、Queue、Stack七种。 线性容器&#xff0c;充分考虑了数据访问的速度&#xff0c;运行时&#xff08;Runtime&#xff09;通过一条字节码指…

Django 仿博客园练习

数据库搭建 部分功能介绍 【一】注册 &#xff08;1&#xff09;效果显示、简单简介 主要亮点 结合了layui和forms组件默认头像可以随着性别的选择发生改变自定义头像可以实时更新显示forms组件报错信息可以局部刷新显示在对应框体下面 没有直接使用layui的前端验证后端验证…

C++教学——从入门到精通 5.单精度实数float

众所周知&#xff0c;三角形的面积公式是(底*高)/2 那就来做个三角形面积计算器吧 到吗如下 #include"bits/stdc.h" using namespace std; int main(){int a,b;cin>>a>>b;cout<<(a*b)/2; } 这不对呀&#xff0c;明明是7.5而他却是7&#xff0c;…

商城业务-检索服务

文章目录 前言一、搭建页面环境1.1 静态界面搭建1.2 Nginx 动静分离1.3 Windows 上传文件1.4 引入 thymeleaf 依赖1.5 Nginx 反向代理1.4 Nginx 配置1.5 gateway 网关配置 二、调整页面跳转2.1 引入依赖2.2 页面跳转 三、检索查询参数模型分析抽取3.1 检索业务分析3.2 检索语句…

【上海大学计算机组成原理实验报告】二、数据传送实验

一、实验目的 了解在模型机中算术、逻辑运算单元的控制方法。学习机器语言程序的运行过程。通过人工译码&#xff0c;加深对译码器基本工作原理的理解。 二、实验原理 根据实验指导书的相关内容&#xff0c;本次实验所要用的CP226实验仪在手动方式下&#xff0c;运算功能通过…

记录何凯明在MIT的第一堂课:神经网络发展史

https://www.youtube.com/watch?vZ5qJ9IxSuKo 目录 表征学习 主要特点&#xff1a; 方法和技术&#xff1a; LeNet 全连接层​ 主要特点&#xff1a; 主要特点&#xff1a; 网络结构&#xff1a; AlexNet 主要特点&#xff1a; 网络结构&#xff1a; Sigmoid Re…

碳素光线疗法与宠物健康

碳素光线与宠物健康 生息在地球上的所有动物、在自然太阳光奇妙的作用下、生长发育。太阳光的能量使它们不断进化、繁衍种族。现在、生物能够生存、全仰仗于太阳的光线。太阳光线中、包含有动物健康所需要的极为重要的波长。因此、和户外饲养的动物相比、在室内喂养的观赏动物、…

全套医院手术麻醉系统源码 人工智能麻醉系统源码 医疗管理系统源码

全套医院手术麻醉系统源码 人工智能麻醉系统源码 医疗管理系统源码 手术麻醉临床信息系统有着完善的临床业务功能&#xff0c;能够涵盖整个围术期的工作&#xff0c;能够采集、汇总、存储、处理、展现所有的临床诊疗资料。通过该系统的实施&#xff0c;能够规范麻醉科的工作流…