OS Structures¶
Abstract
- Operating System Services
- User and Operating System-Interface
- System Calls
- System Services
- Linkers and Loaders
- Why Applications are Operating System Specific
- Operating-System Design and Implementation
- Operating System Structure
- Building and Booting an Operating System
- Operating System Debugging
Operating System Services¶
A View of Operating System Services
红色部分是 helpful to user, 蓝色部分是 better efficiency/operation.
User Operating System Interface
- CLI
Comand Line Interface- shells
- GUI
User-friendly desktop metaphor interface
System Calls¶
Programming interface to the services provided by the OS 用户空间进入内核的接口 interface.
一般用高级语言编写,称为 API(Application Programming Interface) e.g. Win32 API, POSIX API(for Unix, Linux, Mac OSX), Java API(for JVM)
Example of System Call - write
¶
unist
: Unix Standard
printf
是 write
(system call) 的一个 wrapper。这里我们可以通过 man 2 write
查看系统调用的用法(2 表示查询系统调用)
fd=1
表示标准输出。buf
是要输出的内容,指向hello world\n
这个字符串首地址。count=13
是要输出的字节数(包含了\n
)
可以看到,write
的系统调用号为 1。
objdump -d
翻译成汇编可以看到,main
里会调用 __libc_write
。
在 __libc_write
里会将 1(系统调用号)挪到寄存器 %eax
里随后调用了 syscall
指令,跳到 kernel space,并且切换 mode。
在 kernel 空间,
- 调用
kernel_entry
来保护寄存器。 - 从 syscall_table 里拿到函数指针,跳到
write
的处理程序。- syscall_table 是一个 array,系统调用号作为 index,通过 index 取对应系统调用的函数指针。
- 写完后调用
ret_to_user
。- 恢复寄存器
- 返回到 user。
The caller need know nothing about how the system call is implemented.
用户不需要知道系统调用具体实现,OS 已经实现好了。
The Hidden Syscall Table
ARM64 架构的 sys.c
:
这里我们需要通过 C 预处理得到宏展开的文件:
make
.i
文件可以得到预处理后的文件。
write
其实 write
也是一个 wrapper,背后故事可见:
System Calls¶
strace
On Linux there is a “command” called strace
that gives details about which system calls were placed by a program during execution.
strace
: system call trace. e.g. strace cp main.c main
可以看到我们调用 cp main.c main
时,调用了哪些系统调用。
可以用 strace
知道我们的程序调用了什么系统调用之后出问题。
time
time
可以输出 real, user, sys 的时间。(real 表示时钟的时间)
有时 user 和 sys 是多进程同时进行,所以加起来的时间比时钟的时间长。
System Call Parameter Passing¶
传参,如果参数小,直接放在寄存器里;否则可以把地址放在寄存器里,通过地址访问。
Types of System Calls¶
- Process control
create process, terminate process; end, abort; - File management
- Device management
- Information maintenance
- Communications
- Protection
Linkers and Loaders¶
从 .c
文件到 .o
文件经过了下面的流程:
- 预处理
cpp main.c -o main.i
- 编译
gcc -S main.i -o main.s
- 汇编
as main.s -i main.o
怎么生成 main?如何把 main 加载到内存里执行?
Linker combines these into single binary executable file.
Loader loads executable file into memory and starts execution.
ELF binary basics¶
- Executable and Linkable Format - ELF
- Section header & Program header
一个给 Linker 用,一个给 Loader 用。 .text
: code.rodata
: initialized read-only data.data
: initialized data.bss
: uninitialized data
readelf
static variables 在 .data
段,static const 在 .rodata
段,const 在 .data
段。
存有每个段的起始地址,大小,权限等信息。
Linking¶
Linking 分为 static 和 dynamic
- Static linking: All needed code is packed in single binary, leading to large binary
- 把所有的代码都放到一个二进制文件,大。
- 没有
.interp
段
- Dynamic linking: Reuse libraries to reduce ELF file size
- 代码分散在多个文件,运行时再加载,小。
- 有
.interp
段,存的是 loader。
Running a binary¶
运行时的内存布局:
ELF section 被映射到内存里面的不同 segment。 注意区分堆和栈,分配数据时 stack 快,heap 慢。
- who setup ELF files mapping?
- Kernel
- exec syscall
- who setup stack and heap?
- Kernel
- exec syscall
- who setups libraries?
- Loader
- ld-xxx
- text: r-xp
- r--p: rodatas
- rw-p: data
- .bss: uninitialized variables. 给一个全局变量不给值,早期编译器记录它在
.bss
段里,但没有实际空间,映射到内存时就初始化为 0。 - heap/stack 匿名映射,没有一个文件支持。映射为可读可写。
Running a binary (Statically-linked)¶
首先我们通过 strace
查看运行静态链接文件过程中发生的系统调用。
execve
执行对应路径的文件brk
扩充栈write
执行程序里的printf
功能
系统如何知道我们要执行的程序从哪一行开始执行?
通过 entry point address!
sys_execve()
里有 load_elf_binary
函数,从 ELF 头读地址到 elf_entry
,把地址当作 regs->pc
。然后调用 start_thread
函数。
ELF 里有 entry point address(通过 readelf -h
)
entry point address 不是 main()
的地址,而是 _start
的地址,里面会调用 __libc_start_main
函数,里面才调用 main()
函数。
_start
是在读取命令行参数并且传给 main
。
cat /proc/pid/maps
里可以看到进程的内存映射。static 的条目更少,因为需要的东西已经打包到 a.static
内了,不需要外部的库。而 dynamic 需要外部的库。
Summary
Running a binary (Dynamically-linked)¶
相比于静态链接,动态链接:
- 需要一个 loader。
- 动态链接的内存布局中条目更多。
- 动态链接的系统调用比静态的多。
- 动态链接的 entry point 的地址很小,也是对应
_start
的地址,里面有些 symbol 还没有被解析。
类似地,我们先通过 strace
查看运行动态链接文件中使用了哪些系统调用。
可以看到相比于静态链接,多出来的系统都用都和 ld 有关,即 dynamic loader。
动态链接有 .interp
段,在 load_elf_binary
函数中会走另一个分支:entry point 会指向 loader 的地址。
Summary
多出来的系统调用是为了先把 loader 加载进来,loader 再把 libc 加载。
内存布局里多映射的条目也是为了给 loader 使用。
Why Applications are Operating System Specific¶
- 开发的软件不能直接跨平台调用,因为操作系统不同导致系统调用不同,下面的硬件也不同。
- 像 Java 开发的软件可以,因为有 JVM 提供了跨系统的平台。
- Application Binary Interface (ABI) is architecture equivalent of API, defines how different components of binary code can interface for a given operating system on a given architecture, CPU, etc... Application Binary Interface (ABI) 更贴近硬件架构
Operating-System Design and Implementation¶
策略 VS 机制:
- Policy: What will be done?
- Mechanism: How to do it?
The separation of policy from mechanism.
把策略和机制分开。
The Door Example
我们的一个策略:只允许上 OS 的同学进入曹西-201。对应的实现(机制)可以是:我们做一把锁,给 88 个同学都配一把。但这样的策略存在很多被 break 的可能。另一个机制:每个人凭学生卡/人脸识别进入。
Implementation¶
宏内核里放有很多 driver,而 driver 出问题,直接影响到 CPU 的 scheduler.
microkernel: Moves as much from the kernel into user space.
把 driver, file system... 都放到 user space,只留下最核心的东西在 kernel space.
- benefits:
- Easier to extend a microkernel
- Easier to port the operating system to new architectures
- More reliable (less code is running in kernel mode)
- More secure
- detriments:
- Performance overhead of user space to kernel space communication
Many modern operating systems implement loadable kernel modules (LKMs).
Operating-System Debugging
Kernighan’s Law: “Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.”