操作系统基础
参考链接
操作系统:设计与实现 (2024 春季学期) (jyywiki.cn)
应用视角下的操作系统
构造一个最小程序
要理解操作系统首先要理解程序,下面是一个最简单的程序:
1 | int main() |
当生成输出文件之后,可以使用以下的命令来查看生成的可执行文件
1 | objdump 工具可以查看对应的汇编代码 |
当使用以上工具查看生成的main的可执行文件时,发现可执行文件内容较多。编译链接的过程会链接库文件导致可执行文件变大。
那么我们可以尝试着手动的去链接可定位文件,可以先生成一个main.o文件,然后将这个mian.o试着手动链接编译的文件
-
直接用 ld 链接失败:因为ld 不知道怎么链接 printf
-
所以只能不调用 printf ,将程序改为以下内容
1
2
3int main()
{
}运行时出现 Segmentation Fault ,调试时发现在return时出现错误
-
如果改成以下代码
1
2
3
4int main()
{
while(1);
>}运行时会卡在死循环中,说明我们的程序时可以运行的
[!NOTE]
为什么会出现
Segmention falut?
当函数在
renturn时,程序会从内存的rsp寄存器中取出地址给PC,作为下一条执行指令的地址,然后rsp+8,栈(rsp存在栈中)是向下生长的当执行完错误的
return语句之后,PC的地址变为了1这意味着在执行
return之前,内存中的rsp指向运行代码的地址,也就是上面的1,所以出现了问题
程序
程序实际上是一个状态机:
1 | struct CPUState { |
处理器:无情的、执行指令的状态机
- 从 𝑀[𝑃𝐶]中取出一条指令
- 执行它
- 循环往复
程度的退出
程序自己是不能 “停下来” 的!
- 指令集里==没有一条关闭计算机的指令==,那么操作系统是如何在关闭所有软件后,切断计算机的电源的?
只能借助操作系统
1 | movq $SYS_exit, %rax # exit( |
- 把 “系统调用” 的参数放到寄存器中
- 执行==syscall==,操作系统接管程序
- 操作系统可以任意改变程序状态 (甚至终止程序)
[!TIP]
syscall是一条特殊指令,由操作系统提供。当
main函数退出时,可以使用syscall指令告诉操作系统,程序即将退出,然后等待syscall的回应,此时一个系统调用就完成了。
[!TIP]
在操作系统之下是电路,实际上在下面的电路也可以看成有一个小型的操作系统,当上层操作系统向底层的操作系统发送指令之后(通过ACPI(高级配置和电源接口)进行交互),底层的操作系统会向它所管理的元器件发送高低电平,从而达到关机的效果
此时就可以构造最小的程序
==mininal.S==
1 | #include // The x86-64 system call Application Binary Interface (ABI): |
使用以下指令运行上面的汇编程序
1 | gcc -g -S minimal.S minimal.s |
程序 = 状态机
==Everything(二进制文件)= 状态机==
状态
- gdb 内可见的内存和寄存器
初始状态
- 由 ABI 规定 (例如有一个合法的 %rsp)
状态迁移
- 执行一条指令
- gdb 可以单步观察状态机的执行
- syscall 指令: 将状态机 “完全交给” 操作系统
[!IMPORTANT]
- 状态 = 变量数值 + 栈
- 初始状态 = main 的第一条语句
- 状态迁移 = 执行一条语句中的一小步
拆解程序
操作系统上有很多不同种类的程序
Core Utilities (coreutils)
Standard programs for text and file manipulation
系统中默认安装的是 GNU Coreutils
系统/工具程序
其他各种应用程序
- Vscode、OBS-Studio、浏览器、音乐播放器:它们在各种工具程序基础上建立起来 (例:ffmpeg)
追踪一个程序
工具程序代表:编译器 (gcc):
1 strace -f gcc a.c #(gcc 会启动其他进程)
可以管道给编辑器
vim -编辑器里还可以
1 %!grep
- 对于开发者来说,工具的组合是非常重要的
图形界面程序代表:编辑器 (xedit):
1 strace xedit
- 图形界面程序和 X-Window 服务器按照 X11 协议通信
- 虚拟机中的 xedit 将 X11 命令通过 ssh (X11 forwarding) 转发到 Host
任何程序 = minimal.S = 状态机
程序在操作系统上的执行过程:
- 总是从被操作系统加载开始
- 通过另一个进程执行 execve 设置为初始状态
- 经历状态机执行 (计算 + syscalls)
- 进程管理:fork, execve, exit, …
- 文件/设备管理:open, close, read, write, …
- 存储管理:mmap, brk, …
- 最终调用 _exit (exit_group) 退出
==应用程序 = 计算 + 操作系统 API==
- 窗口管理器
能直接管理屏幕设备 (read/write/mmap)
- 能画一个点,理论上就能画任何东西
能够和其他进程通信 (send, recv)
- 任务管理器
- 能访问操作系统提供的进程对象 (M1 - pstree)
- 杀毒软件
- 文件静态扫描 (read)、主动防御 (ptrace)
==操作系统的职责:提供令应用程序舒适的抽象 (对象 + API)==
编译器
既然说 “任何程序” 都和 minimal.S 是一样的
- 为什么我们没有在 C 代码里看到系统调用?
- C 代码是如何变成二进制文件的?
- 到底编译器什么优化能做、什么优化不能做?
如果说程序就是一个状态机,那么如何写一个C语言代码的解释器?
在写C语言的解释器的时候,我们首先需要将C代码简化,将C 代码改写成 SimpleC,将C转化为SimpleC需要遵循以下规则:
Everything (C 程序) = 状态机
- 状态 = 变量数值 + 栈
- 初始状态 = main 的第一条语句
- 状态迁移 = 执行一条语句中的一小步
状态机
“状态机” 是拥有严格数学定义的对象。这意味着你可以把定义写出来,并且用数学严格的方法理解它 —— 形式化方法
状态:
[StackFrame, StackFrame, …] + 全局变量
实际上一个状态就是一个栈帧
初始状态:
- 仅有一个 StackFrame(main, argc, argv, PC=0)
- 全局变量全部为初始值
- 每个栈帧中都有一个PC值,用来保存当前的执行指针
状态迁移:
- 执行 frames[-1].PC 处的简单语句
什么是编译器
编译器的输入
- 高级语言 © 代码 = 状态机
编译器的输出
- 汇编代码 (指令序列) = 状态机
编译器 = 状态机之间的翻译器
SimpleC的直接翻译
运算:
- 把操作数 load 到寄存器、执行运算、store 写回结果
分支/循环:
- 使用条件跳转分别执行代码
函数调用:
- 专门留一个寄存器给栈 (SP, Stack Pointer)
- 将 Stack frame 的信息保存在内存里
- 通过 SP 可以访问当前栈帧的变量、返回地址等
所以,==C 被称为高级汇编语言==
- 存在 C 代码到指令集的直接对应关系
- 状态机和迁移都可以 “直译”
- 于是计算机系统里多了一个抽象层 (“一生二、二生三、三生万物”)
- 更 “高级” 的语言就很难了
- C++
virtual void foo();- Python
[1, 2, 3, *rest]- Javascript
await fetch(...)
编译优化
C 语言编译器在进行代码优化时,遵循的基本准则是在不改变程序的==语义== (即程序的行为和输出结果) 的前提下,提高程序的执行效率和/或减少程序的资源消耗
1
2
3
4
5 int foo(int x)
{
int y = x + 1;
return y - 1;
}一些 “不改变语义” 的例子 (编译优化中最重要的 “三板斧”):
- 函数内联:将函数调用替换为函数体本身的内容
- 常量传播:在编译时计算常量表达式的值并替换
- 死代码消除:删除永远不会被执行到的代码
例子
==编译优化前==
1
2
3
4
5 int compute(int x)
{
int y = x + 1;
return y - 1;
}==编译优化前==
1
2
3
4
5
6
7
8
9
10
11
12 int compute(int x)
{
if(true)
{
x = 1;
}
else
{
}
return x;
}==编译优化后==
1
2
3
4 int compute(int x)
{
return x;
}==编译优化后==
1
2
3
4
5
6
7
8
9
10
11
12 int compute(int x)
{
//if(true) 死代码消除
//{
// x = 1; 常量传播
//}
//else 死代码消除
//{
//}
//return x;
return 1; //常量传播
}
编译的正确性
系统调用是使程序计算结果可见的唯一方法
- 不改变语义 = 不改变可见结果
- 状态机的视角:满足C/汇编状态机生成的所有 syscall 序列完全一致,任何优化都是允许的
C 代码中的不可优化部分
- External function calls (链接时才能确定到底是什么代码)
- 未知的代码可能包含系统调用
- 因此不可删除、移出循环等,且要保证参数传递完全一致
- 编译器提供的 “不可优化” 标注
volatile[load | store | inline assembly]
硬件视角下的操作系统
reset之后执行firmware