引言
- 思考:在当前设计中,内核与应用间的界限是否清晰?
- 很显然,一点也不清晰,内核程序与应用程序统一被编译在一起,应用程序也被当做内核中的一部分
- 所以本章节我们的重点工作就是将应用程序与内核拆分开来
拆分应用程序
- 我们来重新设计一下架构,把应用程序从内核中拆分出去
- 回顾之前的系统启动流程
- 拆分应用程序后的流程如下
拆分应用程序之代码重构
- 代码见:app.c、app.h、task.c、task.h、schedule.c、main.c
- 前面的实验中,我们把任务测试代码都写在了 “main.c” 文件中,这显然是不合理的,现在,我们就来把它从 “main.c” 中移出去
- 创建 “app” 文件夹,其中新增 “app.c” 文件,创建对应的 “app.h” 文件并放到 “include” 文件夹下,那么最外层 “BUILD.json” 配置文件需要在 “dir” 中增加 “app” 文件夹名,同时在 “app” 文件夹下也要新增一个 “BUILD.json” 配置文件负责管理其下的工程源文件
- 首先,我们把原本在 “main.c” 中的任务相关代码移到 “app.c” 中
U08 taskA_stack[512]; // 任务私有栈 void TaskAFunc(void) // 任务执行函数 { static U32 count = 0; while(1) { if(count++ % 10000 == 0) { static U32 j = 0; asm volatile("cli"); SetCursorPos(0, 6); printk("TASK A: %d\n", j++); asm volatile("sti"); } } } ... 省略任务 B C D
- 把 app 和 task 分层,即 app 中不直接调用 task 内容,而是把数据传递到 task,task 负责处理这些数据,我们用 APP_INFO 这个数据结构来传递数据,
typedef struct APP_INFO { void (*pfunc)(void); U08* stackAddr; U16 stackSize; U08* name; E_APP_PRI priority; } APP_INFO;
- 传递过来的数据放到 TASK_OOP taskOop[MAX_TASK_NUM] 数组中,TASK_OOP 类型定义如下:
typedef struct TASK_OOP { QUEUE_NODE QueueNode; U08 active; TASK task; } TASK_OOP;
- 在 app 模块中,我们将应用程序相关数据写到 appInfo 这个数组中
APP_INFO appInfo[MAX_APP_NUM] = {0}; U16 appNum = 0; E_RET AppRegister(APP_FUNC pFunc, U08* stackAddr, U16 stackSize, U08* name, E_APP_PRI priority) { if(appNum >= MAX_APP_NUM) return E_ERR; appInfo[appNum].pfunc = pFunc; appInfo[appNum].stackAddr = stackAddr; appInfo[appNum].stackSize = stackSize; appInfo[appNum].name = name; appInfo[appNum].priority = priority; appNum++; return E_OK; } void AppInit(void) { AppRegister(TaskAFunc, taskA_stack, 512, "TASK A", E_APP_PRI5); AppRegister(TaskBFunc, taskB_stack, 512, "TASK B", E_APP_PRI7); AppRegister(TaskCFunc, taskC_stack, 512, "TASK C", E_APP_PRI9); AppRegister(TaskDFunc, taskD_stack, 512, "TASK D", E_APP_PRI11); }
- 在 task 模块中,我们将 appInfo 中的应用数据转移到 task 自己的任务数据区 TASK_OOP taskOop[MAX_TASK_NUM] 数组中,并根据这些数据创建任务并加入就绪任务队列
#include <app.h> extern APP_INFO appInfo[MAX_APP_NUM]; extern U16 appNum; void TaskInit(void) { U16 index = 0; U16 taskNum = 0; QueueInit(&TASK_READY_QUEUE); // 就绪任务队列初始化 QueueInit(&TASK_WAIT_QUEUE); // 等待任务队列初始化 // 创建第一个任务(空闲任务) TaskCreat(&taskIdle.task, TaskIdleFunc, taskIdle_stack, IDLE_STACK_SIZE, "Idle", E_TASK_PRI15); // 将空闲任务节点添加到就绪任务队列中 QueueAdd(&TASK_READY_QUEUE, (QUEUE_NODE *)&taskIdle); for(index = 0; index < MAX_TASK_NUM && taskNum < appNum; index++) { if(0 == taskOop[index].active) { taskOop[index].active = 1; taskOop[index].task.name = appInfo[taskNum].name; taskOop[index].task.stack_addr = appInfo[taskNum].stackAddr; taskOop[index].task.stack_size = appInfo[taskNum].stackSize; taskOop[index].task.task_entry = appInfo[taskNum].pfunc; taskOop[index].task.priority = appInfo[taskNum].priority; TaskCreat(&taskOop[index].task, taskOop[index].task.task_entry, taskOop[index].task.stack_addr, taskOop[index].task.stack_size, taskOop[index].task.name, taskOop[index].task.priority); QueueAdd(&TASK_READY_QUEUE, (QUEUE_NODE *)&taskOop[index]); taskNum++; } } }
- 注意:由于任务节点改变, “schedule.c” 中 schedule 函数中也要稍微修改一下
void schedule(void) { ... // current_task = (volatile TASK *)((TASK_QUEUE_NODE *)QUEUE_NODE(node, TASK_QUEUE_NODE, QueueNode)->task); current_task = (volatile TASK *)&(((TASK_OOP *)QUEUE_NODE(node, TASK_OOP, QueueNode))->task); ... }
- 注意别忘了任务销毁也要改动
E_RET TaskDestory(void) { ... ((TASK_OOP *)QUEUE_NODE(node, TASK_OOP, QueueNode))->active = 0; ... }
拆分应用程序之单独编译 app
- 上面哗啦哗啦写了一大堆,然而实际上应用程序和内核还是编译在一起,现在我们把它们分开,单独编译出应用程序
- 我们可以单独实现一个编译脚本放到 “app” 文件夹下,专门用于对应用程序部分代码进行编译管理,目前 “app” 文件下目录结构如下:
app |--- AppBuild.py |--- BUILD.json |--- aentry.asm |--- app.c
- 参照 “Build.py”, 实现 “AppBuild.py” ,与 “Build.py” 区别是 “AppBuild.py” 仅实现对 app 的编译和链接工作,见:AppBuild.py
- 由于使用 “AppBuild.py” 进行编译,所以 “BUILD.json” 配置文件稍微做一下修改,“inc” 项中头文件路径改变,这个是 app 下的头文件路径
{ "dir" : [ ], "src" : [ "aentry.asm", "app.c" ], "inc" : [ "../include" ] }
- 可以参照 “kentry.asm” 文件实现 “aentry.asm” 文件,作为应用程序入口,其内容如下:
[section .text] global _start extern AppInit _start: AppEntry: push ebp mov ebp, esp call AppInit leave ret
- 注意了,虽然该修改的代码都已经修改了,但是链接时需要用到 “print.o” 这个编译中间文件,由于脚本原因,只链接 “output/app” 下的 “.o” 文件,我们可以手动把 “output” 文件夹下的 “print.o” 文件复制到 “output/app”,这样子再执行脚本就不会出问题了
- 切换到 “app” 目录下,执行命令:“python AppBuild.py”, 最终生成 “app.bin” 文件
拆分应用程序之内核与应用之间的数据交互
- 应用程序已经被分离出去了,于是,又产生了新的问题,内核与应用之间的数据又该如何交互呢?比如内核 “task.c” 中就需要获得 app 中下面的两个数据
APP_INFOappInfo[MAX_APP_NUM];
U16appNum;
- 原本是统一编译成一个程序,直接使用 “extern” 关键字即可,现在内核与应用分离,肯定不能用 “extern” 关键字了。
- 解决办法:共享内存
- 既然说到共享内存了,那么顺便也将 app 程序的加载地址也考虑一下,重新规划一下内存使用,把内核与应用间共享数据定义在 0xA800 位置,把 app 加载到地址 0x80000,注意这个地址不要大于 1M,因为在实模式下 x86 处理器能访问的最大地址就是 0xFFFFF(1M内),我一开始就规划过大,发现始终无法将 app 程序加载到那个内存地址处
- 注意 “AppBuild.py” 中链接地址也要修改为 0x80000
- 先来考虑内核与应用之间共享内存的使用
- 在 “app.c” 中往贡献内存中写数据
void AppInit(void) { ... // 把应用数据放入共享内存 0xA800 处 *((volatile U32*)0xA800) = appInfo; *((volatile U32*)0xA804) = appNum; ... } • 在内核 “task.c” 中读共享内存数据 void TaskInit(void) { ... APP_INFO* pAppInfo = (APP_INFO *)(*(U32 *)APP_INFO_ADDR); U32 appNum = *((U32*)(APP_NUM_ADDR)); ... }
- 想要内核能跳转到应用程序中执行,我们可以使用函数指针的形式实现
typedefvoid(*APP_INIT)(void);
APP_INITAppInit=(APP_INIT)0x80000; // 0x80000: 应用程序加载地址
AppInit(); // 应用程序模块初始化
- 注意:由于内核与应用分离,根目录下的 “BUILD.json” 配置文件中 “dir” 项应去掉 “app”,因为 app 相关工程代码已经在前面单独编译了
- 代码见:app.c、task.c、share.h、main.c
拆分应用程序之加载应用程序
- 目前我们已经能利用 “AppBuild.py” 成功编译出 app.bin 程序了,接下来就要把 “app.bin” 写到 “a.img” 中,前面已经实现了将 boot、loader 以及 kernel 写入 “a.img” 中,现在写 app 也是类似的,具体就不再详细介绍了,代码实现见:Build.py
- “a.img” 制作成功之后,接下来就是在 “loader.asm” 中将 app 数据读到内存 0x80000 地址处,这里有个需要说明的就是 0x80000 地址超过了 16 位寄存器范围,而 rd_disk_to_mem 函数中使用 bx 寄存器传参,想要访问地址 0x80000 ,于是将 ds 段基址寄存器赋值为 0x8000, mov [bx], ax 这条指令其实相当于 mov [ds:bx], ax,ds:bx 这种表示方法其实就等同于 ds
*
16+bx,ds*
16 就等于 ds 左移 4 位,0x8000 左移 4 位即 0x80000
; 将硬盘扇中 app 数据读入到内存 0x80000 处 mov ax, [0x700] ; loader 所占扇区数 add ax, [0x702] ; + kernel 扇区数 add ax, 2 ; + 2 得到 app.bin 起始扇区 mov cx, [0x704] ; 因为 app 加载地址 0x80000 超过 0xFFFF,通过改动段基址 ds = 0x8000 实现访问内存地址 0x80000 mov dx, 0x8000 mov ds, dx mov bx, 0x0000 call rd_disk_to_mem ; 需恢复 ds=0, 下面的程序需要 ds 为 0 mov dx, 0x0 mov ds, dx
- 到这里程序就已经完成了,为了测试,我们稍微修改一下 “aentry.asm”
_start: AppEntry: mov eax, 0x8899 ; 仅用于调试 mov ebx, 0x5566 ; 仅用于调试 jmp $ ; 仅用于调试 push ebp mov ebp, esp call AppInit leave ret
- 好了,跑起来看一看,当然,啥也看不见,程序死在了上面 app 入口的 jmp $ 处
- 使用 Ctrl+C 退出,使用 “reg” 指令,得到:
eax: 0x00008899 34969 ecx: 0x000007f8 2040 edx: 0x00000000 0 ebx: 0x00005566 21862 esp: 0x00007bdc 31708 ebp: 0x00007be8 31720 esi: 0x00000b04 2820 edi: 0x000009ac 2476 eip: 0x0008000a eflags 0x00003002: id vip vif ac vm rf nt IOPL=3 of df if tf sf zf af pf cf
- 从中我们可以看出,eax,ebx 的值已被成功修改为我们想要的值,这说明目前程序已经能从 kernel 跳转到 app 执行了
- 代码见:loader.asm、aentry.asm
异常处理
- 去掉 “aentry.asm” 中的调试代码,跑起来
_start: AppEntry: push ebp mov ebp, esp call AppInit leave ret
- 果然没有想象中的美好,总要出点问题,貌似只有空闲任务运行,其它任务都没运行起来
- 从 app 入口开始往里查,程序从 AppEntry 执行到 AppInit,我们在 AppInit 函数开头放个打印信息看看程序有没有进来执行注册 app 数据
void AppInit(void) { printk("AppInit\n"); ... }
- 这回运行一下,现象更奇特了,注意了,注意了,不仔细看差点没发现,打印第一行 “Boot...” 消失了
- 初步排除 boot 部分代码问题,因为成功加载了 loader,左思右想,那就是打印第一行被其它打印覆盖了,那么肯定怀疑刚加的 AppInit 函数中的打印
- 继续思考,app 中调用的 print 打印相关函数跟 kernel 中是调用同一个函数吗?
- 很显然,由于我们独立编译了 app,链接时还复制了 print.o 文件,这说明内核 print 相关函数与 app 的 print 相关函数用的不是同一个地址,仅仅只是名字相同而已,kernel 和 app 中各有一份
- 于是在 app 中初始化一下,增加打印颜色和设置光标位置,因为我怀疑默认的打印颜色正好是黑色,所以打印出来的字符串看不到
void AppInit(void) { SetCursorPos(0, 3); // 设置光标位置: (0, 3) SetFontColor(E_FONT_WHITE); // 设置打印字体颜色: 白色 printk("AppInit\n"); ... }
- 再次运行看一下现象,果然如上面猜想的一样,这回成功打印出 “AppInit” 字符串了,但是其它几个任务依旧没有运行起来
- 顺着程序运行往下查呗,接下来就是把应用数据放入共享内存 0xA800 处,这个代码并没有什么问题,那么就是 “task.c” 中获取应用数据出问题了
- 一看,果然,共享内存地址定义出错了
// #define APP_INFO_ADDR (SHARE_START_ADDR + 0xA800) // #define APP_NUM_ADDR (SHARE_START_ADDR + 0xA804) #define APP_INFO_ADDR (SHARE_START_ADDR + 0x800) #define APP_NUM_ADDR (SHARE_START_ADDR + 0x804)
- 修改完成,再次编译运行,哈哈哈,这回终于 OK 了
- 本次异常问题解决相关代码见:aentry.asm、app.c、share.h