抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

七つの海

今日、海を見た。もう怖くない

操作系统原理的上机实验的报告一共有四个,其他的报告的地址是:

第一次:https://shiraha.cn/2020/class-FoDOS-experiment-1/
第二次:https://shiraha.cn/2020/class-FoDOS-experiment-2/
第三次:https://shiraha.cn/2020/class-FoDOS-experiment-3/
第四次:https://shiraha.cn/2020/class-FoDOS-experiment-4/

Front-matter

本次实验的所有源代码可以在https://dev.azure.com/Pure-Asahi/_git/2020_Spring_In_Class_Job?path=%2Fosnmb%2Fexp 查看到。

实验要求

本次上机实验的实验要求如下:

#####
《操作系统原理》第一次上机实验

一、实验目的

  1. 理解操作系统生成的概念和过程;
  2. 理解操作系统两类用户界面(操作界面,系统调用)概念;

二、实验内容

  1. 在 Ubuntu 或其他 Linux 平台环境下裁剪和编译 Linux 内核,并启用新的内核。
  2. 在 Ubuntu 或其他 Linux 平台为 Linux 内核增加 1-3 个新 的系统调用,并启用新的内核,编写一个应用程序测试新增加的系 统调用是否能正确工作。
  3. 在 Windows 环境下,编写一个批处理程序(算命大 师.bat),程序运行后,输入:出生年月日(例如 2000-07-31)。系 统输出相应的属相和星座,例如:你属兔, 狮子座。要求:输入进 行合法性检查,能循环接收用户的输入,直到输入 q 或 Q 就退出。

要求一、二在上机前自己做过了,本次实验报告只简述实验过程和贴图。要求三完成了简单的 Bat 版本和 Powershell 的复杂版本。

实验内容

  1. 在 Ubuntu 或其他 Linux 平台环境下裁剪和编译 Linux 内核,并启用新的内核。
  2. 在 Ubuntu 或其他 Linux 平台为 Linux 内核增加 1-3 个新 的系统调用,并启用新的内核,编写一个应用程序测试新增加的系 统调用是否能正确工作。
  3. 在 Windows 环境下,编写一个批处理程序(算命大 师.bat),程序运行后,输入:出生年月日(例如 2000-07-31)。系 统输出相应的属相和星座,例如:你属兔, 狮子座。要求:输入进 行合法性检查,能循环接收用户的输入,直到输入 q 或 Q 就退出。

第一、第二个实验的内容原文来自本博客:https://shiraha.cn/2019/Linux-FoDOS-experiment-dian/,选择的 Linux 平台是 Arch Linux。
第三个实验的内容原文来自本博客:https://shiraha.cn/2020/class-FoDOS-experiment-bat-programming/,这篇文章只包括了 Powershell 的脚本的实验过程。

实验过程

下面是对于每一个任务的原理和过程的简述:为了遵循实际实验的最佳顺序,讲述顺序为实验二、实验一、实验三(实验二的完成本身需要实验一的工作

增加系统调用

总的来说,增加最简单的系统调用主要分为 增加新函数 -> 增加新声明 -> 将新调用增加到系统调用注册表 三个步骤。复制一份下载的纯净内核文件,修改其中的include/uapi/asm-generic/unistd.hinclude/linux/syscalls.h两个头文件来增加系统调用的函数。

进入系统调用注册表的目录arch/x86/entry/syscalls,根据要安装的操作系统位数修改其下的tbl文件,注册增加的系统调用。特别注意不应该与已经存在的系统调用的编号冲突,这点在后面增加宏的时候再次重复。

使用vim打开系统调用的实现的源文件kernel/sys.c来增加刚才增加的函数的实现。如果要增加的是在内核缓冲区打印消息这种最简单的系统调用,则它可以是下面这样:

1
2
3
4
5
6
asmlinkage void sys_shirohashow(void)
{
printk("Shiroha do your best!");
printk("\n@Edit by Shiroha on 2019-11-15.");
return 0;
}

这样,我们就增加了一个系统调用函数sys_shirohashow的实现。完成之后还要在include/uapi/asm-generic/unistd.h增加宏。这个系统调用的含义是在内核缓冲区(用户程序不可以实现的功能)打印一条消息,需要使用dmesg -c命令清除所有缓冲区日志来查看。

dmesg命令主要有下面三个用途:

  • 列出所有检测到的硬件
  • 输出指定行数的日志
  • 清空dmesg缓冲区日志

这里主要就是使用了第三个功能,用来检查printk的输出结果。

此外,上述函数体的修饰符asmlinkage宏确保了这个函数使用堆栈传递参数;所有的系统调用函数都应该使用这个进行修饰。

添加完实现之后还需要在unistd.h文件中增加这个函数的声明。这个文件在 Arch 中的位置是include/uapi/asm-generic/unistd.h;我们使用 Vim 打开它并在末尾增加:

1
2
#define __NR_shirohashow 765
__SYSCALL(__NR_shirohashow,sys_shirohashow)

765 是这个系统调用的编号,选取它的原因是因为 346/283 被其他系统调用占用了而它没有。需要确认一个系统调用编号是否已经被占用,可以打开系统调用注册表进行确认(比如syscall_64.tbl,可以使用find指令找到这个文件所在目录)。

还有一处需要增加声明的地方是include/linux/syscalls.h;打开这个文件并且增加标准的 C 函数声明:

1
asmlinkage void sys_shirohashow(void);

这个文件应该有注释表明了这个函数实现所在的位置,建议遵循规则增加到注释kernel/sys.c的区域。

将修改过的内核和纯净内核使用diff -Naur生成差异补丁,再使用patch命令应用到纯净内核上。使用make -j6进行多线程编译。当然你也可以直接在修改的内核上直接编译,patch 一次纯属娱乐,没太多实际意义。当然也可以作为对于patch指令的熟悉。

编译成功后执行安装,并将必要的文件复制到特定的地方之后更新grub的引导信息,就完成了内核的应用。应用新内核将会在下一个任务的实验过程中较为详细的介绍。

应用新内核之后,在运行新内核的操作系统上写C程序,调用增加的系统调用,就可以看到我们在内核中编写的程序可以成功运行了。随后通过一些方法(对于printk,就是使用dmesg -c查看内核缓冲区)验证实验结果。对于上述步骤增加的系统调用,提供一个示例 C 程序用于测试:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <sys/reg.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/types.h>

int main()
{
syscall(765);
return 0;
}

编译后执行,应该就可以在内核缓冲区中看到输出的鼓励话语了。

编译内核

实验背景:使用 Hyper-V (第二代) 虚拟机平台,安装 Arch Linux。已经下载好了对应版本的 Linux 官方的纯净内核源代码文件。

从官网上下载的内核已经提供了编译的配置文件,甚至还提供了一个比较图形化的menuconfig页面,提供了较为可视化的 .config 文件的生成方法。因为我们只是在一般的 PC 上安装新 Linux 内核,所以不需要进行特别的裁剪和配置,使用缺省配置就好:

1
zcat /proc/config.gz > .config

复制得到 .config 文件之后还建议修改文件中的CONFIG_LOCALVERSION的值,避免将要编译的内核文件覆盖当前内核文件。生成了 config 文件之后就可以使用make -jx开始多线程编译了。建议 x 取值和计算机所持有的物理核心数相同以获得最大编译速度。

安装新编译生成的新内核的步骤可以简单归纳为:安装内核模块 -> 复制内核文件 -> 制作initramfs镜像 -> 复制System.map -> 生成新的启动引导。

安装内核模块可以使用下面的命令:

1
sudo make modules_install 

这条命令会将编译好的模块安装到主目录 /lib/modules 下。这样,会使得这些模块独立于虚拟机原有内核的模块。

复制内核文件可以使用下面的命令:

1
2
3
4
5
# 对于32位(i686)内核:
sudo cp -v arch/x86/boot/bzImage /boot/vmlinuz-linux53

# 对于64位(x86_64)内核:
sudo cp -v arch/x86_64/boot/bzImage /boot/vmlinuz-linux53

将内核编译完成生成的bzImage(较大的压缩的内核映像,使用gzip压缩)文件复制到/boot目录下。

制作initramfs(初始内存盘)镜像可以通过复制并且修改mkinitcpio(一个创建initramfs的脚本)preset,这样就可以通过官方内核一样的方式生成自定义内核的initramfs镜像。复制之后需要使用vim修改这个文件。

1
2
sudo cp /etc/mkinitcpio.d/linux.preset /etc/mkinitcpio.d/linux53.preset
sudo vi /etc/mkinitcpio.d/linux53.preset

打开linux53.preset文件之后,修改部分字段使得它与新的自定义内核所匹配。需要修改的字段如下:

1
2
3
4
5
# /etc/mkinitcpio.d/linux53.preset

ALL_kver="/boot/vmlinuz-linux53"
default_image="/boot/initramfs-linux53.img"
fallback_image="/boot/initramfs-linux53-fallback.img"

修改并保存之后执行sudo mkinitcpio -p linux53就可以使用官方内核生成的方式生成自定义内核的initramfs镜像。

复制System.map的步骤可能不是必须的。如果虚拟机的/boot挂载到的分区的文件系统是ext4格式。就需要将解压目录下的System.map文件复制到/boot中,并且创建/boot/System.map,将新建的System.map软链接到复制到其中的System.map中。需要执行的命令如下:

1
2
3
sudo touch /boot/System.map
sudo cp System.map /boot/System.map-MyKernel
sudo ln -sf /boot/System.map-Mykernel /boot/System.map

但是由于本次使用的虚拟机的 boot 分区挂载的分区是vfat文件系统的,无需也不必创建软链接;所以本次上机实验中这一步实际并没有去做。

最后需要更新更新 grub 引导的配置信息。在安装了 grub 之后,使用命令grub-mkconfig -o /boot/grub/grub.cfg可以生成grub默认的配置信息。它会自动地将刚添加的内核增加到启动配置中。在Ubuntu这种系统中,这个命令被包装成了update-grub或其他形式。因为本次实验使用的是 Arch 发行版,所以需要完整的命令。

最后使用 reboot 指令重启虚拟机,通过安装的 screenfetch 工具来获取当前的系统信息,就可以发现系统的内核信息已经更新了,实验完成。在 Arch 中,需要使用系统默认的包管理工具 pacman 安装 screenfetch

脚本编写

分析实验要求,绘制程序执行流程图:

流程图

接下来就是使用一些具体的脚本语言实现这个设计了。

对于 Powershell 脚本,可以使用的“库”有很多,实现也就可以更加的花哨一些。单就读取用户输入方面们就有很多的方案可供选择:可以使用Powershell原生的Read-Host,也可以使用框架提供的GUI窗口;甚至还可以使用 Visual Basic 的窗口。

得益于 Powershell 脚本功能的强大,我们可以定义一个函数完成对于日期合法性的判断。最后根据用户输入的日期进行一些处理,得到输出的字符串即可。为了节约篇幅,这里省略了一些预先声明的,和处理日期生成输出字符串相关的一些变量的初始化(以省略号代替)。使用框架提供的输入窗口获取用户输入的源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

[int[]] $days = 29,31,28,31,30,31,30,31,31,30,31,30,31
$dayIsIllegal = "The days you input is illegal. try again"
[string[]] $months = ...
[string[]] $iiyokoiyo = @(...)

function isLunar ([int] $y)
{
if($y % 4) {return 'False'}
else {if($y % 400) {return 'True'}
else {if($y % 100) {return 'False'}
else {return 'True'}}}
}

while(1)
{
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Birthday input form'
$form.Size = New-Object System.Drawing.Size(300,200)
$form.StartPosition = 'CenterScreen'

$okButton = New-Object System.Windows.Forms.Button
$okButton.Location = New-Object System.Drawing.Point(65,120)
$okButton.Size = New-Object System.Drawing.Size(75,23)
$okButton.Text = 'OK'
$okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$form.AcceptButton = $okButton
$form.Controls.Add($okButton)

$cancelButton = New-Object System.Windows.Forms.Button
$cancelButton.Location = New-Object System.Drawing.Point(160,120)
$cancelButton.Size = New-Object System.Drawing.Size(75,23)
$cancelButton.Text = 'Cancel'
$cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$form.CancelButton = $cancelButton
$form.Controls.Add($cancelButton)

$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point(10,20)
$label.Size = New-Object System.Drawing.Size(280,20)
$label.Text = 'Input your Birthday as YYYY-MM-DD'
$form.Controls.Add($label)

$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = New-Object System.Drawing.Point(10,40)
$textBox.Size = New-Object System.Drawing.Size(260,20)
$form.Controls.Add($textBox)

$form.Topmost = $true

$form.Add_Shown({$textBox.Select()})
$result = $form.ShowDialog()

if ($result -eq [System.Windows.Forms.DialogResult]::OK)
{
$x = $textBox.Text
"Your input: "+$x
if($x -eq 'q' -or $x -eq 'Q')
{
"Exting ..."
exit
}
[int[]] $data = $x.Split('-')
if($data.Count -ne 3)
{
Write-Host -ForegroundColor RED "The String you input is not refer to a date."
continue
}
if($data[0] -lt 1)
{
Write-Host -ForegroundColor YELLOW "You cannot born before AC. try again"
continue
}
if($data[1] -lt 1 -or $data[1] -gt 12)
{
Write-Host -ForegroundColor RED "The month is illegal. try again"
continue
}
$lunar = isLunar $data[0]
$month = $data[1]
if ($lunar -eq 'True' -and $data[1] -eq 2) {$month = 0}
if($data[2] -lt 1 -or $data[2] -gt $days[$month])
{
Write-Host -ForegroundColor RED $dayIsIllegal
continue
}

Write-Host -ForegroundColor YELLOW "PROOVE STRAT:"
"You born in "+$months[$data[1]]+", which means "+$data[1]+", and there is "+$iiyokoiyo[$data[1]]
"You born at "+$data[2]+", and there is "+$iiyokoiyo[$data[2]]
Write-Host -ForegroundColor GREEN "Q.E.D. You are HonMono No Yaju Senpai ! "

pause
}
}

使用原生的Read-Host获取用户输入也是一种选择;下面是使用Read-Host获得用户输入的部分代码(和上面代码不同的部分):

1
2
3
4
5
6
7
8
9
while(1)
{
$x = Read-Host "Please input your birthday as YYYY-MM-DD "
"Your input: "+$x
if($x -eq "") {"You input nothing, try again."}
...
...
pause
}

上述代码如果对于 Powershell 不甚了解的话则难以理解其中的一些部分,这是因为 Powershell 基于 .NET 框架,提供了很多 Windows 的原生接口。如果是相对低级的 bat 批处理程序,代码就会更加的简洁易懂。下面的代码是使用 bat 脚本语法书写的“算命大师”。和上面的 ps1 脚本不同,仅执行任务要求的星座、属性计算(使用分支语句实现),并且没有复杂的日期判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@echo off
title 算命帶師
:scanf
set /p input=輸入出生日期(按照YYYYMMDD格式)
set "year=%input:~0,4%"
set "md=%input:~4,4%"
set /a mod=%year%%%12
set hint=您輸入的日期格式不太正確,請重試

if %input% EQU "Q" exit
if %input% EQU "q" exit

if %md% LEQ 0099 (echo %hint%
goto scanf)
if %md% GEQ 0132 (if %md% LEQ 0200 (echo %hint%
goto scanf) )
if %md% GEQ 0229 (if %md% LEQ 0300 (echo %hint%
goto scanf) )
if %md% GEQ 0332 (if %md% LEQ 0400 (echo %hint%
goto scanf) )
if %md% GEQ 0431 (if %md% LEQ 0500 (echo %hint%
goto scanf) )
if %md% GEQ 0532 (if %md% LEQ 0600 (echo %hint%
goto scanf) )
if %md% GEQ 0631 (if %md% LEQ 0700 (echo %hint%
goto scanf) )
if %md% GEQ 0732 (if %md% LEQ 0800 (echo %hint%
goto scanf) )
if %md% GEQ 0832 (if %md% LEQ 0900 (echo %hint%
goto scanf) )
if %md% GEQ 0931 (if %md% LEQ 1000 (echo %hint%
goto scanf) )
if %md% GEQ 1032 (if %md% LEQ 1100 (echo %hint%
goto scanf) )
if %md% GEQ 1131 (if %md% LEQ 1200 (echo %hint%
goto scanf) )
if %md% GEQ 1232 (echo %hint%
goto scanf)

if %mod% EQU 0 echo 你属猴
if %mod% EQU 1 echo 你属鸡
if %mod% EQU 2 echo 你属狗
if %mod% EQU 3 echo 你属猪
if %mod% EQU 4 echo 你属鼠
if %mod% EQU 5 echo 你属牛
if %mod% EQU 6 echo 你属虎
if %mod% EQU 7 echo 你属兔
if %mod% EQU 8 echo 你属龙
if %mod% EQU 9 echo 你属蛇
if %mod% EQU 10 echo 你属马
if %mod% EQU 11 echo 你属羊
echo 而且是

if "%md%" LEQ "0119" echo 魔蝎座
if "%md%" GEQ "0120" if "%md%" LEQ "0218" echo 水瓶座
if "%md%" GEQ "0219" if "%md%" LEQ "0320" echo 双鱼座
if "%md%" GEQ "0321" if "%md%" LEQ "0419" echo 白羊座
if "%md%" GEQ "0420" if "%md%" LEQ "0520" echo 金牛座
if "%md%" GEQ "0521" if "%md%" LEQ "0621" echo 双子座
if "%md%" GEQ "0622" if "%md%" LEQ "0722" echo 巨蟹座
if "%md%" GEQ "0723" if "%md%" LEQ "0822" echo 狮子座
if "%md%" GEQ "0823" if "%md%" LEQ "0922" echo 处女座
if "%md%" GEQ "0923" if "%md%" LEQ "1023" echo 天秤座
if "%md%" GEQ "1024" if "%md%" LEQ "1122" echo 天蝎座
if "%md%" GEQ "1123" if "%md%" LEQ "1222" echo 射手座
if "%md%" GEQ "0321" if "%md%" LEQ "0419" echo 白羊座
if "%md%" GEQ "1222" echo 魔蝎座

goto scanf

bat可以取出字符串的一部分为变量赋值,然后再对变量的值进行判断。运行之后应该就可以看到结果了。

实验结果

这里是任务完成后的截图或其他证明。

任务一:增加系统调用

执行实验过程中的实例代码,使用dmesg -c命令可以获得以下输出:

屏幕截图_61_.jpg

可以看到已经在内核缓冲区输出了系统调用中的字符串了。

任务二:安装新内核

下图是安装新内核之后使用screenfetch命令看到的输出:

屏幕截图_33_.jpg

内核版本已经由默认的 5.3.4 变成了 5.3.10,说明新内核已经成功使用了。

任务三:脚本编程

使用Read-Host的 Powershell 脚本:

__OWFP_BLXG_A2FQFD_I3ZE.jpg

使用 .NET 框架提供窗口的 Powershell 脚本:

1_3_1.png

输入内容后会得到和第一张图一样的输出。需要特别注意的是:一般的家用版本的 Windows 10 自带的 Powershell 是不允许直接加载ps1脚本的,需要先行修改运行策略。修改的脚本如下:

1
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

最后是 BAT 批处理脚本“算命大师”的运行结果:

1_3_0.png

可以看到基本功能已经实现并且正常工作。

体会

通过这次实验,我熟悉了Linux内核的应用过程,对Linux命令的理解更进一步;更加生动的理解了系统调用的概念以及Linux操作系统从POST开始之后的启动过程;熟悉了 Powershell 脚本编程,以及基于这项技术的简单开发和部分 .NET API 的使用;

对于受众较小的 Arch Linux 操作系统,比起在网络上漫无目的的查找论坛、博客,不如认真研读官方文档的 trouble-shooting 以及对一些问题可能原因的分析和解释。不仅可以对解决问题带来更加精准的帮助,还可以拓宽我们看待问题的视野。

评论