NJU-OS课上用到的各种冷知识

早就听说南大jyy老师的操作系统课很牛逼,看了两节课视频后惊为天人,完全符合我对计算机的想象。一大堆小工具的应用,行云流水的操作,太熟练了,特此做个笔记记录一下。

python的sympy包

老师主张第一门语言学习python,并且吐槽微积分考那么多计算技巧没啥用,在现代都可以用电脑解决,比如python的sympy包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> from sympy import *
>>> x = var('x')
>>> x
x
>>> atan(x)
atan(x)
>>> init_printing()
>>> integrate(atan(x))

⎛ 2 ⎞
log⎝x + 1⎠
x⋅atan(x) - ───────────
2
>>> diff(atan(x))
1
──────
2
x + 1

STFW && RTFM

STFW(search the fucking web)
多用google/bing/github/stackoverflow

RTFM(read the fucking manual)
系统自带的man和社区维护的tldr对比:社区维护的tldr相对来说更好读懂,但是主要还是靠man

X-macro

课堂上用X-macro模拟了一个数字电路状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<unistd.h>

#define REGS_FOREACH(_) _(X) _(Y)
#define RUN_LOGIC X1 = !X && Y;\
Y1 = !X && !Y;
#define DEFINE(X) static int X,X##1;
#define UPDATE(X) X = X##1;
#define PRINT(X) printf(#X " = %d; ",X);

int main(){
REGS_FOREACH(DEFINE);
while(1){//clock
RUN_LOGIC;
REGS_FOREACH(PRINT);
REGS_FOREACH(UPDATE);
putchar('\n');sleep(1);
}
}

但这全是宏写的代码实在看不懂,可以使用gcc对其预处理,把宏全部展开
1
gcc -E a.c

展开之后,main函数里的干净代码就比较易懂了
1
2
3
4
5
6
7
8
9
int main(){
static int X,X1; static int Y,Y1;;
while(1){
X1 = !X && Y; Y1 = !X && !Y;;
printf("X" " = %d; ",X); printf("Y" " = %d; ",Y);;
X = X1; Y = Y1;;
putchar('\n');sleep(1);
}
}

wget

wget是一个从网络上下载文件的软件,老师上课时用wget下载课件上的演示代码文件

1
wget https://jyywiki.cn/pages/OS/2022/demos/logisim.c

这个logisim.c相当于刚才a.c数字电路状态机的加强版,里面有七个变量,并且变量按照逻辑数码管的排列变动。

看起来没有用,但是如果为其写一个小后端,就可以变成七段数码管的显示

1
wget https://jyywiki.cn/pages/OS/2022/demos/seven-seg.py

把a.out的输出用管道丢给这个python写的小后端,就可以实现七段数码管显示数字
1
./a.out | python3 seven-seg.py

用借助gdb理解状态机

一段递归的汉诺塔求解代码给很多同学带来了相当大的伤害

1
2
3
4
5
6
7
8
9
10
void hanoi(int n, char from, char to, char via){
if(n == 1)
printf("%c -> %c\n",from,to);
else {
hanoi(n - 1, from, via,to);
hanoi(1, from, to, via);
hanoi(n - 1, via, to, from);
}
return;
}

写一个main.c调用这个函数试试
1
2
3
4
5
#include<stdio.h>
#include"hanoi-r.c"
int main(){
hanoi(3,'A','B','C');
}

1
gcc main.c && ./a.out

得到结果为
1
2
3
4
5
6
7
A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B

如何理解递归,可以用gdb,直接在状态机的视角上观察
1
gcc -g main.c && gdb a.out

比如切换到源代码视角上运行
1
2
3
4
5
layout src
start
s //下一步
info frame //查看栈帧信息
finish //直接完成这个函数比如printf

了解了状态机模型后就可以手动模拟一个递归实现这个算法
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
typedef struct {
int pc,n;
char from, to, via;
} Frame;

#define call(...) ({*(++top) = (Frame) { .pc = 0,__VA_ARGS__}; })
#define ret() ({ top--;})
#define goto(loc) ({ f->pc = (loc) - 1;})

void hanoi(int n, char from, char to, char via){
Frame stk[64],*top = stk-1;
call(n,from,to,via);
for(Frame *f; (f = top)>= stk;f->pc++){
switch (f->pc)
{
case 0:
if(f->n ==1){
printf("%c -> %c\n",f->from, f->to);
goto(4);
}
break;
case 1:
call(f->n-1, f->from, f->via, f->to);
break;
case 2:
call(1, f->from, f->to, f->via);
break;
case 3:
call(f->n-1, f->via, f->to, f->from);
break;
case 4:
ret();
break;

default:
assert(0);
}
}
}

还可以切换到汇编代码视角上运行
1
2
3
4
5
layout asm
start
s //下一步
info frame //查看栈帧信息
finish //直接完成这个函数比如printf

最小的hello world

如何构造一个最小的状态机,初始化后三五步后就调用syscall打印helloworld然后退出?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<sys/syscall.h>

.globl _start
_start:
movq $SYS_write, %rax
movq $1, %rdi
movq $st, %rsi
movq $(ed - st), %rdx
syscall

movq $SYS_exit, %rax
movq $1, %rdi
syscall

st:
.ascii "\033[01;31mHello,OS world\033[0m\n"
ed:

直接写汇编,然后编译链接
1
gcc minimal.S -c && ld minimal.o

ANSI Escape Code

刚才那段最小helloworld通过特殊代码输出了红色的字符,就是ANSI Escape Code。它是特殊编码的字符实现终端控制。

1
"\033[01;31mHello,OS world\033[0m\n"

杀人的面试题

一个普通的人畜无害的helloworld程序的第一条指令在哪里?
它的等价问法是:二进制程序状态机的初始状态是什么?
既不是main()里的第一条指令,也不是libc里的_start

问神奇的gdb吧:

1
2
3
4
5
(gdb) starti
Starting program: /home/glc/OS/a.out

Program stopped.
0x00007ffff7fe32b0 in _start () from /lib64/ld-linux-x86-64.so.2

gdb说第一条指令在/lib64/ld-linux-x86-64.so.2
再打印进程内存看看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) info proc mappings
process 364
Mapped address spaces:

Start Addr End Addr Size Offset Perms objfile
0x555555554000 0x555555555000 0x1000 0x0 r--p /home/glc/OS/a.out
0x555555555000 0x555555556000 0x1000 0x1000 r-xp /home/glc/OS/a.out
0x555555556000 0x555555557000 0x1000 0x2000 r--p /home/glc/OS/a.out
0x555555557000 0x555555559000 0x2000 0x2000 rw-p /home/glc/OS/a.out
0x7ffff7fbd000 0x7ffff7fc1000 0x4000 0x0 r--p [vvar]
0x7ffff7fc1000 0x7ffff7fc3000 0x2000 0x0 r-xp [vdso]
0x7ffff7fc3000 0x7ffff7fc5000 0x2000 0x0 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc5000 0x7ffff7fef000 0x2a000 0x2000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fef000 0x7ffff7ffa000 0xb000 0x2c000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7fff000 0x4000 0x37000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 rw-p [stack]

这些就是这个程序的初始状态

然而凭什么第一条指令在ld-linux-x86-64.so.2呢?

因为程序里是这样写的,于是老师直接手动把生成的二进制文件改里面的ld-linux-x86-64.so改成了rtfm.so,然后在/lib64下把这个so文件软链接到rtfm.so,a.out能够正常运行

1
vim -b a.out

把ld-linux-x86-65.so.2前7个字符改成rtfm.so,剩下字符改成-,:w保存,:%!xxd将文件转换为十六进制显示,把所有-对应的二进制编码2d改成0,保存退出

1
sudo ln -s ld-linux-x86-64.so.2 rtfm.so

软链接出一个rtfm.so

然后就可以正常运行a.out了

计算机系统不存在玄学:一切都建立在确定的机制上

重要工具 strace

1
strace ./hello.out

在屏幕上输出一个程序执行过程中所有的系统调用

例如,用strace看一个编译器是怎样完成编译过程的

1
strace -f gcc hello.c

gcc会启动其它进程,-f用于追踪子进程,具体如何从这一大堆信息里面查看呢?

可以通过管道输出到vim

1
strace -f -gcc hello.c &| vim - //不知为何这个用不了

先打开vim然后执行:r !strace -f -gcc hello.c

然后在编辑器里面还可以grep,这样就只剩下execve了

1
!%grep execve

再把失败的过滤掉,-v反向过滤
1
!%grep -v ENOENT

最后把逗号换成换行,就可以清晰看到execve启动程序时的参数了
1
:%s/, /\r /g

总之,操作系统不再是一个玄学的黑箱子,而是可以观测内部结构的科学状态机


NJU-OS课上用到的各种冷知识
http://tsailooy.top/2023/12/03/NJU-OS课上用到的各种冷知识/
作者
Tsailooy
发布于
2023年12月3日
更新于
2023年12月3日
许可协议