应用视角的操作系统

本讲内容:指令序列和高级语言的状态机模型;回答以下问题:

  • 什么是软件 (程序)?
  • 如何在操作系统上构造最小/一般/图形界面应用程序?
  • 什么是编译器?编译器把一段程序翻译成什么样的指令序列才算 “正确”?

构建一个最小的程序

1
2
3
4
5
#include<stdio.h>
int main(){
printf("Hello World\n");
return 0;
}

gcc 编译出来的文件一点也不小

  1. objdump 工具可以查看对应的汇编代码
  2. –verbose 可以查看所有编译选项 (真不少)
  3. printf 变成了 puts@plt
  4. -Wl,–verbose 可以查看所有链接选项 (真不少)
    原来链接了那么多东西
    还解释了 end 符号的由来
  5. -static 会链接 libc (大量的代码)

    gcc a.c 和 gcc a.c -static 有什么区别?

强行构造最小的 Hello, World?

1
2
3
int main(){

}
1
2
gcc -c hello.c    -> hello.o 
ld hello.o -e main -> a.out

执行a.out,会发生Segmentation Fault.

如果改为

1
2
3
int main(){
while(1);
}

再继续执行上面的编译命令,发现程序可以正常执行了(死循环)。

查看汇编语言,猜测程序返回时的 ret 出现了错误。

解决异常退出

解决办法:用一条特殊的指令请操作系统帮忙

1
2
3
movq $SYS_exit,  %rax   # exit(
movq $1, %rdi # status=1
syscall # );
  • 把 “系统调用” 的参数放到寄存器中
  • 执行 syscall,操作系统接管程序
    • 程序把控制权完全交给操作系统
    • 操作系统可以改变程序状态甚至终止程序

理解高级编程语言程序

编程语言也是一个状态机

  • 非递归的汉诺塔
    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
    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++) {
    n = f->n; from = f->from; to = f->to; via = f->via;
    switch (f->pc) {
    case 0: if (n == 1) { printf("%c -> %c\n", from, to); goto(4); }
    // 为什么goto(4)只是设置pc为3,是因为循环会++
    break;
    case 1: call(n - 1, from, via, to); break;
    case 2: call( 1, from, to, via); break;
    case 3: call(n - 1, via, to, from); break;
    case 4: ret(); break;
    default: assert(0);
    }
    }
    }
    什么叫函数调用

    函数是由很多个栈帧组成的,每一个栈帧都有一个PC

什么是函数调用?

函数调用就是在栈帧的顶部再加上一个栈帧,这个栈帧的PC是0,然后把参数放到栈上

什么是函数返回

把顶部的栈给抹掉

什么是执行一条语句

取最顶上栈帧PC上的语句执行

  • 代码讲解
    这是递归版的汉诺塔
    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 省略了
    }

    根据上面的思想,把整个hanoi函数理解成一个栈帧,每一个栈帧有一个PC,还需要参数信息

可以理解成函数体每一条语句都是一条PC指针
每一次循环,都会取最顶上的栈帧来操作
这也就是下部分的逻辑。

1
2
3
4
5
6
7
8
switch (f->pc) {
case 0: if (n == 1) { printf("%c -> %c\n", from, to); goto(4); } break;
case 1: call(n - 1, from, via, to); break;
case 2: call( 1, from, to, via); break;
case 3: call(n - 1, via, to, from); break;
case 4: ret(); break;
default: assert(0);
}

操作系统上的软件 (应用程序)

任何程序 = minimal.S = 调用 syscall 的状态机

可执行文件是操作系统中的对象

  • 与大家日常使用的文件 (a.c, README.txt) 没有本质区别

  • 操作系统提供 API 打开、读取、改写 (都需要相应的权限)

查看可执行文件

  • vim, cat, xxd 都可以直接 “查看” 可执行文件
  • vim 中二进制的部分无法 “阅读”,但可以看到字符串常量
  • 使用 xxd 可以看到文件以 “\x7f” “ELF” 开头
  • Vscode 有 binary editor 插件

在 Vim 中输入 %!xxd 命令会将当前编辑的文件转换成十六进制表示,并在 Vim 中显示。这个命令的作用是将当前编辑的文件作为输入传递给 xxd 命令(%表示对整个文件执行操作),xxd 命令会将其转换成十六进制格式,并将结果输出到标准输出流,此时 Vim 会将其读取并显示在编辑器中。

动手实验:观察程序的执行

工具程序代表:编译器 (gcc)

  • 主要的系统调用:execve, read, write
  • strace -f gcc a.c (gcc 会启动其他进程, -f选项会追踪所有子进程)
    • 可以管道给编辑器 vim -
    • 编辑器里还可以 %!grep (细节/技巧)

grep 命令的 -e 选项用于指定一个或多个匹配模式,这些模式可以是正则表达式或普通字符串。

1
strace ls |& grep -e read -e write

图形界面程序代表:编辑器 (xedit)

  • 主要的系统调用:poll, recvmsg, writev
  • strace xedit
    • 图形界面程序和 X-Window 服务器按照 X11 协议通信
    • 虚拟机中的 xedit 将 X11 命令通过 ssh (X11 forwarding) 转发到 Host

应用视角的操作系统
http://example.com/2023/08/02/操作系统/jyy操作系统/应用视角的操作系统/
作者
LiuZhaocheng
发布于
2023年8月2日
许可协议