Compare commits

...

16 Commits

22 changed files with 2233 additions and 11 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
.vscode/
.vscode/
git-push-all.bat

View File

@@ -1,4 +1,4 @@
# Lecture 07 Procedures 函数的调用
# Lecture 07 Procedures 过程 \(函数的调用\)
## Mechanisms 机制 / 目录
@@ -8,7 +8,7 @@
- [Passing Data 传递数据](#passing-data-传递数据)
- [传入参数](#传入参数)
- [返回值](#返回值)
- [Managing Local Data 管理其他变量](#managing-local-data-管理其他变量)
- [Managing Local Data 管理其他变量与内存](#managing-local-data-管理其他变量与内存)
- [Stack Frame 栈帧](#stack-frame-栈帧)
- [Recursion 递归](#recursion-递归)
- [`ret` 指令](#ret-指令)
@@ -97,20 +97,29 @@
返回值一般使用```%rax```进行传递。
## Managing Local Data 管理其他变量
## Managing Local Data 管理其他变量与内存
### Stack Frame 栈帧
一般情况下,由于诸多与函数相关的特点,我们会在栈上分配一些内存给局部变量。加上其他要分配的内存[^extra-mmr-allocate],这部分被称作“栈帧”。
有时,我们会在栈上分配一些内存给局部变量。常见情况比如:
- 寄存器不够存放所有的局部变量
- 某些局部变量是个Array或者Struct
- 对一个局部变量取地址(&)。这个操作需要它在内存中有一个固定地址才能进行,因此会给它分配一个栈空间。
加上其他要分配的内存[^extra-mmr-allocate],这部分被称作“栈帧”。
| 函数特点 | 栈特点 |
| ---------- | ---------- |
| 返回后,函数内分配的局部变量被丢弃 | 更改```rsp```指针即可丢弃数据 |
| 函数内可能会调用其他函数 | 直接将返回地址压栈 |
| 单线程调用其他函数时,原函数停止继续运行 | 保存原函数 的帧指针,在其之上可直接建立新栈帧 |
| 原函数的数据保持不变 | 新分配一个栈即可避免数据被覆盖 |
[^extra-mmr-allocate]: 上一个函数的寄存器内容、Windows下的影子空间、返回地址等等内容。
需要注意的是在C语言中struct、union这种“庞大”的数据类型也会被留在栈上。如果处理不当很可能Stack Overflow。因此在处理大型数据结构时通常建议使用动态内存分配如malloc来避免栈空间的过度使用并使用free来手动释放。
### Recursion 递归
由于“栈帧”的特性,它很适合用来做递归。系统会为每一层递归单独分配内存空间,彼此之间不会因为相同的代码导致变量的地址也相同(使用```%rbp + 数```管理)。
@@ -152,17 +161,15 @@ A: 在编译时,编译器会根据函数预计内存的使用情况来生成
| ```%rax``` | Caller-Save | 返回值 |
| ```%rdi``` | Caller-Save | 传递参数 |
| ```%rsi``` | Caller-Save | 传递参数 |
| ```%rdx``` | Caller-Save | 传递参数 |
| ```%rcx``` | Caller-Save | 传递参数 |
| ```%rdx``` | Caller-Save | 传递参数 |
| ```%r8``` | Caller-Save | 传递参数 |
| ```%r9``` | Caller-Save | 传递参数 |
| ```%r10``` | Caller-Save | 临时寄存器 |
| ```%r11``` | Caller-Save | 临时寄存器 |
| | | |
| **```%rbx```** | Callee-Save | 被调用者保存寄存器 |
| ```%r12``` | Callee-Save | 被调用者保存寄存器 |
| ```%r13``` | Callee-Save | 被调用者保存寄存器 |
| ```%r14``` | Callee-Save | 被调用者保存寄存器 |
| ```%r12``` to ```%r15``` | Callee-Save | 被调用者保存寄存器 |
| ```%rbp``` | Callee-Save[^rbp-linux-reg-saving] | 特殊[^rbp-linux-reg-saving] |
| ```%rsp``` | Callee-Save[^rsp-linux-reg-saving] | 特殊[^rsp-linux-reg-saving] |

View File

@@ -188,7 +188,7 @@ get_short_int:
出于硬件原因[^Memory-Alignment-Hardware-Reason],结构体中的成员在内存中的位置需要按照特定的边界进行对齐。这意味着编译器可能会在成员之间插入填充字节,以确保每个成员都位于正确的内存地址上。
[^Memory-Alignment-Hardware-Reason]: 内存对齐是为了提高访问效率。现在的机器一次从内存中读取大约64字节。如果数据没有对齐,可能需操作系统或者硬件的额外处理,导致效率降低
[^Memory-Alignment-Hardware-Reason]: 无论数据是否对齐x86-64硬件都基本可以正常工作。但Intel依旧建议对其数据以获得更好的性能。如果数据对齐,CPU可能需要两次内存访问来获取数据导致性能下降
- 编译器会找到最大的成员的大小,并将结构体的总大小调整为该大小的倍数。内存对齐也会按照该大小进行。

View File

@@ -0,0 +1,52 @@
# 目录
- [CPU寄存器](#cpu寄存器)
- [x86-64 Linux System V ABI的寄存器用途](#x86-64-linux-system-v-abi)
- [Flags 标志位](#flags-标志位)
## CPU寄存器
### x86-64 **Linux** System V ABI
| 寄存器 | 用途 | 调用约定 | 32位 | 16位 | 8位 |
| ------ | ---- | -------- | ----- | ----- | --- |
| ```%rax``` | 返回值 | Caller-saved | ```%eax``` | ```%ax``` | ```%al``` |
| ```%rbx``` | 基址寄存器 | Callee-saved | ```%ebx``` | ```%bx``` | ```%bl``` |
| ```%rcx``` | 传递参数 | Caller-saved | ```%ecx``` | ```%cx``` | ```%cl``` |
| ```%rdx``` | 传递参数 | Caller-saved | ```%edx``` | ```%dx``` | ```%dl``` |
| ```%rsi``` | 传递参数 | Caller-saved | ```%esi``` | ```%si``` | ```%sil``` |
| ```%rdi``` | 传递参数 | Caller-saved | ```%edi``` | ```%di``` | ```%dil``` |
| ```%rsp``` | 栈指针 | Callee-saved | ```%esp``` | ```%sp``` | ```%spl``` |
| ```%rbp``` | 帧指针 | Callee-saved | ```%ebp``` | ```%bp``` | ```%bpl``` |
| ```%r8``` | 传递参数 | Caller-saved | ```%r8d``` | ```%r8w``` | ```%r8b``` |
| ```%r9``` | 传递参数 | Caller-saved | ```%r9d``` | ```%r9w``` | ```%r9b``` |
| ```%r10``` | 临时寄存器 | Caller-saved | ```%r10d``` | ```%r10w``` | ```%r10b``` |
| ```%r11``` | 临时寄存器 | Caller-saved | ```%r11d``` | ```%r11w``` | ```%r11b``` |
| ```%r12``` - ```%r15``` | 被调用者保存寄存器 | Callee-saved | ```%r12d``` - ```%r15d``` | ```%r12w``` - ```%r15w``` | ```%r12b``` - ```%r15b``` |
| | | |
| **```%rip```** | 指令指针 | | ```%eip``` | ```%ip``` | |
| **```%rflags```** | 标志寄存器 | | ```%eflags``` | ```%flags``` | |
### Flags 标志位
| VS显示名称 | 标志位 | 全称 | 为1时表明... | 用户态可用 |
| ---------- | ------ | ---- | ---- | ---------- |
| OV | OF | **O**ver**f**low Flag | 有符号数运算结果溢出 | √ |
| UP | DF | **D**irection **F**lag | 字符串指令的处理方向递减 | √ |
| EI | IF | **I**nterrupt **F**lag | 允许外部中断 | |
| PL | SF | **S**ign **F**lag | 结果为负数 | √ |
| ZR | ZF | **Z**ero **F**lag | 结果为零 | √ |
| AC | AF | **A**uxiliary carry **F**lag | (辅助进位) | √ |
| PE | PF | **P**arity **F**lag | 结果的二进制表示中1的个数为偶数 | √ |
| CY | CF | **C**arry **F**lag | 无符号数运算结果溢出 | √ |
## GDB调试常用命令
### 程序运行控制
| 命令 | 别名 |作用 | 备注 |
| ---- | ---- | ---- | ---- |
| ```run``` | ```r``` | 运行程序 | 可以带参数,如```run arg1 arg2``` |
| ```continue``` | ```c``` | 继续执行程序 | 在断点处暂停后使用 |
| ```stepi``` | ```s``` | 单步执行 | 进入函数调用 |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
Border relations with Canada have never been better.
1 2 4 8 16 32 64
0 207
7 0
)/.%&'
4 3 2 1 6 5

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Overview
这个仓库是学习CS的笔记。各个文件夹分别对应不同的课程里面是我在学习过程中总结的笔记.
This repository contains notes on various CS courses. Each folder corresponds to a different course, and inside are the notes I have compiled during my learning process.

View File

@@ -1,4 +1,4 @@
# L01 Overview 概述
# Overview 概述
- [What is an OS? 什么是操作系统?](#what-is-an-os)
- [Process Abstraction 进程抽象](#process-abstraction-进程抽象)

View File

@@ -0,0 +1,187 @@
# Four Fundamental OS Concepts
## Complexity 复杂性
操作系统的硬件非常复杂。如果没有操作系统,用户必须直接与硬件交互,这将非常困难。操作系统通过提供抽象来隐藏硬件的复杂性,使用户能够更容易地使用计算机。
这也就意味着,如果处理不当:
- 复杂性会“泄露”出来。例如一些第三方驱动程序导致系统崩溃,或者某些功能无法正常工作。
- 安全漏洞。例如2017年Intel的CPU存在一个名为Meltdown的漏洞。攻击者甚至可以访问kernel模式的内存
- 不同版本的库会导致应用程序可能出问题。(现在我们有了Docker)
一般来说,操作系统进行的"抽象"包括:
- Processor -> Thread
- Memory -> Address Space
- Disks, SSDs, ... -> Files
- Networks -> Sockets
- Machines -> Processes
## Thread of Control
Thread 是一个独一无二的context, 其包括程序计数器、寄存器、堆栈、CPU标志位、内存等等。
### Resident / Running 驻留(运行态)
当一个线程处于Resident状态时表示该线程正在被CPU执行。
Resident指: 寄存器当前保存了该线程的state (content)。
- 包括程序计数器(PC, program counter)、当前执行的指令的地址
- PC指向内存中下一条指令的地址
- 包括程序当前正在计算的数据
- 栈指针
- 剩下的、内存里的数据
### Suspended / Ready 挂起(就绪态)
当一个线程处于Suspended状态时表示该线程已经准备好运行但由于某些原因如等待资源、等待I/O操作完成等而暂时未执行。
与Resident状态具有很多相反的状态
- CPU的state不再是该线程的content而是另一个线程的content
- PC指向另一个线程的指令地址
- 大多数情况下线程的content被保存在内存中而不是寄存器中
### Content Switch 上下文切换
#### 使用场景
现代的操作系统通常支持多线程。电脑上可以同时运行很多线程但单个CPU核心一次只能执行一个线程。当操作系统需要切换正在运行的线程时就会发生上下文切换。
以图中为例:
![CPU上下文切换](/UCB-CS162/images/L02-content-switch.png)
- T1时刻vCPU1在CPU核心上vCPU2在内存中
- T2时刻vCPU1在内存中vCPU2在CPU核心上
在T1和T2之间操作系统进行了一次**context switch**,也就是上下文切换:
- 将vCPU1的状态保存到**Thread Control Block** (TCB, 位于内存) 中。
- 将vCPU2的状态从内存加载到CPU核心上
- 其他操作.....
上下文切换不是毫无开销的。一般来讲,它会耗费几微秒的时间。因此,操作系统会尽量减少上下文切换的次数,并使用各种手段减少上下文切换的开销,以提高系统性能。
有多种条件可以出发一次Content Switch: 计时器、I/O事件、系统调用、线程优先级变化等等。
#### Thread Control Block (TCB)
TCB是一些数据保存了线程的状态信息包括寄存器值、堆栈指针、程序计数器、PC等。
通常来讲TCB被存储在内存的kernel空间中。
#### Address Space 地址空间
![简化的地址空间](/UCB-CS162/images/L02-Simplified-Address-Space.png)
## Reliability 可靠性
简单的上下文切换不提供操作系统保护而我们显然不会希望一个user program崩掉整个系统因此操作系统需要提供一些保护机制。
- 可靠性:破坏操作系统通常会导致其崩溃
- 安全性:限制线程的操作范围
- 隐私性:限制每个线程仅能访问其被允许访问的数据
- 公平性每个线程应限制在其应得的系统资源份额内CPU时间、内存、I/O等
仅靠软件是不足以更好的保护系统的,因此我们有了一些硬件层面的保护机制:
### Base and Bound (B&B) 基址寄存器、边界寄存器
![Base & Bound image](/UCB-CS162/images/L02-Base-and-Bound.png)
如图所示,程序地址“看起来”位于 0 ~ 100 之间;但,当它被加载到内存内时,它被重新定位到 1000 ~ 1100 之间。
这是一种基于编译器、加载器的保护机制。它把操作系统和用户程序隔离,保护操作系统。
我们也可以通过添加一个加法器来实现更高效的B&B机制
![B&B Hardware Assistant 1](/UCB-CS162/images/L02-B&B-Hardware-Assistant-1.png)
这是一种硬件辅助的内存重定向。
B&B机制有很多缺点。最显著的缺点之一是它无法简单地处理用户程序allocate或者free内存的情况。由此我们有了一个更复杂的机制:
### Address Space Translation 地址空间转换
我们可以通过增添一个特定的“小盒子”,来实现更高效的地址空间转换:
![Address Space Translation](/UCB-CS162/images/L02-Address-Space-Translation-Overview.png)
这个“小盒子”被称为**Memory Management Unit** (MMU),它可以将虚拟地址转换为物理地址。
这个解决方案很好地解决了B&B机制的缺点。用户程序可以allocate或者free内存而不需要担心地址空间的重叠问题。其中有一个至今仍然在使用的机制: **Paging** (分页机制)。
### Paging 分页机制
分页机制将内存划分为固定大小的页通常是4KB。每个页都有一个对应的页表项记录了该页在物理内存中的位置。当程序访问一个虚拟地址时MMU会查找对应的页表项将虚拟地址转换为物理地址。
![Paged Memory](/UCB-CS162/images/L02-Paged-Memory.png)
我们还可以实现一点更“酷”的东西。为了节约内存我们甚至可以让一些Page实际上不在内存中对这些page的访问会触发一个**page fault**程序中断后操作系统将所需的page从磁盘加载到内存中然后继续执行程序。
## Processes 进程
进程是具有受限权限的执行环境。
- (受保护的)具有一个或多个线程的地址空间
- 拥有内存(地址空间)
- 拥有文件描述符、文件系统上下文...
- 封装一个或多个共享进程资源的线程
复杂的应用程序可以 fork/exec创建/执行)子进程。
某种意义上来说,进程是一些“城市”,而操作系统是管理他们的“政府”。
- 每个城市都有自己的居民(线程),有自己的资源(内存、文件描述符等)。程序假设它们是独立的,不会相互干扰。
- 政府负责管理这些城市确保城市A出问题不会波及到城市B、审查城市AB之间的通信、分配硬件资源给城市, etc.
### 为什么需要进程?
- 它让不同程序、组件之间相互隔离,不会一个崩溃连带其他崩溃。
- 操作系统免受它们的影响
- 提供内存保护
### 保护与效率之间的基本权衡
- 进程**内**通信更容易。
- 线程之间可以直接访问共享内存
- 进程**间**通信更难
- 需要使用IPC机制如管道、消息队列、共享内存等来进行通信
### 单线程与多线程
![Single Thread vs Multi Thread](/UCB-CS162/images/L02-Single-Thread-vs-Multi-Thread.png)
可以从途中看到,各个线程**共享资源**(同一个地址空间、文件描述符等),但每个线程都有**自己的程序计数器、寄存器、堆栈**等。
## privilege levels 权限级别
上边我们提到了页表。那如果应用A想要把页的映射地址改到一个它不应该访问的地址上怎么办我们有了权限级别来解决这个问题。
### Dual Mode Operation 双模式操作
CPU至少会提供以下两个模式:
- User mode (Ring 3) 用户模式:应用程序运行在这个模式下,权限受限,无法直接访问硬件资源。
- Kernel mode (Ring 0) 内核模式:操作系统运行在这个模式下,拥有完全的权限。
由此我们解决了问题当操作系统让其他应用程序运行时它会将CPU切换到User mode。这样子应用A就不能直接修改页表了。
如果应用程序需要从硬盘读取数据它会发出一个系统调用system call请求操作系统执行这个操作。操作系统会将CPU切换到Kernel mode执行相应的操作然后再切换回User mode。
由此我们得出了经典的UNIX系统架构:
![Simplified Unix System Architecture](/UCB-CS162/images/L02-Simplified-Unix-System-Architecture.png)
### How to switch? 如何切换当前模式?
从用户态到内核态:
- ```System call``` 系统调用
- 定义: 进程请求系统服务例如exit
- 系统调用类似于函数调用
- 没有要调用的系统函数的地址
- 类似于远程过程调用 (RPC)
- 将系统调用 ID 和参数整理到寄存器中,并执行系统调用
- ```Interrupt``` 中断
- 某些原因触发上下文切换
- 例如Timer 、I/O 设备
- ```Trap``` 陷阱或```Exception``` 异常
- 进程内部同步事件触发上下文切换
- 例如:```Protection Violation - Segmentation Fault```(段错误)、除以零, etc.
在一次,例如```Interrupt```发生后CPU会把当前正在运行的程序保存在```Thread Control Block (TCB)```中。TCB中保存了程序计数器、寄存器值、堆栈指针等并存储在Kernal memory中。

View File

@@ -0,0 +1,127 @@
# Processes and Threads
## Motivation for threads 为什么我们需要线程
- 操作系统必须支持多个任务同时运行。例如,进程、终端或系统维护。
- 操作系统必须支持多个连接同时运行。例如多用户、Web连接等。
- 进程必须能够同时执行多个任务。例如,一个游戏必须同时有图形、声音与运算逻辑等。
- 操作系统必须同时访问多个硬件,或者至少看起来像是同时访问。你不会希望动鼠标、按键盘时,系统不能访问网络或磁盘。
一些定义:
- Multi-Processing多个 CPU核心
- Multi-Programming多个作业/进程
- Multi-Threading多个线程/进程
### Concurrency 并发
**并发**运行两个线程意味着什么?
- 调度器可以自由地,以任意顺序和交错方式,运行线程
- 线程可能会运行至完成,或者以大块或小块的时间片运行
![MultiProcessing vs MultiProgramming](images/L03-MultiProcessing-vs-MultiProgramming.png)
### Computes that never finish 永远无法完成的计算
想象如下代码其中ComputePi()会尝试计算π的最后一位并将计算过程写入文件pi.txt。
```C
main() {
ComputePI("pi.txt");
PrintClassList("classlist.txt");
}
```
这个例子中显然ComputePi是个不可能完成的函数。因此我们完全没有办法print ClassList程序也无法停止除非你Ctrl-C或者关闭电脑。
但是使用线程,我们可以这么做:
```C
main() {
create_thread(ComputePI, "pi.txt");
create_thread(PrintClassList, "classlist.txt");
}
```
这样即便ComputePI永远无法完成我们也可以得到ClassList。
### IO / Compute Overlap
这是一个各种操作的时间表:
| Operation | Time |
| :--- | :--- |
| L1 cache reference | 0.5 ns |
| Branch mis-predict | 5 ns |
| L2 cache reference | 7 ns |
| Mutex lock/unlock | 25 ns |
| Main memory reference | 100 ns |
| Compress 1K bytes with Zippy | 3,000 ns |
| Send 2K bytes over 1 Gbps network | 20,000 ns |
| Read 1 MB sequentially from DDR2 memory | 250,000 ns |
| Round trip within same datacenter | 500,000 ns |
| Disk seek on HDD | 10,000,000 ns |
| Read 1 MB sequentially from HDD disk | 20,000,000 ns |
| Send packet CA->Netherlands->CA | 150,000,000 ns |
Disk Seek硬盘寻址等操作显然需要很久比如10ms。我们显然不希望CPU的一个核心等待这么久不做任何计算。
## Introduction to threads 线程简介
线程有三个状态。如下:
- Running 运行态: 线程正在使用CPU的时间片
- Ready 就绪态: 线程准备好运行了但还没有上CPU
- Blocked 阻塞态: 线程不能继续运行
我们可以基于此设计调度器。例如如果两个线程都不需要IO它们很可能会轮流使用同一个CPU核心:
![Thread Scheduling Without IO](images/L03-Thread-Scheduling-Without-IO.png)
但是如果其中一个线程需要执行IO操作那么它就会被阻塞另一个线程就可以继续使用CPU核心
![Thread Scheduling With IO](images/L03-Thread-Scheduling-With-IO.png)
注: Blocked状态不会直接转为Running状态必须先转为Ready状态。
### How to manage threads 如何管理线程
一个程序默认就含有一个线程。如果想要创建其他线程我们必须使用一次System Call。
在C语言中使用```pthread_create()```函数来创建线程。
```C
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);
```
- 创建线程,并以 `arg` 为唯一参数执行 `start_routine`。
- `return` 相当于隐式调用 `pthread_exit`。
```C
void pthread_exit(void *value_ptr);
```
- 终止线程,并使 `value_ptr` 对任何成功的 `join` 操作可用
```C
int pthread_join(pthread_t thread, void **value_ptr);
```
- 挂起调用线程的执行,直到目标线程终止。
- 返回时,若 `value_ptr` 非 `NULL`,则终止线程传递给 `pthread_exit()` 的值将存储在 `value_ptr` 指向的位置。
#### `void *(*start_routine)(void*)`是什么玩意?
我们从内向外看这个东西。
`*start_routine` 是一个函数指针,指向一个函数,这个函数接受一个 `void*` 类型的参数,并返回一个 `void*` 类型的值。
## System Calls
System Call 接口隔开了用户程序与操作系统内核。
这使得系统设计看起来像一个沙漏而System Call正式沙漏中间最窄的地方
如图所示:
![System is the interface between user and kernel](images/L03-System-Call-In-OS.png)
你会注意到System Call之上用户程序之下还有一层Portable OS Library。这个库允许程序员使用同一套API来编写程序。
- C程序猿们会使用`libc`,提供很多以某种方式包装的系统调用。

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB