oslab_final
OSLab 综合实验
- 实验1
- 利用当前OrangeS所提供的系统调用和API,自行编写一个应用程序,并编译
生成存储在文件系统 - 在Shell中调用该程序,可启动并执行进程
- 进程结束后返回Shell
- 利用当前OrangeS所提供的系统调用和API,自行编写一个应用程序,并编译
- 实验2
- 内核模块的编写:完成一个Linux/Windows内核/驱动模块的编写,
能够实现对文件访问的监控、或者对键盘设备、USB设备、网络设备、
蓝牙设备等的监控。
- 内核模块的编写:完成一个Linux/Windows内核/驱动模块的编写,
内存管理
Init 进程
- 使用 init 进程作为父进程来执行 fork(),根据 fork() 的返回值判断该进程是父进程还是子进程。
- 如果返回值为 0,表示该进程为子进程
- 如果返回值大于 0,表示该进程为父进程,返回值为其创建的子进程的 pid
- 返回值为 -1 表示子进程创建失败。
- 通过循环不停地进行 wait()。当进程 P 的子进程未 exit(),进程 P 先于子进程 exit() 时,init() 可以接受 P 的子进程并使他们退出,清理进程表。
fork 准备工作
- 修改最大进程数的宏定义 NR_PROCS,在进程表 proc_table[] 中预留空项
- 将未用到的进程表表项的 p_eflags 设为 FREE_SLOT,以此判断表项是否为空
- 遍历进程表,对 init 进程的 LDT 进行单独赋值,区分与 fork() 出来的子进程占用的内存空间,init 进程内存空间大小为内核空间大小
- 获取内核的内存使用范围
- loader.asm,将内存信息写入到内存中
- 写入 Magic Number 作为内存使用信息的标识
- 写入内存大小
- 写入 kernel.bin 在内存中的物理位置
- 通过 get_kernel_map() 读取内核的内存范围,内核占用内存 0x1000~0x3BFA8 的位置,大小约 240 KB
- loader.asm,将内存信息写入到内存中
- 进程表与 LDT 之间的关系
fork(), wait(), exit()
向 MM 发送 FORK/WAIT/EXIT 消息,执行对应操作
MM(memory management)
通过不断循环来接受消息。接收到 FORK/WAIT/EXIT 的消息后,调用 do_fork() / do_wait() / do_exit() 进行相应处理。
do_fork()
- 根据 FREE_SLOT 标记查找空闲的进程表表项,选择合适的位置放入,无空闲表项时返回 -1
- 将进程表表项的索引作为 pid 分配给子进程
- 将父进程的进程表表项完全复制到子进程中,标记上父进程的 pid
- 分配内存
- 读取父进程的 LDT,获取代码段、数据和堆栈段的基址和大小,得到父进程的内存占用情况
- 通过判断三个段的基址、界限和大小是否相等,来保证三个段指向同一片内存空间
- 使用 alloc_mem() 为子进程分配与父进程一样大的内存空间,将父进程的内存信息复制到子进程的内存空间中
- 通过 init_desc() 更新子进程的 LDT
- 通知 FS 执行 fs_fork(),完成父子进程之间的文件共享。
- 发送消息解除阻塞,将返回值 0 传递给子进程,将子进程 pid 传递给父进程
内存分配 alloc_mem()
- 将 10MB 以上的内存空间留给用户进程使用,通过宏定义进程空间基址和每个进程默认分配空间大小
1 |
|
- 对于请求大于 1MB 空间的进程请求不予以分配,返回错误信息
- 分配内存的起始地址为 基址+(用户进程数-1)*默认分配空间大小
- 分配后的内存空间大于总内存空间,不予以分配并返回错误信息
文件描述符处理
fs_fork()
记录有多少个进程使用同一个 inode,每次 fork() 后 i_cnt 自增 1
记录有多少个进程在使用 f_desc_table[] 中同一的 file_desc,每次 fork() 后 fd_cnt 自增 1
fs_exit()
记录有多少个进程使用同一个 inode,每次 exit() 后 i_cnt 自减 1
记录有多少个进程在使用 f_desc_table[] 中同一的 file_desc,每次 exit() 后 fd_cnt 自减 1
do_exit() 函数
- 通知 FS 执行 fs_exit(),结束文件共享
- 释放 pid 对应进程的内存空间
- 如果父进程使用了 wait() 进行等待
- 清除父进程的 WAITING 位
- 像父进程发送解除阻塞的消息,父进程 wait() 结束
- 释放进程的进程表表项,子进程 exit() 结束
- 父进程未使用 wait() 则将设置进程的 HANGING 位
- 遍历进程表,如果该进程存在子进程 B
- 将子进程 B 的父进程设置为 init()
- 判断 init() 是否为 WAITING 且子进程 B 状态为 HANGING
- 如果是
- 清除 init() 的 WAITING 位
- 向 init 发送消息,接触阻塞,init 进程 wait() 结束
- 释放子进程 B 的进程表表项,子进程 exit() 结束
- 如果不是
- init 正在 WAITING,B 没有 HANGING,B 调用 exit() 再执行
- init 没有 WAITING,B 正在 HANGING,inti 调用 wait() 再执行
- 如果是
do_wait()
- 遍历进程表,如果发现存在正在 HANGING 的子进程 A
- 向进程发送消息解除阻塞,wait() 结束
- 释放子进程 A 的进程表表项,子进程 exit() 结束
- 没有 HANGING 状态的子进程
- 设置进程的 WAITING 位
- 没有子进程
- 向进程发送携带错误信息返回值的消息,wait() 结束
cleanup()
- 向父进程发送进程退出消息
- 将进程表表项的标志位 p_eflags 设为 FREE_SLOT,表示该进程表项可用
do_exec()
- 从消息中获取要执行程序的参数信息,如文件名、调用者
- 通过 stat() 获取要执行文件的文件大小
- 读取被执行文件,将文件写入 MM 的缓存中
- 根据 ELF 文件的文件头,将程序的各个部位放到合适的位置中
- 建立参数栈,通过计算源地址和目标地址的偏移 delta 重新定位已经在 execv() 建立好的栈
- 对 ecx 和 eax 进行赋值,为将 argv 和 argc 压栈做准备
- 通过设置 eip 设置程序的入口地址
- 通过设置 esp 设置程序的堆栈地址
- 修改进程名为被执行文件的名称
execl()
- 接受命令行的参数,转换成指针形式交给 execv() 处理
execv()
- 遍历参数,获取参数个数
- 将指针数组的末尾赋值为零
- 遍历所有的字符串,将字符串复制到堆栈结构 arg_stack[] 中
- 将每个字符串的地址写入指针数组的对应位中
- 将 arg_stack[] 的首地址和长度等内容以消息的形式发送给 MM,让 MM 执行 do_exec()
shell 的实现
- init() 启动两个 shell,运行在 TTY1 和 TTY2 中,通过 Ctrl + Fn 进行切换
- 读取用户输入,fork 一个子进程
- 子进程中将输入交给 execv() 执行
- 遇到无效命令则回显输入
键盘记录驱动
驱动程序结构
- 驱动加载 module_init(kbdlogger_init)
- 驱动卸载 module_exit(kdblogger_exit)
- 许可证说明 MODULE_LICENSE(“GPL”)
- 通过 register_keyboard_notifier() 将键盘记录在缓冲区 buf 中,在卸载驱动的时候将缓冲区 buf 写入到文件中
keymap 结构
将 keycode 划分为两个部分,前两个字节为 type,后两个字节为 value
根据 type 第二个字节,将键盘布局进行划分
- ASCII 区
- type 第二个字节为 0x0 和 0xb
- 开启大写时,type 第二个个字节为 0xa
- 小键盘区
- 数字键启用时,第二个字节为 0x3
- 数字键关闭时,第二个字节为 0x2
- 功能锁
- type 第二个字节为 0x2
- 方向键
- type 第二个字节为 0x6
- 模式切换键
- type 第二个字节为 0x7
- Fn 和 方向键上方的控制键
- type 第二个字节为 0x1
根据 value 设置同一级键盘区不同 keycode 对应的输出
- ASCII 键盘区
- value 值作为输出字符串在数组 ascii[] 中的索引
- 根据 value 值判断大写键是否开启,记录大写键按键和释放按键行为
- Fn 键和方向键上方控制键
- Fn 键中的 n 值由 value 的低字节决定,n = (value & 0x0f) + 1
- 非 Fn 键根据 value 低字节作为输出字符在数组 fncs[] 中的索引
- 方向键
- value 值作为输出字符串在数组 arw[] 中的索引
- 模式切换键
- value 值作为输出字符串在数组 arw[] 中的索引
- 记录模式切换键的按键行为
- 小键盘
- 数字键禁用/启用时,value 作为 unlocked_nums[] / locked_nums[] 中的索引
实验过程分析
- 按键占用问题
在 Ubuntu 系统中使用 bochs,ALT 键会被 Ubuntu 独占,无法在 bochs 中直接使用 ALT 键。书中代码的使用了 ALT + Fn 进行不同 TTY 的切换,无法正常使用。
这里可以将 ALT 键更改为未被占用的 CTRL 键。tty.c 中的函数 PUBLIC void in_process(TTY* tty, u32 key) 控制了 TTY 的切换,将 select_console 发生条件中的 FLAG_ALT_L 和 FLAG_ALT_R 修改为 FLAG_CTRL_L 和 FLAG_CTRL_R,即可使用 CTRL + Fn 进行 TTY 的切换。
- 应用程序安装问题
每次往文件系统中创建文件时,都需要在 mkfs() 中添加代码,并使用 dd 命令将文件写入磁盘映像中。
为了减少工作量和出错的可能性,可以通过将所有应用压缩成 tar 文件,放进文件系统中后再解压。这样一来,应用程序的安装过程变成了:①编译链接应用程序;②将应用程序压缩为 tar 文件;③将 tar 压缩包写入磁盘映像中特定扇区;④在 mkfs() 中新建一个 tar 文件,在 init() 过程将文件解压,放入文件系统中。
- 文件解压过程无法完成
直接添加应用程序 dnmd 时,tar 压缩包解压过程无法完成,猜测可能是由于添加 dmnd 后文件过大,无法全部解压。将原 pwd 删除后问题解决。
- 如何得知系统的 keymap 信息
当前系统 keymap 的获取:在终端输入 dumpkeys 命令可以查看当前系统的键盘映射表。
实验结果
添加并执行应用程序
运行 bochs,使用 ctrl + F2 切换到 TTY1
运行应用程序 dnmd,执行结果
键盘记录器测试
加载监控驱动后,输入字符串进行测试,卸载驱动,记录文件 keyboard.log 会在驱动卸载时自动生成。
1 |
|
键盘记录器执行结果