oslab_final

OSLab 综合实验

  • 实验1
    • 利用当前OrangeS所提供的系统调用和API,自行编写一个应用程序,并编译
      生成存储在文件系统
    • 在Shell中调用该程序,可启动并执行进程
    • 进程结束后返回Shell
  • 实验2
    • 内核模块的编写:完成一个Linux/Windows内核/驱动模块的编写,
      能够实现对文件访问的监控、或者对键盘设备、USB设备、网络设备、
      蓝牙设备等的监控。

内存管理

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
  • 进程表与 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
2
#define PROC_BASE			   0xA00000
#define PROC_IMAGE_SIZE_DEFAULT 0x100000
  • 对于请求大于 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
2
3
4
sudo -s
insmod logger.ko
// 输入测试字符串
rmmod logger

键盘记录器执行结果