6.S081 第一讲:概述
课程地址:https://www.bilibili.com/video/BV19k4y1C7kA
民间大佬做的课程资料翻译:http://xv6.dgs.zone/
捏妈妈的,这课我从开始看就一直摆,不能摆了,我火速给他干完!
一、操作系统的目标
- 对硬件抽象
- 让许多应用程序复用硬件(可以同时运行多个软件,且不相互干涉)
- 程序间的隔离
- 程序间的共享 Sharing
- 安全/权限系统 security
- Performance
- Range of OS,能够支持许多不同的功能
二、操作系统概述
用户空间User:运行软件的空间,例如vim,gcc,shell
Kernel:一个单独的特殊程序,管理计算机硬件资源,电脑启动时Kernel就启动
Kernel中提供了一些内置服务,例如
- 文件系统(文件名等)访问磁盘
- 对进程的管理
- 内存分配
每个正在运行的程序都称作进程,其占用一定的内存
内核也提供对进程的某个操作的权限机制,访问控制
课程主要研究内核以及内核提供的接口
系统调用:内核提供的API,跳转到内核来完成某些操作
课程实验环境:QEMU模拟器,RISC-V微处理器指令集,xv6系统
Shell指令的本质:
例如mkdir
指令,实际上它是存在于这台计算机上的一个可执行文件,一个程序,输入mkdir
指令时会运行这个程序,来完成一些功能。在一些
pwn
题中,拿到靶机的shell后会发现能够使用的指令很少,应该就是因为靶机削减了一些不必要的的指令程序。
只能说第一节课确实没啥含金量的知识点,印象里第一节课老师大概也是教了一下我们这个实验用的 xv6 是怎么个用法。
实验
0x00 启动xv6
按6.S081的指导,下载好xv6
的源代码后直接make qemu
就可以了
使用的系统是ubuntu 20.04,执行make qemu
时发现缺少了一些软件包,只需要使用apt
指令进行下载安装即可。
0x01 sleep(Easy😋)
这个实验要求编写一个sleep程序并在xv6中使用
达到使得进程暂停用户所输入的时间数的目的。
我的代码:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
void main(int argc, char *argv[]) {
if(argc <= 1) {
fprintf(2, "sleep: invaild input.\n");
exit(0);
}
int tick = atoi(argv[1]);
if(tick < 0) {
fprintf(2, "sleep: invaild input:%s\n", argv[1]);
exit(0);
}
sleep(tick);
printf("(nothing happens for a little while)\n");
exit(0);
}
按照实验要求,将sleep.c
文件放入user文件夹
然后在Makefile的这个位置填写相关信息即可:
(被水印挡住的部分是
$U/_sleep\
)
这里就是仅新增了一个条目:$U/_sleep\
然后来测试一下这个实验有没有通过,使用指令
$ ./grade-lab-util sleep
直接薄纱!
0x02 pingpong(Easy😋)
实验要求:
编写一个使用了unix系统调用的程序来实现两个进程之间的通信
父进程向子进程发送字节,子进程打印"<pid>: received ping"
,其中<pid>
是进程ID
然后子进程在管道中写入字节发送给父进程并推出
父进程读取从子进程发送来的字节然后打印"<pid>: received pong"
并推出
我的源代码:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
void main(int argc, char *argv[]) {
int fd[2];
int stat;
pipe(fd);
char a;
if(fork() == 0) {
//son
while(read(fd[1], &a, 1) == 0);
printf("%d: received ping\n", getpid());
write(fd[1], "a", 1);
}else {
//father
write(fd[1], "a", 1);
wait(&stat);
while(read(fd[0], &a, 1) == 0);
printf("%d: received pong\n", getpid());
}
exit(0);
}
运行测试指令:
$ ./grade-lab-util pingpong
薄纱!
0x03 Primes(Moderate/Hard😨)
这个实验有点小离谱,使用进程间通信实现素数算法
然后我代码写的也挺离谱的,一个 unsigned long long
就嗯拆成俩int
用
我的源代码:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
typedef unsigned long long piper;
piper newPipe() {
piper p;
pipe(((int *)&p));
return p;
}
void pipeWriteInt(piper pipe, int toWrite) {
write(((int *)&pipe)[1], &toWrite, 4);
}
int pipeReadInt(piper pipe) {
int toRet;
int result = read(((int *)&pipe)[0], &toRet, 4);
return result == 0 ? 0 : toRet;
}
void pipeClose(piper p) {
close(((int *)&p)[1]);
close(((int *)&p)[0]);
}
void pipeCloseRead(piper p) {
close(((int *)&p)[0]);
}
void pipeCloseWrite(piper p) {
close(((int *)&p)[1]);
}
int getPipeFirst(piper pipe) {
int t = pipeReadInt(pipe);
if(t == 0)exit(0);
fprintf(2, "prime %d\n", t);
return t;
}
void pipe_sender(piper lpipe, piper rpipe, int first) {
int i;
while((i = pipeReadInt(lpipe)) != 0) {
if(i % first != 0) {
pipeWriteInt(rpipe, i);
}
}
pipeCloseRead(lpipe);
pipeCloseWrite(rpipe);
}
void prime(piper lpipe) {
pipeCloseWrite(lpipe);
piper rpipe = newPipe();
int first = getPipeFirst(lpipe);
pipe_sender(lpipe, rpipe, first);
if(fork() == 0) {
prime(rpipe);
}else {
wait(0);
}
}
void main(int argc, char *argv[]) {
piper p = newPipe();
int i;
for(i = 2; i <= 35; i++) {
pipeWriteInt(p, i);
}
if(fork() == 0) {
prime(p);
}else {
pipeClose(p);
wait(0);
}
exit(0);
}
虽然代码离谱,但是运行了测试指令
$ ./grade-lab-util primes
之后,
仍然薄纱!
0x04 find(Moderate😨)
1. 如何遍历目录啊,你▢▢▢▢的
在欣赏
user/ls.c
之前,先听首歌放松放松吧
显然,只需要使用open
函数,向open
函数输入一个字符串即可得到对应文件/文件夹的文件描述符。
创建一个struct stat
结构体对象,这个对象能保存一个文件描述符描述的对象的一些信息,使用fstat
即可将相关信息写入这个结构体对象。
通过fstat
可以看到对应文件描述符指向的是一个普通文件还是一个文件夹,如果要遍历文件夹,还需要用到一个struct direct
结构体……
等等,你不会觉得我要亲自从头写一遍这个byd代码吧,直接从ls.c
的基础上改不就完事了?
user/ls.c
还是挺容易懂的,然而懒得自己写了就直接在这个基础上改了。
2. 开始薄纱!
我的代码如下:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
char *
fmtname(char *path)
{
static char buf[DIRSIZ + 1];
char *p;
// Find first character after last slash.
for (p = path + strlen(path); p >= path && *p != '/'; p--)
;
p++;
// Return blank-padded name.
if (strlen(p) >= DIRSIZ)
return p;
memmove(buf, p, strlen(p));
memset(buf + strlen(p), '\0', DIRSIZ - strlen(p));
return buf;
}
void find(char *path, char *name)
{
char buf[512], *p;
char *ptr;
int fd;
struct dirent de;
struct stat st;
if ((fd = open(path, 0)) < 0)
{
fprintf(2, "ls: cannot open %s\n", path);
return;
}
if (fstat(fd, &st) < 0)
{
fprintf(2, "ls: cannot stat %s\n", path);
close(fd);
return;
}
switch (st.type)
{
case T_FILE:
printf("%s\n", path);
break;
case T_DIR:
if (strlen(path) + 1 + DIRSIZ + 1 > sizeof buf)
{
printf("find: path too long\n");
break;
}
strcpy(buf, path);
p = buf + strlen(buf);
*p++ = '/';
while (read(fd, &de, sizeof(de)) == sizeof(de))
{
if (de.inum == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
if (stat(buf, &st) < 0)
{
printf("find: cannot stat %s\n", buf);
continue;
}
ptr = fmtname(buf);
if(!strcmp(name, ptr)) {
printf("%s\n", buf);
}
if (st.type == T_DIR && ptr[0] != '.')
{
find(buf, name);
}
}
break;
}
close(fd);
}
int main(int argc, char *argv[])
{
int i;
if (argc < 3)
{
find(".", argv[2]);
exit(0);
}
for (i = 2; i < argc; i++)
find(argv[1], argv[i]);
exit(0);
}
运行一下测试指令
./grade-lab-util find
薄纱!
0x05 xargs(moderate🥱)
1. 基本
然而本fw并不知道unix下的xargs指令是怎么玩的,令人感叹。
大概看明白了。首先,对于unix shell的|
符号,是这个情况:
例如:
$ echo flag | cat
这个指令将输出flag
为什么会输出flag呢?当我们直接使用cat指令时,我们就可以往程序里输入数据,我们输入什么,程序就输出什么。
使用|
符号,实际上就是把echo
指令的 stdout
给重定向为cat
的stdin
,也就是说,echo flag | cat
就相当于我们输入了一个cat
指令之后再在下面输入flag
同样的,
$ echo test2 | grep t
将会输出:
继续测试一个:
#include<stdio.h>
#include<stdlib.h>
int main() {
int i;
scanf("%d", &i);
printf("%d\n", i);
return 0;
}
使用gcc编译,输出了一个a.out
程序,来运行它:
$ echo 1888|./a.out
这个scanf
没等我们自己输入一个数字,而是直接使用了echo
向a.out
的stdin
输出的字符串转化为的数字。
2. 开始薄纱!
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
char *newArg[256];
char buf[512];
int readLine(char *b) {
int i = 0;
char buffer;
while(read(0, &buffer, 1) == 1 && buffer != 10) {
b[i] = buffer;
i++;
}
b[i] = '\0';
return i;
}
void main(int argc, char *argv[]) {
int i, k;
if(argc < 2) {
fprintf(2, "xargs: too few arguments!\n");
exit(0);
}
while(readLine(buf) != 0) {
for(i = 0; i < argc - 1; i++) {
newArg[i] = argv[i + 1];
}
newArg[argc - 1] = buf;
if(fork() == 0) {
exec(argv[1], newArg);
exit(0);
}else {
wait(&k);
continue;
}
}
exit(0);
}
运行一下测试指令:
$ ./grade-lab-util xargs
薄纱!
0x06 实验成绩与提交实验
首先在实验目录创建一个time.txt
文件,里面填写一个整数,表示做实验用的时间。
然后尝试用make grade
指令测试一下自己的实验成绩:
满昏,赢!
首先使用以下指令应用我们对实验文件做出的更改:
$ git commit -am "ready to submit my lab"
然后,使用提交网站来提交作业
使用自己的邮箱注册6.S081的网站,然后你就会收到一个邮件,里面包含你的api-key
运行
$ make handin
指令来提交作业。
初次提交会要求我们输入我们的api-key
可能会因为电脑上缺少一个叫做
curl
的工具而运行失败,手动apt
安装即可
提交成功!
0x07 实验总结
-
fork()
函数:创建一个新的进程,这个新的进程是完全复制当前进程的,且其也执行到父进程执行完毕fork()
函数的位置
并且,在父进程中,fork()
函数返回子进程的id
,而在子进程中,返回0,因此,可以利用这样的写法使得父进程与子进程的代码执行分离开:if(fork() == 0) { //子进程的代码 }else { //父进程的代码 }
-
pipe(int pip[2])
函数:创建一个管道,管道可以理解成一种特殊的文件,它不存在于硬盘上,只存在于内存中。pipe
函数的参数是一个长度为2的int
数组,这个函数会为数组中的两个元素赋值,是两个文件描述符。其中,pip[0]
是用于向管道中读取数据的文件描述符,pip[1]
是用于向管道写入数据的文件描述符。管道机制可以用于进程之间的通信。wait()
函数:使一个进程停止,除非它的子进程消失,否则这个进程则会一直卡在执行到wait
函数的位置。
-
遍历文件的方法:
open
函数可以接受一个字符串,然后返回对应的文件描述符- 有一个
fstat
函数,向里面传进一个文件描述符与一个struct stat
对象的指针,即可向这个stat
结构体对象中写入有关这个文件描述符的一些数据。 st.type
保存一个值,表示对应文件描述符的类型,宏定义的常量T_FILE
表示这是个文件,T_DIR
表示这是个文件夹- 之后的兄弟我也看不明白了,反正跟着
user/ls.c
走就完事了
-
关于
shell
中的|
符号
我们平常使用Linux的时候,经常进行这样的操作:ls | grep aaa
实际上,如果当我们直接输入grep aaa
,程序就会进入一个让我们不断输入内容的模式,
当我们输入的字符串含有aaa,则程序会输出这个字符串,并且把aaa给标红
执行ls | grep aaa
时,实际上ls
已经被执行了,但是用这种写法,ls
的stdout
流会被重定向为grep
的stdin
流,因此,grep
会从ls
输出的那些内容中进行读取来完成它自己的任务,其效果就与我们执行grep aaa
后,手动把一个目录里的全部文件的名字敲上去一样。
好累好累~
听首歌放松一下吧[https://music.163.com/song?id=25892489&userid=1344834927](