第六周阅读材料


345,1314
进程=正在执行的程序=程序似乎独占cpu -> cpu虚拟化,让每个进程拥有一个cpu
如何实现cpu的虚拟化:策略(调度策略)+机制(上下文切换)
正在执行的程序需要用到机器的哪些部分:(地址空间+寄存器=保存状态) & (cpu)(执行指令状态流转)
如何将存在于磁盘的程序变成运行中的程序(进程)?
1.静态数据->进程地址空间
2.给进程地址空间分配 stack, heap 等
3.初始化main 函数参数
4.pc指针指向main函数
进程状态:

  1. 运行
  2. 就绪
  3. 阻塞
    一些api示例:
    fork、exec、wait、kill
    一个有趣的tips:open文件默认占用最小的文件描述符,close stdout(1),然后open a文件,则文件描述符1指向a文件。
    然后介绍了下地址空间的抽象。及操作内存的一些方法。注意系统调用brk与sbrk,mmap

    0xB操作系统上的进程


    最小的操作系统


    操作系统在启动后(完全接管了硬件后)做的第一件事情是什么?
    thread-os:CPU Reset → Firmware → Boot loader → Kernel _start()
    操作系统会加载 “第一个程序”(将所有一切交给它,然后剩下的所有进程等都是它创建的;linux=systemd)

    • RTFSC(latest Linux Kernel)
      • 如果没有指定启动选项 init=,按照 “默认列表” 尝试一遍
      • 从此以后,Linux Kernel 就进入后台,成为 “中断/异常处理程序”
        程序:状态机
    • C 代码视角:语句
    • 汇编/机器代码视角:指令
    • 与操作系统交互的方式:syscall
      To-do:玩一玩 linux_minimal
      如何理解这个最小的linux?启动后的状态?
      内核是一系列的可执行文件。
      qemu-system-x86_64 \
       -nographic \
       -serial mon:stdio \
       -m 128 \
       -kernel vmlinuz \
       -initrd build/initramfs.cpio.gz \
       -append "console=ttyS0 quiet acpi=off"
      


      示例这里的qemu启动流程:

  4. 内核加载
    • QEMU 加载 vmlinuz 内核文件到内存,并开始执行。
  5. initramfs 加载
    • 内核将 initramfs.cpio.gz 解压到内存中的临时文件系统。
  6. 执行 init 脚本
    • 内核在 initramfs 中查找并执行 /init 脚本(或 /linuxrc)。
    • 该脚本负责加载必要的驱动程序、检测硬件设备,并挂载根文件系统。
  7. 挂载根文件系统
    • initramfs 中的脚本会挂载真正的根文件系统(如 /dev/sda1)。
  8. 切换到根文件系统
    • 挂载成功后,initramfs 会将控制权交给根文件系统中的 initsystemd,完成系统启动。
  9. 清理 initramfs
    • 一旦切换到根文件系统,initramfs 占用的内存会被释放。
      其中内核加载:将内核vmlinuz(引导代码+压缩的内核映像+解压代码)加载进内存,然后进入内核引导阶段
      1.执行引导代码:
      - 初始化cpu(设置运行模式)初始化基本硬件(中断控制、定时器) - 设置内存布局 - 执行解压代码解压内核
      2.执行内核初始化代码(解压后开始执行)
      - 初始化内核子系统(内存管理,设备管理,进程管理…) - 解析启动参数 - 初始化硬件设备 - 加载 initramfs(解压到临时文件系统tmpfs) - 执行init脚本(一般目的:脚本负责加载驱动程序、检测硬件设备、挂载根文件系统。)
      (init脚本的初始化硬件与引导阶段的初始化硬件范围有所不同,引导阶段的初始化可以理解为:确保内核能够运行并加载 initramfs。)
      操作系统启动后就调用init程序,然后init进程再通过systemcall 创造整个世界(进程、内存、文件系统(网络…))。
      image-20250219192120180
      CPU Reset → Firmware → Loader → Kernel _start() → 执行第一个程序 /bin/init → 中断/异常处理程序
      操作系统为所有应用程序提供API:
  10. 进程管理(状态机)
  11. 存储管理(地址空间)
  12. 文件管理(数据对象)

    fork


    语义:复制一个完全一样的状态机
    fork boom::(){:|:&};:
    funny thing
    ```c #include

void mian() { printf(“hello”); int* p; p = NULL; *p = 1; }

<br>
输出没有hello
<br>
```c
#include <stdio.h>

void mian() {
  printf("hello 
"); int* p; p = NULL; *p = 1; }


输出有hello

#include <stdio.h>

void mian() {
  printf("hello"); fflush(stdout);
  int* p;
  p = NULL;
  *p = 1;
}


输出有hello
结论:
stdout:
指向终端:line buffer
指向pipe/file:full buffer

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
  int n = 2;
  for (int i = 0; i < n; i++) {
    fork();
    printf("Hello
"); } for (int i = 0; i < n; i++) { wait(NULL); } }


命令行:6个hello
管道:8个hello
image-20250219203544399

execve


语义:重置状态机,将状态机重置为某个程序的初始状态。

#include <unistd.h>
#include <stdio.h>

int main() {
  char * const argv[] = {
    "/bin/bash", "-c", "env", NULL,
  };
  char * const envp[] = {
    "HELLO=WORLD", NULL,
  };
  execve(argv[0], argv, envp);
  printf("Hello, World!
"); }


Printf不会被执行!
main函数实际有一个环境参数。

exit


语义:销毁状态机。
执行 exit调用的状态机,从系统消失
image-20250219210636697
atexit 会清空缓冲区
_exit(0) / syscall(SYS_exit, 0) 直接结束,不管缓冲区

0XC进程地址空间


进程的地址空间是什么?


c语言的状态机:全局heap + 共享变量 + stack frame
汇编语言状态机:没有stack frame 链,没有 堆与栈了:平坦的地址空间 + 寄存器
pmap:查看进程地址空间,被分成了多个段
段内的地址空间都可以合法访问,不在段内或者违反权限的访问= SIGSEGV
Proc/pid/maps
Readelf -l
二者对比?找出对应的,为什么有些不太对应?
vvar
vdso:实现不进入的内核的系统调用
动态连接,刚开始地址空间没有libc,运行到main时就有了libc = 进程的地址空间可以变化
答:
进程的地址空间就是内存里若干连续的段
每一段都可以访问(读、写、执行),可能是映射到某个文件,也可能是在进程间共享
to-do:RTFM- pmap,proc

如何管理进程的地址空间


  • mmap & munmap
    mmap:映射到地址空间
    munmap:将地址空间的一块区域拿开
    mprotect:修改权限
    mmap的实际机制,引入了新的问题memory-mapped file一致性问题:msync

    进程地址空间隔离


    进程空间的概念,实现了进程的隔离。每个*ptr只能访问本进程(本状态机)的内存
    proc下的mem 文件代表了进程的内存,可以当作文件进行访问。
    修改金钱 - 修改进程内存中指定位置的值
    增强按键 - 事件监听,然后处理
    变速齿轮 - 为进程单独维护一个时钟系统调用
    外挂:针对游戏的gdb调试器
    可以修改内存,也就可以修改代码:https://jyywiki.cn/pages/OS/2022/demos/dsu.c
    修改页权限,再将跳转的代码植入

    0xD系统调用与shell


    shell:用户角度的操作系统
    Kernel 提供系统调用,sehll 提供用户接口(对kernel的包装)
    操作系统,加载了init后就是 系统调用的执行者,中断的管理者
    todo :

  • 之前好像用java实现过shell and csapp中似乎也实现过?需要复习?
  • man sh

    A Zero-dependency UNIX Shell (from xv6)


    Todo 阅读 https://jyywiki.cn/pages/OS/2022/demos/sh-xv6.c
    管道的实现?
    job控制 ctrl+z 后台 bg fg jobs
    shell总结:

    终端


    为什么有时候ctrl + c 可以退出,有些又不能退出?
    终端是用户与系统交互的界面,shell是解释程序。打开终端,运行shell。
    RTFM:tty,stty
    终端是 UNIX 操作系统中一类非常特别的设备!https://tmuxcheatsheet.com/
    img
    Tmux 就是多个终端
    可以echo重定向到终端,也可以vi终端

    echo hello > /dev/ttys001
    


    strace 的结果重定向到log,然后新的窗口查看,然后执行tmux,并操作
    image-20250223194742509
    todo:csapp 中信号处理部分的实验当时有一个bug来着。
    一个shell 一个session。
    进程fork的,都是一个进程组。

    1. 启动进程,此时进程连接终端a
    2. fork 进程,此时两个进程都连接终端a
    3. 终端输入,两个进程会争抢
    4. signal 则会同时发送给进程组的所有进程

todo:RTFM: setpgid/getpgid(2)
login shell 的概念
终端是与前台shell绑定的

0xE libc


shell 在kernel的syscall基础上包了一层,提供了kernel与用户交流的基础。
Q:如何在系统调用之上构建程序能够普遍受惠的标准库?
libc
todo:freestanding 下可以引用的头文件,数据结构
https://jyywiki.cn/OS/2022/slides/14.slides.html#/1/2
libc:

  1. 高情商系统调用api
  2. 纯粹的计算(string,排序,查找)https://jyywiki.cn/OS/2022/slides/14.slides.html#/2/3 printf的buff?
  3. 封装文件描述符
  4. 更多的进程/操作 封装
  5. 地址空间封装 fast path & slow path
    todo:跟着课程玩一玩…
    todo:书籍:thinking, fast and slow
    image-20250225205930459

    0xF fork


    期中检测?
    https://jyywiki.cn/OS/2022/slides/15.slides.html#/1/2
    NP complete?

    Fork-文件描述符


    fork = 状态机复制
    exrc = 状态机重置
    1.
    但fork是复制的文件描述符 在exec时不会被重置:父进程持有指向a文件的fd=1,子进程也会持有指向a文件的fd=1,exec后还是有。– shell中的管道实现
    open 提供了更个性化的选项。man open
    2.

    write(3,"a",1);
    write(3,"b",1);
    


    一个进程,写入ab 而不是b覆盖a,因为文件描述符有偏移量。
    但如果引入fork,此时父子偏移量一直,然后写ab呢?

    fd = open("a.txt", O_WRONLY | O_CREAT); assert(fd > 0);
    pid_t pid = fork(); assert(pid >= 0);
    if (pid == 0) {
      write(fd, "Hello");
    } else {
      write(fd, "World");
    }
    


    • 原子性 (RTFM: write(2), BUGS section)
    • dup的offset 是共享还是独占?- 共享一个offset

      Fork-Copy-on-write


      所有的页面都是操作系统持有的,进程用的其实是对页面的引用
      页的引用计数+写时复制
      image-20250226205937640

      Fork - 平行宇宙


      fork 可以实现无回溯的DFS
      https://jyywiki.cn/pages/OS/2022/demos/dfs-fork.c
      image-20250226213738642
      安卓zygote
      fork 七宗罪:
      https://www.microsoft.com/en-us/research/uploads/prod/2019/04/fork-hotos19.pdf

      0x10可执行文件


      可执行文件是什么


      本次课涉及的手册

    • System V ABI: System V Application Binary Interface (AMD64 Architecture Processor Supplement) (repo)
    • 和更多 refspecs
      more and more 欠账😂 fucking manual
      在简化的概念模型基础上,才能通过手册去填充。知道&理解 ? 为什么要了解到某个层次?
      可执行文件是什么? 状态机的描述
      操作系统 “为程序 (状态机) 提供执行环境”
    • 可执行文件 (状态机的描述) 是最重要的操作系统对象!
      fork 复制状态机,execve重置状态,将进程状态机修改为可执行文件描述的状态
      要描述进程的状态需要那些内容:
      image-20250303203914536
      1.寄存器(包含pc)
      ​ 大部分由ABI规定,操作系统初始化(例如 starti 的pc)
      2.进程的内存布局(stack,heap,静态变量等)
      ​ 二进制文件+ABI共同决定(例如 argv,envp)
      Gdb starti
      0x0000000000401620 in _start ()
      (gdb) info registers 
      rax            0x0                 0
      rbx            0x0                 0
      rcx            0x0                 0
      rdx            0x0                 0
      rsi            0x0                 0
      rdi            0x0                 0
      rbp            0x0                 0x0
      rsp            0x7fffffffe4c0      0x7fffffffe4c0
      r8             0x0                 0
      r9             0x0                 0
      r10            0x0                 0
      r11            0x0                 0
      r12            0x0                 0
      r13            0x0                 0
      r14            0x0                 0
      r15            0x0                 0
      rip            0x401620            0x401620 <_start>
      eflags         0x200               [ IF ]
      cs             0x33                51
      ss             0x2b                43
      ds             0x0                 0
      es             0x0                 0
      fs             0x0                 0
      gs             0x0                 0
      k0             0x0                 0
      k1             0x0                 0
      k2             0x0                 0
      k3             0x0                 0
      k4             0x0                 0
      


      为什么rip指向0x401620 ?为什么rsp=0x7fffffffe4c0?
      程序的进程空间布局:

      281135:   /learn/os/no9/a.out
      0000000000400000      4K r---- a.out
      0000000000401000    604K r-x-- a.out
      0000000000498000    164K r---- a.out
      00000000004c1000     28K rw--- a.out
      00000000004c8000     20K rw---   [ anon ]
      00007ffff7ff9000     16K r----   [ anon ]
      00007ffff7ffd000      8K r-x--   [ anon ]
      00007ffffffde000    132K rw---   [ stack ]
      ffffffffff600000      4K --x--   [ anon ]
       total              980K
      (gdb) 
      


      rip指向了第二部分,可读可执行的部分,rsp这是操作系统给分配的,其他的寄存器初始状态则是ABI规定的
      可执行文件是一个数据结构,描述了状态机的初始状态,迁移(指令,冯诺依曼结构下也是存储在内存)
      一个描述了状态机的初始状态 + 迁移的数据结构
      1.elf主要用于描述初始内存结构
      2.还包括了其他有用的信息 (例如便于调试和 core dump 的信息)
      操作系统要能正常执行的一个文件需要满足哪些条件?
      1.没有权限 chmod -x

      -rwxr-xr-x 1 root root 900216 Mar  3 20:44 a.out
      -rw-r--r-- 1 root root     21 Mar  3 20:44 demo.c
      root@cd2c2g:/learn/os/no9# chmod -x a.out
      root@cd2c2g:/learn/os/no9# strace ./a.out 
      execve("./a.out", ["./a.out"], 0x7ffd56cd48c0 /* 25 vars */) = -1 EACCES (Permission denied)
      strace: exec: Permission denied
      


      2.非可执行文件

      -rwxr-xr-x 1 root root 900216 Mar  3 20:44 a.out
      -rwxr-xr-x 1 root root     21 Mar  3 20:44 demo.c
      root@cd2c2g:/learn/os/no9# strace ./a.c
      strace: Can't stat './a.c': No such file or directory
      root@cd2c2g:/learn/os/no9# strace ./demo.c
      execve("./demo.c", ["./demo.c"], 0x7ffc65d74290 /* 25 vars */) = -1 ENOEXEC (Exec format error)
      strace: exec: Exec format error
      +++ exited with 1 +++
      


      常见操作系统可执行文件:

    • 如果你还爱我.avi
    • Windows 下的PE(Portable Executable)
    • linux/unix下的:a.out /ELF (Executable Linkable Format)/She-bang(#!)
      ```c #! /usr/bin/python3

print(“hello”)

<br>
此时a.c也变得可执行了。she-bang 就是一个变种的execve
<br>
a.c 还可以设置为我们自己的
<br>
```c
#! ./a.out hello world


root@cd2c2g:/learn/os/no9# ./a.c 1 2 3 4
argv[0] = ./a.out
argv[1] = hello world  #she-bang 脚本不管空格只有一个参数
argv[2] = ./a.c
argv[3] = 1
argv[4] = 2
argv[5] = 3
argv[6] = 4


#! 将argv替换处理后,再执行execve
是谁决定了一个文件能不能执行?

操作系统代码 (execve) 决定的。
动手试一试

  • strace ./a.c
  • 你可以看到失败的 execve!
    • 没有执行权限的 a.c: execve = -1, EACCESS
    • 有执行权限的 a.c: execve = -1, ENOEXEC
  • 再读一遍 execve (2) 的手册
    • 读手册的方法:先理解主干行为、再查漏补缺
    • “ERRORS” 规定了什么时候不能执行
      常见的可执行文件:
      就是操作系统里的一个普通对象
  • 绿导师原谅你了.avi
  • Windows 95/NT+, UEFI
    • PE (Portable Executable), since Windows 95/NT+
  • UNIX/Linux
    • a.out (deprecated)
    • ELF (Executable Linkable Format)
    • She-bang
      • 我们可以试着 She-bang 一个自己的可执行文件!
      • She-bang 其实是一个 “偷换参数” 的 execve

        解析可执行文件


        GNU binutils binary utilities

  • 生成可执行文件
    • ld (linker), as (assembler)
    • ar, ranlib
  • 分析可执行文件
    • objcopy/objdump/readelf (计算机系统基础)
    • addr2line, size, nm
      我们执行的是二进制文件,为什么gdb可以调试,可以知道出错对应源文件的那一行,即源文件-二进制文件 的映射是如何实现的?
      add2line
      编译器新增了调试信息
      将一个 assembly (机器) 状态映射到 “C 世界” 状态的函数
  • The DWARF Debugging Standard
    • 定义了一个 Turing Complete 的指令集 DW_OP_XXX
    • 可以执行 “任意计算” 将当前机器状态映射回 C
    • RTFM
      接下来是一些funny 的: todo 玩一玩 1小时04分钟 自己输出调用stack
      https://jyywiki.cn/OS/2022/slides/16.slides.html#/2/5
      unwind.c
  • 需要的编译选项
    • -g (生成调试信息)
    • -static (静态链接)
    • -fno-omit-frame-pointer (总是生成 frame pointer)
    • 可以尝试不同的 optimization level
      • 再试试 gdb
        ——
        没有 frame pointer 的时候呢?
  • Linus 锐评 Kernel backtrace unwind support
  • 一般问题:Still open (有很多工作可以做)
    逆向代码…

    链接与重定位


    image-20250304204601700
    预处理-编译-汇编-链接
    gcc -S 不汇编链接,只预处理+编译 输出汇编代码
    gcc -c 只进行到汇编,不链接 输出二进制目标文件
    ```c void hello();

int main() { hello(); }

<br>
```c
#include <stdio.h>

void hello() {
	printf("hello worlf 
"); }


gcc -c main.c hello.c

root@cd2c2g:/learn/os/no9# objdump -d main.o

main.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:	f3 0f 1e fa          	endbr64 
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	b8 00 00 00 00       	mov    $0x0,%eax
   d:	e8 00 00 00 00       	call   12 <main+0x12>
  12:	b8 00 00 00 00       	mov    $0x0,%eax
  17:	5d                   	pop    %rbp
  18:	c3                   	ret  


没有链接时main 不知道hello的具体位置

root@cd2c2g:/learn/os/no9# nm main.o
                 U hello
0000000000000000 T main


此时目标文件是一个数据结构,不知道hello的位置就在此处填了0

#include <stdio.h>
#include <stdint.h>
#include <assert.h>

int main();

void hello() {
        //printf("hello worlf 
");
char *p =(char *)main + 0xd + 1; int32_t offset = *(int32_t *)p; assert((char *)main + 0x12 + offset == (char *)hello); }


gcc -c main.c hello.c
gcc main.o hello.o -static

#上述assert 成立


elf文件是一个数据结构,需要满足一定的约束
image-20250304213359242
image-20250304213725591
链接时需要将补0占位的32位字节填充:S+A-P = offset
S:hello的位置
A:-4
P:main+0xe =(char *)main + 0xd + 1
满足:

assert((char *)main + 0x12 + offset == (char *)hello);


image-20250304215426351
175e-1757 = 7
image-20250304215543274
c 形式语义:共享内存+stack frame,语句
gcc -S 汇编代码:汇编语言形式语义 (寄存器+内存,指令)
as:汇编为二进制的数据结构 + 约束条件( .o描述了汇编的一切)
ld:将所有.o 的约束条件满足


0x11动态链接&加载


静态加载


elf文件数据结构定义: /usr/include/elf.h

root@cd2c2g:/usr/include# readelf -h /bin/ls
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x6aa0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          136232 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30
root@cd2c2g:/usr/include# file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=36b86f957a1be53733633d184c3a3354f3fc7b12, for GNU/Linux 3.2.0, stripped
root@cd2c2g:/usr/include# 


加载器:
1.解析数据结构
2.复制数据到内存
3.创建进程运行时初始状态(argv,envp,…)https://jyywiki.cn/OS/2022/slides/17.slides.html#/1/1 RTFM-Initial Process Stack
共同构建了进程的初始状态
4.跳转pc
Todo :自定义loader!!!!
https://jyywiki.cn/pages/OS/2022/demos/loader-static.c
https://jyywiki.cn/pages/OS/2022/demos/env.c
mmap 将数据搬到内存

./loader minimal


loader 在操作系统提供的 open,mmap,close 基础上实现
复习前面的?
cpu reset后,将磁盘前512字节(MBR)加载进入内存,然后跳转到0x7c00,开始执行bootloader
image-20250305204914588
此时没有操作系统,通过硬件驱动将数据加载到内存
bootloader:https://jyywiki.cn/pages/OS/2022/demos/bootmain.c

动态链接&加载


这是一串魔法,但是需要先理解动态链接才能理解。
again。
原始的动态库:.data(包含GTO) .txt(包含PLT)
代码段都是:只读&共享
数据段都是:进程私有
即:共享库在所有的进程中PLT是一样的,但GTO是不同的。
PLT都是固定的代码模板,PLT中保存的是相对于GTO符号的偏移量,无论共享库被加载到哪里,偏移量不变,动态链接器会在第一次访问函数时填充GTO中对应函数的在进程的实际地址。
函数->PLT->GTO->实际地址

plt_func:
  jmp *GOT[n]  ; 跳转到GOT[n]存储的地址
  push index   ; 压入重定位索引
  jmp plt_common ; 跳转到公共处理逻辑(动态链接器填充GOT)


+----------------------------------+ 0xFFFFFFFFFFFFFFFF(内核空间)
|          内核空间                | ← 所有进程共享,用户态不可访问
+----------------------------------+ 0xFFFF800000000000(典型内核地址边界)
|             栈(Stack)           | ← 向低地址增长,存储函数调用帧/局部变量
|                  ↓               |
+----------------------------------+
|          内存映射区域            | ← 包含共享库、文件映射、匿名内存等
|  (动态链接器加载的共享库驻留区)    |
|  +----------------------------+  |
|  |      共享库N(libbar.so)   |  |
|  |  - 代码段(.text):只读共享  |  | ← PLT指令(如`jmp *GOT[0x14]`)
|  |  - 数据段(.data):私有可写  |  | ← libbar.so的独立GOT(动态填充)
|  +----------------------------+  |
|  |      共享库1(libfoo.so)   |  |
|  |  - 代码段(.text):只读共享  |  | ← PLT指令(如`jmp *GOT[0x14]`)
|  |  - 数据段(.data):私有可写  |  | ← libfoo.so的独立GOT(动态填充)
|  +----------------------------+  |
|                  ↑               |
+----------------------------------+
|             堆(Heap)           | ← 向高地址增长,存储动态分配的内存(如`malloc`)
+----------------------------------+
|          BSS段(.bss)           | ← 未初始化的全局/静态变量(初始为0)
+----------------------------------+
|          数据段(.data)         | ← 已初始化的全局/静态变量(可读可写)
+----------------------------------+
|          代码段(.text)         | ← 可执行程序的机器指令(只读)
+----------------------------------+
|          保留区域                | ← 禁止访问的低地址区域(如NULL指针保护)
+----------------------------------+ 0x0000000000000000(低地址)


+----------------------------------+ 高地址
|             栈(Stack)           |
+----------------------------------+
|          内存映射区域            |
|  +----------------------------+  |
|  |       libbar.so            |  |
|  |  - PLT: jmp *GOT[0x14]     |  | ← 所有进程共享同一PLT代码页
|  |  - GOT: 0x7f8a00200014     →  | 指向实际函数地址(如`sin`)
|  +----------------------------+  |
|  |       libfoo.so            |  |
|  |  - PLT: jmp *GOT[0x14]     |  | ← 所有进程共享同一PLT代码页
|  |  - GOT: 0x7f8a00000014     →  | 指向实际函数地址(如`printf`)
|  +----------------------------+  |
+----------------------------------+
|             堆(Heap)           |
+----------------------------------+
|      可执行程序(main_program)   |
|  - .text: main函数入口           |
|  - .data: 全局变量               |
+----------------------------------+ 低地址


call printf@PLT

#PLT 条目(代码段,只读共享)
printf@PLT:
    jmp *GOT_ENTRY_FOR_PRINTF(%rip)   ; 第一次跳转:检查 GOT 是否已填充
    push $INDEX_IN_RELOC_TABLE         ; 若未填充,触发动态链接器解析
    jmp .PLT0                          ; 跳转到公共解析逻辑


自定义动态链接


DL_HEAD

LOAD("libc.dl") # 加载动态库
IMPORT(putchar) # 加载外部符号
EXPORT(hello)   # 为动态库导出符号

DL_CODE

hello:
  ...
  call DSYM(putchar) # 动态链接符号
  ...

DL_END


too hard:https://jyywiki.cn/OS/2022/slides/17.slides.html#/2/5
暂时先不去了解自定义的二进制动态链接了,真的太难了…(至少需要一天的时间,泛泛了解不如不了解。)

第7周阅读材料


sh-xv6.c
https://jyywiki.cn/pages/OS/2022/demos/sh-xv6.c
层层解析,非常像read-eval
执行:
重定向:关闭再打开
List:执行完左边 wait 再执行右边
管道:修改1 然后执行左侧,修改0 然后执行右侧
文件描述符的继承
todo:csapp复习

第8周阅读材料


15,18~23
虚拟化:
内存虚拟化:进程仿佛独占整个内存 (地址转换)
cpu虚拟化:进程仿佛独占cpu

地址转换


Version1:基址+界限 两个寄存器
(动态重定位)
这个抽象概念下:
硬件支持:

  1. 特权模式(区分用户 与 操作系统)
  2. 两个寄存器
  3. 提供MMU,进行地址转换&越界检查
  4. 修改那两个寄存器的指令
  5. 注册异常处理的指令(越界)
  6. 触发异常(使用特权指令 / 越界)
    操作系统支持:
  7. 创建进程时分配
  8. 结束进程时回收
  9. 上下文切换支持
    完整交互流程
    image-20250316193500073

    分页


    页表:虚拟页与物理页之间的映射(进程级别)
    image-20250316195000756
    PTE 页表项(Page Table Entry)
    结构:除了映射信息外,还有一堆其他信息,用于加强控制
    X86-64 页表物理地址:CR3
    risc-v:satp Supervisor Address Translation and Protection Register
    地址-虚拟页-物理页-加载-偏移量-读取数据

分页-TLB


TLB是什么?结构?
TLB未命中
上下文切换与TLB:AISD(address space identifier)
替换策略:LRU

分页-多级页表


对全部地址空间分页–页表过大
分段+分页,先对进程的地址空间分段,然后只针对分段的地址空间分页

分页-交换空间


交换空间

分页-交换策略


先进先出,随机,LRU,近似LRU
好吧,及其迅速的翻完了这些部分,没有去理解。
todo 结合csapp,重新理解内存这一部分。

0x12 xv-6


unix:https://dl.acm.org/doi/10.1145/357980.358014
unix 传奇
xv-6 教学版本导读:
https://jyywiki.cn/pages/OS/manuals/xv6-riscv-rev2.pdf
研究vscode的编译配置(很久以前的todo…)
bear 命令
todo: console 与 终端 究竟是什么?
一个XV6典型:
ecall,如何实现的,进程的trampoline & trapframe 从汇编的ecall 到c代码的syscall

0x13上下文切换


啊哈,终于到了操作系统最重要的部分了。 todo最好再不依靠视频调试一遍xv6
image-20250315194835234

  • ecall 指令:跳转到 trampoline 代码
  • 保存所有寄存器到 trapframe
  • 使内核代码能够继续执行

为什么用户的while(1) 不会让操作系统彻底卡死?
image-20250316135406829
用户程序= 状态机
操作系统= 状态机的集合,管理所有的状态机 + 自己的状态
中断:将pc指针的指令强行替换为ecall/syscall
程序状态机执行的过程中会被操作系统中断(响应中断信号?)
image-20250316141003376

上下文切换 = 处理器的虚拟化(操作系统做的所有事情,处理器都看不到)
1.硬件发生中断
2.切换到操作系统代码执行
3.操作系统切换到另外一个进程
image-20250316152717388
进程唯一持有的可见的操作系统资源就是 文件描述符。文件描述符是进程状态的一部分
image-20250316160554644
ecall 将$pc 指向$stvec Supervisor Trap Vector Base Address Register
stvec指向的地址,存储的代码,需要干哪些动作以实现进程的切换?
image-20250316161242283
image-20250316170141041
image-20250316170213850
操作系统中看到的trapframe 就是我们在qemu中看到物理地址。
image-20250316170600309
操作系统中有一部分内存是直接映射的物理内存
封存:操作系统确实将进程的寄存器存放在trapframe中,同时可以访问到该地址。
image-20250316170912507
image-20250316175722789

0x14调度策略


操作系统给我们提供了中断的机制:上下文切换
处理器以固定的频率被打断 (linux 内核可以配置固定的频率)

Round-Robin


Round-Robin:1-2-3,1-2-3 ,轮询,下一个线程在等待IO就next
多个死循环+文本编辑前台处理
优先级:linux niceness (-20~19,越nice,越倾向让别人运行) nice命令,nice 相差10,获取cpu的比例大概相差10倍
taskset

MLFQ&CFS


Round-Robin的问题,前台与后台如何更自然的调度?
MLFQ:动态优先级
若干个Round-Robin队列,每个队列一个优先级。
1.初始创建,优先级较高
2.用完时间片,调低优先级
3.经常类似io让出时间片,调高优先级
CFS:Complete Fair Scheduling
记录运行时间,每次中断调度运行时间最小
实现优先级:优先级越高,时钟越快。(尽可能让出来)
1.父子进程如何处理
​ parent frist
​ 子进程继承父进程的vruntime
2.IO 1分钟返回后,被唤醒(直接独占1分钟?)
​ 被唤醒的进程获取当前系统中最小的vruntime
3.整数溢出
​ 假设:系统中最近、最远的时刻差不超过数轴的一半

bool less(u64 a, u64 b) {
  return (i64)(a - b) < 0;
}


image-20250318203034572
再假设:进程之间有协作 ? 前面的所以都被推翻🫠
优先级反转:校长等jyy,jyy优先级最低 = 校长优先级最低

void xiao_zhang() { // 高优先级
  sleep(1); // 休息一下先
  mutex_lock(&wc);
  ...
}

void xi_zhu_ren() { // 中优先级
  while (1) ;
}

void jyy() { // 最低优先级
  mutex_lock(&wc);
  ...
}


处理优先级反转:
优先级继承….(条件变量失效)
再假设:多个cpu
调度太难了:
Linux Namespaces Control Groups(docker,操作系统中创建操作系统)
todo:man namespaces (7), cgroups (7)

0x15操作系统设计


操作系统:一组对象+访问对象的api(有点类一个数据机构,可以往里面塞元素,可以取元素🫠)
真正的操作系统接口设计规范:https://pubs.opengroup.org/onlinepubs/9699919799/mindex.html
微内核:https://sel4.systems/
外内核
unikernel:https://dl.acm.org/doi/10.1145/3447786.3456248
https://dl.acm.org/doi/10.1145/2541883.2541895
一个应用程序连着操作系统内核,直接在虚拟机内跑。
Take-away messages

  • “操作系统” 的含义随应用而变
    • 可以大而全 (Linux/Windows API)
    • 可以只有最少的硬件抽象 (Microkernel)
    • 可以没有用户态 (Unikernel)
  • 互联网时代
    • 从井里走出去:RTFM, RTFSC
    • 然后去改变这个世界