操作系统上的程序——蒋炎岩课程笔记

516 阅读5分钟

课程链接:jyywiki.cn/OS/2022/

操作系统上的程序

1. 数字电路与状态机

  • 状态 = 寄存器保存的值 (flip-flop)
  • 初始状态 = RESET (implementation dependent)
  • 迁移 = 组合逻辑电路计算寄存器下一周期的值

例:

X′=¬XY

Y′=¬X∧¬Y

shell代码如下:

#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 -E 文件名 展开红,可以得到直接用c实现数字电路模拟器的代码

更完整的实现数字模拟器:

代码如下:

#include <stdio.h>
#include <unistd.h>

#define REGS_FOREACH(_)  _(X) _(Y)
#define OUTS_FOREACH(_)  _(A) _(B) _(C) _(D) _(E) _(F) _(G)
#define RUN_LOGIC        X1 = !X && Y; \
                         Y1 = !X && !Y; \
                         A  = (!X && !Y) || (X && !Y); \
                         B  = 1; \
                         C  = (!X && !Y) || (!X && Y); \
                         D  = (!X && !Y) || (X && !Y); \
                         E  = (!X && !Y) || (X && !Y); \
                         F  = (!X && !Y); \
                         G  = (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);
  OUTS_FOREACH(DEFINE);
  while (1) { 
  
    RUN_LOGIC;
    OUTS_FOREACH(PRINT);
    REGS_FOREACH(UPDATE);
    putchar('\n');
    fflush(stdout);
    sleep(1);
  }
}

利用管道连接后端实现:

后端.py代码如下:

import fileinput
 
TEMPLATE = '''
\033[2J\033[1;1f
     AAAAAAAAA
    FF       BB
    FF       BB
    FF       BB
    FF       BB
    GGGGGGGGGG
   EE       CC
   EE       CC
   EE       CC
   EE       CC
    DDDDDDDDD
''' 
BLOCK = {
    0: '\033[37m░\033[0m', # STFW: ANSI Escape Code
    1: '\033[31m█\033[0m',
}
VARS = 'ABCDEFG'

for v in VARS:
    globals()[v] = 0
stdin = fileinput.input()

while True:
    exec(stdin.readline())
    pic = TEMPLATE
    for v in VARS:
        pic = pic.replace(v, BLOCK[globals()[v]]) # 'A' -> BLOCK[A], ...
    print(pic)

管道连接命令为:.c文件名 | python3 .py文件名

运行结果如下:

2.what is the 程序 ?

2.1 源代码角度

C 程序的状态机模型 (语义,semantics)

  • 状态 = 堆 + 栈

  • 初始状态 = main 的第一条语句

  • 迁移 = 执行一条简单语句

    • 任何 C 程序都可以改写成 “非复合语句” 的 C 代码

c程序是一个状态机,那么如何理解函数调用和函数返回?

  • 状态 = stack frame 的列表 (每个 frame 有 PC) + 全局变量

  • 初始状态 = main(argc, argv), 全局变量初始化

  • 迁移 = 执行 top stack frame PC 的语句; PC++

    • 函数调用 = push frame (frame.PC = 入口)
    • 函数返回 = pop frame

应用:将任何递归程序就地转为非递归

  • 汉诺塔难不倒你 hanoi-nr.c

  • A → B, B → A 的也难不倒你

    • 还是一样的 call(),但放入不同的 Frame

代码如下:

#include <stdio.h>
#include <assert.h>

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

typedef struct {
  int pc, n;
  char from, to, via;
} Frame;

void hanoi(int n, char from, char to, char via) {
  Frame stk[64], *top = stk - 1;
  call(n, from, to, via);
  Frame *f;
  for (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);
    }
  }
}

int main(){
        hanoi(3,'A','B','C');
        return 0;
}

2.2 二进制代码角度

也是状态机

  • 状态 = 内存 M + 寄存器 R
  • 初始状态 = (稍后回答)
  • 迁移 = 执行一条指令

操作系统上的程序,所有的指令都只能计算

  • deterministic: mov, add, sub, call, ...
  • non-deterministic: rdrand, ...
  • 但这些指令甚至都无法使程序停下来 (NEMU: 加条 trap 指令)

如下图,

为了让程序停下来,我们可以使用某些指令,例如neum_trap

2.2.1 调用操作系统 syscall

  • 把 (M,R) 完全交给操作系统,任其修改

  • 一个有趣的问题:如果程序不打算完全信任操作系统?

  • 实现与操作系统中的其他对象交互

    • 读写文件/操作系统状态 (例如把文件内容写入 M)
    • 改变进程 (运行中状态机) 的状态,例如创建进程/销毁自己

  • 程序 = 计算 + syscall
  • 问题:怎么构造一个最小的 Hello, World?

2.2.2 构造最小 Hello, World

如果是下面代码的hello world的话,这个代码并不是最小的,甚至可以说很大

int main() {
  printf("Hello, World\n");
}

解决办法:hello world 的汇编实现

.globl. start
_start:
  movq $SYS_ _write, %rax # write(
  movq $1, %rdi           # fd=1,
  movq $st, %rsi          # buf=st,
  movq $(ed - st), %rdx   # count=ed-st
  syscall                 # );
  movq $SYS_ exit, %rax   # exit(
  movq $1, %rdi           # status=1
  syscall                 # );
st:
   .ascii "\033[01;31mHello, OS World\033[0m\n"
ed:

ps:printf的背后sys_write函数 即系统调用syscall

状态机视角的程序

  • 程序 = 计算 → syscall → 计算 → ...

2.2.3 特殊编码的字符实现终端控制

  • vi.c from busybox

  • telnet ``towel.blinkenlights.nl (电影;Ctrl-] and q 退出)

  • dialog --msgbox 'Hello, OS World!' 8 32

  • ssh sshtron.zachlatta.com (网络游戏)

    • 所以编程可以从一开始就不那么枯燥

    • 看似复杂,实际简单明了

3.what is the 编译器 ?

  • 编译器:源代码 S (状态机) → 二进制代码 C (状态机)

  • C=compile(S)

  • 编译 (优化) 的正确性 (Soundness):

    • SC 的可观测行为严格一致

      • system calls; volatile variable loads/stores; termination
    • rivially 正确 (但低效) 的实现

      • 解释执行/直接翻译 S 的语义
  • 现代 (与未来的) 编译优化

    • 在保证观测一致性 (sound) 的前提下改写代码 (rewriting)

      • Inline assembly 也可以参与优化

        • 其他优化可能会跨过不带 barrier 的 asm volatile
      • Eventual memory consistency

      • Call to external CU = write back visible memory

        • talk is cheap, show me the code!

4.操作系统中的一般程序

  • 操作系统收编了所有的硬件/软件资源

    • 只能用操作系统允许的方式访问操作系统中的对象

      • 从而实现操作系统的 “霸主” 地位
      • 例子:
      • #include <stdio.h>
        #include <fcntl.h>
        #include <unistd.h>
        
        void try_open(const char *fname) {
          int fd = open(fname, O_RDWR);
          printf("open("%s") returns %d\n", fname, fd);
          if (fd < 0) {
            perror("  FAIL");
          } else {
            printf("  SUCCESS!\n");
            close(fd);
          }
        }
        
        int main() {
          try_open("/something/not/exist");
          try_open("/dev/sda"); // hard drive
        }
        
    • 这是为 “管理多个状态机” 所必须的

      • 不能打架,谁有权限就给他
  • 杀人的面试题 (1):一个普通的、人畜无害的 Hello World C 程序执行的第一条指令在哪里?

    • 等价问法

      • “二进制程序状态机的初始状态是什么?”

        • main 的第一条指令 ❌
        • libc_start
    • 问 gdb 吧

      • info proc {mappings,...} - 打印进程内存

  • main() 之前发生了什么?

    • ld-linux-x86-64.so 加载了 libc

      • 之后 libc 完成了自己的初始化

        • RTFM: libc startup on Hurd
        • main() 的开始/结束并不是整个程序的开始/结束
        • 例子:
        • #include <stdio.h>
          
          __attribute__((constructor)) void hello() {
            printf("Hello, World\n");
          }
          
          // See also: atexit(3)
          __attribute__((destructor)) void goodbye() {
            printf("Goodbye, Cruel OS World!\n");
          }
          
          int main() {
          }
          
  • main 执行之前、执行中、执行后,发生了哪些操作系统 API 调用?

    • 这门课中很重要的工具:strace

      • system call trace

      • 理解程序运行时使用的系统调用

        • demo: strace ./hello-goodbye
        • 在这门课中,你能理解 strace 的输出并在你自己的操作系统里实现相当一部分系统调用 (mmap, execve, ...)

5.总结

  • 程序 = 状态机

    • 源代码 S: 状态迁移 = 执行语句
    • 二进制代码 C: 状态迁移 = 执行指令
    • 编译器 C=compile(S)
  • 应用视角的操作系统

    • 就是一条 syscall 指令
  • 计算机系统不存在玄学;一切都建立在确定的机制上

    • 理解操作系统的重要工具:gcc, binutils, gdb, strace

参考文章:

zhuanlan.zhihu.com/p/77663680

www.zhihu.com/question/21…

jyywiki.cn/OS/2022/sli…