系统调用流程的看上去挺简单的,首先在用户进程中使用 int 0x80 指令触发软中断,然后在对应的 Int0x80_Entry 中断入口根据 eax 跳转到不同的子功能函数执行,示意图如下:
user
|--- BUILD.json
|--- include
| |--- common.h
| |--- syscall.h
|--- lib
| |--- BUILD.json
| |--- syscall.c
// 无参数的系统调用
#define _SYS_CALL0(_NR) ({ \
U32 retval; \
asm volatile( \
"int $0x80" \
: "=a" (retval) \
: "a" (_NR) \
: "memory" \
); \
retval; \
})
// 一个参数的系统调用
#define _SYS_CALL1(_NR, ARG1) ({ \
U32 retval; \
asm volatile( \
"int $0x80" \
: "=a" (retval) \
: "a"(_NR), "b"(ARG1) \
: "memory" \
); \
retval; \
})
...(省略)
extern syscall_table
Int0x80_Entry:
BeginISR
push edi ; 第 5 个参数
push esi ; 第 4 个参数
push edx ; 第 3 个参数
push ecx ; 第 2 个参数
push ebx ; 第 1 个参数
call [syscall_table + eax*4]
add esp, 20 ; 跨过上面 5 个参数
EndISR
// void Int0x80Handle(U32 eax)
// {
// if(0 == eax)
// {
// TaskDestory();
// }
// }
extern E_RET SYS_TaskDestory(void); // 任务销毁
typedef E_RET (*SYSCALL_FUNC)();
SYSCALL_FUNC syscall_table[] = {
SYS_TaskDestory,
};
目前 syscall_table 仅有一个系统调用接口,那就是销毁任务,接下来我们要在应用程序 app 中调用到该系统调用实现销毁任务
由于目前 app 代码中包含的依旧包含了内核头文件,内核中头文件目录与 user 头文件目录下都有 “syscall.h” ,为了避免编译错误,暂时把 user 中的 “syscall.h” 改名为 “u_syscall.h”, “syscall.c” 改名为 “u_syscall.c”
“app” 目录下的 “BUILD.json” 文件中 “inc” 项需要增加 user 头文件的路径 "../user/include",这个头文件路径是相对于编译脚本 “AppBuild.py” 的路径。其实 app 本不应该包含内核头文件路径的,不过现在应用程序代码还没完全拆分,将就一下吧,等后面再完全拆分
前面不是实现了几个 _SYS_CALLx 宏嘛,具体用法如下,由于销毁当前任务函数不需要传参,所以调用 _SYS_CALL0 即可,“u_syscall.c” 中目前仅有这一个接口函数,当然,现在接口函数太少,所以目前以及接下来我们的用户接口函数都写到这个文件中,等以后接口丰富了,再分类出来
E_RET TaskDestory(void)
{
return _SYS_CALL0(0);
}
先进入 “user” 文件夹下,执行 “python UserBuild.py”,编译出临时文件 “u_syscall.o”,这个文件在 “output/user/” 目录下,把它复制到 “output/app/” 目录下
准备工作已经完成,是不是感觉还缺点什么?显然,我们在应用程序 app 中还没有调用 TaskDestory 这个接口
修改 “app.c” 中 TASK B,让其在执行一段时间后销毁
void TaskBFunc(void) // 任务执行函数
{
static U32 count = 0;
while(1)
{
...
if(count > 100000)
{
printk("TaskDestory\n");
TaskDestory();
}
}
}
切回工程根目录,执行 “python Build.py” 命令,接下来流程就不说了,bochs 运行起来,成功实现了系统调用,成果展示
不管系统调用中的参数是几个,中断入口 “Int0x80_Entry” 处始终压入 5 个参数,也许你会对此感到奇怪
是这个样子的,子功能函数都有自己的函数声明,声明中包括参数个数及类型,编译时编译器会根据函数声明在栈中匹配处正确的参数数量,进入函数体后,根据 C 调用约定,栈顶的 4 字节是函数返回地址,往上(高地址栈底方向)的 4 字节是第一个参数,再往上的 4 字节是第二个参数,依此类推。在函数体中,编译器生成的取参指令是从栈顶往上(跨过栈顶的返回地址)获取参的,参数个数是通过函数声明事先确定好的,因此不会出现获取到错误的参数,从而保证了多余的参数用不上,因此,即便我们压入了 5 个参数,但对于少于 5 个参数的函数也不会出错,仅仅只是浪费一点点栈空间而已
实际体验一下吧,本次改动代码见:u_syscall.c、u_syscall.h、syscall.c、app.c
在 user 中的 “u_syscall.c” 中写一个用于测试的系统调用函数,其函数声明放到 “u_syscall.h” 中,函数内容如下
void TestFunc(U32 arg1, U08 arg2, U32 arg3, U16 arg4)
{
_SYS_CALL4(1, arg1, arg2, arg3, arg4);
}
在内核中的 “syscall.c” 中也对应实现其对应的 “SYS_TestFunc” 函数并放到 syscall_table 数组中
void SYS_TestFunc(U32 arg1, U08 arg2, U32 arg3, U16 arg4)
{
printk("arg1 = %d; arg2 = %d; arg3 = %d; arg4 = %d\n", arg1, arg2, arg3, arg4);
}
...
SYSCALL_FUNC syscall_table[] = {
SYS_TaskDestory,
SYS_TestFunc,
};
进入 “user” 文件夹下,执行 “python UserBuild.py”,编译出中间文件 “u_syscall.o”,这个文件在 “output/user/” 目录下,把它复制到 “output/app/” 目录下
把上一步放到 “Build.py” 脚本中吧,具体见:Build.py
在应用程序 “app.c” 中调用一下 “TestFunc” 系统调用函数
void TaskDFunc(void) // 任务执行函数
{
static U32 count = 0;
TestFunc(1, 2, 3, 4);
while(1)
{
...
}
}
最后再切回工程根目录,执行 “python Build.py” 命令,编译运行,看看结果
函数调用都有返回值,那么系统调用呢?肯定也要有返回值呀
我们知道,根据 C 调用约定 函数调用的返回值是存储到 eax 寄存器中的
我们再看一下 0x80 中断入口,在执行完 call [syscall_table + eax*4] 后,根据 C 调用约定,系统调用的返回值已经存储到 eax 寄存器中了,然而,在接下来的步骤 EndISR 中,经过恢复上下文后,eax 又会被恢复到之前的 eax 的值
Int0x80_Entry:
BeginISR
...
call [syscall_table + eax*4]
add esp, 20 ; 跨过上面 5 个参数
EndISR
所以解决办法就是把 eax 寄存器中的值写到任务上下的 eax 元素中,具体操作如下:
%macro EndISR 0
mov esp, [current_reg] ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置
mov [esp + 11*4], eax ; 把 eax 寄存器中的值写到任务上下文 eax 元素中
; 恢复上下文
pop gs
pop fs
pop es
pop ds
popad ; 恢复通用寄存器
add esp, 4 ; 跳过 err_code
iret ; 恢复 ss esp eflags cs eip 这 5 个寄存器
%endmacro
内核中代码改动如下,随便给个返回值 7
U32 SYS_TestFunc(U32 arg1, U08 arg2, U32 arg3, U16 arg4)
{
printk("arg1 = %d; arg2 = %d; arg3 = %d; arg4 = %d\n", arg1, arg2, arg3, arg4);
return 7;
}
user 用户接口层代码修改如下
U32 TestFunc(U32 arg1, U08 arg2, U32 arg3, U16 arg4)
{
return _SYS_CALL4(1, arg1, arg2, arg3, arg4);
}
应用程序 app 中代码改动如下,调用 TestFunc,并打印出调用后的返回值
void TaskDFunc(void) // 任务执行函数
{
...
ret = TestFunc(1, 2, 3, 4);
printk("ret = %d\n", ret);
while(1)
{
...
}
}
改动不多,代码就不单独提供了,直接看一下运行效果吧
补充:由于 EndISR 在别的中断中也会用到,然而目前只有 0x80 号中断需要将 eax 寄存器的值写到任务上下文 eax 元素中 ,为了不影响其它中断,最好复制一份 EndISR ,改名为 EndISR1,在 EndISR1 中有把 eax 寄存器中的值写到任务上下文 eax 元素中这一步操作,而在 EndISR 中没有这一步操作,其它中断依旧使用 EndISR 恢复上下文
改动代码见:u_syscall.c、u_syscall.h、syscall.c、print.c、print.h、app.c
系统调用的基本流程我们也已经熟悉了,稍微拓展提升一下,遇到可变参数函数又如何实现系统调用呢?参数个数都不固定,那么系统调用怎么传参呢?
方法很多种,比如我们可以把要用户打印的各种格式的数据拼成一个字符串数组,计算出拼出来的字符串长度,然后把这个字符串首地址和长度传递给内核,再由内核实现打印到屏幕上,这时候不就固定只传 2 个参数了嘛
接下来我们将实现用户打印 print 函数,print 函数为可变参数函数
由于目前 user 接口层代码较少,暂时把接口实现都放到 “u_syscall.c” 文件中吧
首先定义一个全局数组用于打印,将要打印的内容统统转为字符存入该数组中,最大打印字符数 BUFF_LEN
static U08 print_buff[BUFF_LEN] = {0};
print 函数实现与内核 printk 实现基本一致,只不过内核 prntk 是直接打印出来,而 print 是把所有要打印的数据都转成字符存到 print_buff 中,还有一点区别就是 print 最后调用 _SYS_CALL2 宏将字符数组首地址和字符串长度这两个参数传递到内核 SYS_print 函数
U32 print(const char * fmt, ...)
{
...
va_start(args, fmt);
// 遍历字符串 fmt
for(; *fmt; ++fmt)
{
// 寻找格式转换字符 '%', 如果不是 '%', 则将其转存到 print_buff 中
if(*fmt != '%')
{
if(index < BUFF_LEN)
ptr[index++] = *fmt;
continue;
}
// 如果找到了 '%', 则取 '%' 后的第一个字符
++fmt;
switch (*fmt) // 根据 '%' 后的字符类型采取不同的打印接口
{
case 'c': // 字符
...
break;
case 's': // 字符串
...
break;
case 'x': // 十六进制
...
break;
case 'd': // 十进制
...
break;
default:
break;
}
}
va_end(args);
return _SYS_CALL2(1, ptr, index);
}
真正实现打印功能函数 SYS_print 在内核 “print.c” 中,其内容如下:
U32 SYS_print(U08* buf, U16 len)
{
U32 ret = 0;
U16 i = 0;
for(i = 0; i < len; i++)
{
PrintChar(buf[i]);
ret += 1;
}
return ret;
}
接下来肯定就是将系统调用函数 SYS_print 放到 “syscall.c” 的 syscall_table 数组中了,注意其在数组中的位置要与 user 中函数调用接口一致
打印功能系统调用实现已经完成了,肯定想尝试一下吧,别急,顺便把 SetCursorPos 也改成系统调用形式吧,内核中改名为 SYS_SetCursorPos,具体实现就不再介绍了,对大家来说肯定小意思
既然打印已经实现了系统调用,那么在工程上我们就要实现内核代码与应用代码的完全分离了,首先 “app” 文件夹下的 “BUILD.jspn” 文件就不能涉及任何内核中的代码了,其中头文件只能包含自己目录下的以及 “user” 目录下的,但是 app 的代码编译又需要包含 “app.h” 这个头文件,于是,我们把内核 “include” 下的 “app.h” 文件复制一份到 “user/include” 目录下;回想一下,之前我们是不是把 kernel 编译生成的 “output/” 目录下的 “print.o” 中间文件复制一份到到 “output/app/” 目录下了,因为当时 app 程序中用到的打印依旧使用内核实现的代码,现在改为了系统调用方式,于是要删除 “output/app/” 目录下的 “print.o” 文件
差点漏了一点,由于用户打印 print 为可变参数函数,其参数获取也要使用到 “stdarg.h”, 所以要把内核 “include” 目录下的 “stdarg.h” 复制一份到 “user/include/” 目录下
其实,从此刻开始,内核与应用才彻底分离
最终成果肯定是要展示一下的,虽然效果跟没分离前的并无两样
总结一下当前的系统架构,如下图所示
应用程序 app 目前已与内核 kernel 完全分离
应用程序 app 不可直接使用内核 kernel 中的任何东西
应用程序 app 想要使用内核 kernel 中的功能,必须通过中间接口层 user 实现,user 中通过 int 0x80 号软中断实现系统调用
本质上讲 user 和 app 都属于 3 特权级的应用程序,user 中编译实现的代码编译形成的中间 ".o" 文件最终还是被 app 链接到一起形成应用程序
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。