为什么应用程序时特定于操作系统的
看视频看到了一个很有意思的问题: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 | mov rax, 57 // set the system call number |
- 不同的操作系统可能读取不同的寄存器来获取系统调用序号,所以如果读取的是
%rbx
,那么直接系统就 crash 了。 - 就算是读取同一寄存器,但对于同一序号可能实现的是不同的 syscall。
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 为例:
文件中的不同 section 具有不同的用途,.debug
用于调试,.symtab
用于链接,.data
和.bss
用于初始化/未初始化数据的读写
4. 运行时环境 runtime
Java 运行在 JVM 上,Python 代码通过解释器解释执行,JS 也会被浏览器的 Javascript 引擎解释执行,通过虚拟化可以很好地避免上述问题。但现代程序都是模块化的,如果特定模块所需要的环境在另一个操作系统上并没有提供,那么整个程序都无法运行。