看视频看到了一个很有意思的问题:Why applications are operating-system specific?

观点看法

对于这个问题,我的能想到答案便是 syscalls 。尽管机器的架构可能是相同的(即应用程序被编译为同一指令集),但应用并非直接与硬件交互,它们之间存在了一个中间层——操作系统。应用在 user mode 下只能执行有限的计算指令,而为了使用 I/O 设备、内存、文件系统等软硬件资源,必须调用 OS 提供给用户的 API,即 system calls,将控制转移给 OS,CPU 切换为 kernel mode,执行写死的 syscall 内核代码并返回结果给应用。

不同操作系统提供的 syscalls 有很大的不同。以 Windows 和 Unix/Linux 的创建进程的系统调用为例:

  • 在 Win 的 API 下,进程调用CreateProcess()创建一个进程执行指定的可执行文件
  • Unix/Linux 下,进程调用fork()仅复制了进程本身,为了加载执行程序还需调用exec()

实现的不同导致 syscall 的行为不同,也就无法实现应用程序的跨平台了。所以同一段代码逻辑,在编程语言层面的编写就有很大不同了,编译为汇编代码那么就会有更大的不同!


在总结视频内容的基础之上,详细补充在 Operating System Concepts 章节笔记中缺失的一些概念并在此之上延申出一些思考:

  • 系统调用 system calls
    • Shell 的构建
    • x86 syscall指令:Hardware-based context switch v.s. Software-based context switch
  • ABI (Application Binary Interface)

1. 系统调用 System Call

上面说了我对 syscall 的思考,实际上,就算不同操作系统对同一系统调用的接口与实现相同,但底层实现的细节依旧会导致应用无法跨平台运行。

系统调用表 System Call Table

首先补充上下文切换的一些知识点:Hardware-based context switch v.s. Software-based context switch

抢占式调度依赖中断实现,其中时间片轮转(Round Robin)依赖时钟中断实现,CPU 响应中断会进行进程的上下文切换,保存当前进程的所有状态到 PCB 中,再从 PCB 加载新进程的状态

system call 也会使 CPU 从用户态转换为内核态,为了执行在操作系统内核代码中写定的系统调用例程,CPU 同样需要进行上下文的切换,但这个过程只保留一些必要的寄存器值,因为本质上还是在执行这个进程。至少需要保存的寄存器有:

  • Program Counter (PC) / %rip
  • Stack Pointer (%rsp) 切换为内核栈
  • 标志寄存器
  • syscall 传参所占用的寄存器

回到之前的问题,如果程序汇编代码中有如下的 syscall(x86 为例):

1
2
mov rax, 57  // set the system call number
syscall // transfer control
  1. 不同的操作系统可能读取不同的寄存器来获取系统调用序号,所以如果读取的是%rbx,那么直接系统就 crash 了。
  2. 就算是读取同一寄存器,但对于同一序号可能实现的是不同的 syscall。
    windows.png
    linux.png

2. 参数传递与 ABI

大部分系统调用都需要传递参数,那么将参数写到什么地方也会产生问题:OS1可能读取的是寄存器上的参数值,而OS2可能读取的是内存上的参数值。通过内存传递参数又分为通过栈或者一块特定的内存空间。这就是底层 ABI 之间的差异造成的不兼容。


Application Binary Interface (ABI)

类比 API 是我们如何在语言层面调用函数,ABI 则是在汇编/二进制码层面具体是如何调用的。

ABI = binary-level contract that defines how functions, data, and system calls are represented and interacted with at runtime.

除了前述的参数传递的规定,ABI 还包括:

  • Register usage: registers are caller-saved vs callee-saved
  • Binary format: Format of executable files (like ELF on Linux, PE on Windows)
  • System call interface: How system calls are made (via syscall, int 0x80, etc.)
  • Exception handling: How exceptions are represented and handled in binary

具体举 x86-64 System V ABI (Linux) 为例:

  • 前六个参数的值设置在: RDI, RSI, RDX, RCX, R8, R9
  • 返回值设置: RAX
  • call之前栈必须以 16 字节对齐
  • Caller-saved: RAX, RCX, RDX, RSI, RDI, R8–R11
  • Callee-saved: RBX, RBP, R12–R15

3. 可执行文件格式

不同操作系统的可执行文件格式不同,其中包含了描述该文件的 meta-data 和具体文件数据。

以 ELF 为例:elf.png
文件中的不同 section 具有不同的用途,.debug用于调试,.symtab用于链接,.data.bss用于初始化/未初始化数据的读写

4. 运行时环境 runtime

Java 运行在 JVM 上,Python 代码通过解释器解释执行,JS 也会被浏览器的 Javascript 引擎解释执行,通过虚拟化可以很好地避免上述问题。但现代程序都是模块化的,如果特定模块所需要的环境在另一个操作系统上并没有提供,那么整个程序都无法运行。