OSTEP:13&&14 内核如何启动,进程创建及进程地址空间

操作系统如何启动大体过程

1.CPU上电进行CPU Reset

使cpu有一个具体初始的状态(这是一个软硬间约定),对于x86来说 规范定义在64-ia-32-architectures-software-developer-system-第3卷的Chapter 9

  • CR0 = 0x60000010
    • 16-bit 模式
  • EIP = 0x0000fff0 CS = 0xf000
  • EFLAGS = 0x00000002
    • interrupt disabled
  • ………

qemu利用-S停到第一条指令和手册完全一样(对于模拟也就是“抄”手册)

image-20230924183932737

2.reset后执行事先在cs:ip处的ROM存储的厂商提供的firmware(固件),ffff0 处通常是一条向 firmware 跳转的 jmp 指令

比如我们qemu -S停在第一条指令

image-20231107151656324

fireware里是传统的bios或者目前流行的uefi

3.对于较为简单的BIOS来说(虽然过时,但是虚拟机好像都是用的bios)

BIOS先硬件自检POST(Power-On Self Test 开机自检),然后BIOS把第一个可引导设备(存储设备)的第一个扇区512字节加载到物理内存的 0x7c00 处(这是一个约定),然后把pc指向0x7c00(此时还是16bit实模式),这512个字节又叫主引导记录(Master Boot Record,MBR),qemu里的bios采用了seabios

  • 512字节
    • 启动代码 446字节
      • 检测分区正确性
      • 加载并跳转到磁盘上的引导程序
    • 硬盘分区表 64字节
      • 描述分区状态和位置
      • 每个分区描述信息占16字节
    • 结束标志 2字节 55AA
      • 主引导记录的有效标志,通过这个确定一个设备是可引导的

Windows为什么一般C盘是系统盘?

早期的Windows的A,B盘都是软盘,BIOS先去软盘里面读前512个bytes,看最后两个byte是否为55aa,如果是就加载这块磁盘,否则读下一块,如果都不是就启动失败。读完A/B盘之后引导到C盘

做内核pwn时的可引导压缩镜像bzImage前512字节 bzImage详细介绍: https://gohalo.me/post/kernel-compile.html

image-20230924004155691

emm,为什么会是pe格式,难懂哦

这512字节的Bootloader进行一系列初始化包括把操作系统代码载入到RAM,随后将控制权交给操作系统

操作系统内核的启动:CPU Reset → Firmware → Boot loader → Kernel_start()→ …

状态机视角理解fork execve exit

进程就是状态机

fork:复制一个一模一样的状态机(除了返回值和pid)

fork 状态机复制包括持有的所有操作系统对象

fork完后进入并发,由操作系统调度

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
for (int i = 0; i < 2; i++) {
fork();
printf("Hello\n");
}
for (int i = 0; i < 2; i++) {
wait(NULL);
}
}
grxer@Ubuntu22 ~/tmp> ./a.out
Hello
Hello
Hello
Hello
Hello
Hello
grxer@Ubuntu22 ~/tmp> ./a.out | wc -l
8
grxer@Ubuntu22 ~/tmp> ./a.out | cat
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello

造成这种奇怪现象的原因终端的stdout是linebuffer,而管道是linebuffer

linebuffer:遇到\n将缓冲区的内容输出

fullbuffer:到达设定容量后才会将缓冲区的内容输出

对于fullbuffer在fork时缓冲区里是有“Hello”的,会被复制到新的状态机

execve:将当前运行的状态机重置成另一个程序的初始状态

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!\n");
}
grxer@Ubuntu22 ~/tmp> ./a.out
PWD=/home/grxer/tmp
HELLO=WORLD
SHLVL=0
_=/usr/bin/env

execve状态机被重置,环境变量变了,printf没被执行

grxer@Ubuntu22 ~/tmp> strace ./a.out &| grep execve
execve("./a.out", ["./a.out"], 0x7ffc2b593b20 /* 56 vars */) = 0
execve("/bin/bash", ["/bin/bash", "-c", "env"], 0x7fffa6c12100 /* 1 var */) = 0
execve("/usr/bin/env", ["env"], 0x561b43500700 /* 4 vars */) = 0

strace用的stderr输出,个人用的fish,fish里的&|等于bash里的|&,等价于 2>&1 将stderr重定向到stdout

exit:销毁状态机

  • exit(0) stdlib.h 中声明的libc函数
    • 会调用 atexit
    • libc做一系列用户层面清除(释放TLS,清除缓冲区等等)后调用_exit(0)做内核层面清除
  • _exit(0) - glibc 的 syscall wrapper
    • 执行 “exit_group” 系统调用终止整个进程(所有线程)
    • 不会调用 atexit
  • syscall(SYS_exit, 0)
    • 执行 “exit” 系统调用终止当前线程
    • 不会调用 atexit

操作系统启动后

控制权给到操作系统后,操作系统会启动一个进程(这个进程必须是一个类似while循环的无终止),随后Kernel就进入后台,成为 “中断/异常处理程序”

对于我们linux来说会从下面几个路径尝试创建init进程 https://elixir.bootlin.com/linux/latest/source/init/main.c#L1493

image-20230924013930964

image-20230924014144178

随后由init进程利用fork execve exit三个系统调用进行长久的状态机复制,重置,终止,配合其他系统调用创造整个操作系统世界

进程地址空间

测试程序动态链接

int main(){}

地址空间

555555554000-555555555000 r--p 00000000 08:03 4983575                    /home/grxer/tmp/a.out
555555555000-555555556000 r-xp 00001000 08:03 4983575 /home/grxer/tmp/a.out
555555556000-555555557000 r--p 00002000 08:03 4983575 /home/grxer/tmp/a.out
555555557000-555555558000 r--p 00002000 08:03 4983575 /home/grxer/tmp/a.out
555555558000-555555559000 rw-p 00003000 08:03 4983575 /home/grxer/tmp/a.out
7ffff7c00000-7ffff7c28000 r--p 00000000 08:03 2885986 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7c28000-7ffff7dbd000 r-xp 00028000 08:03 2885986 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7dbd000-7ffff7e15000 r--p 001bd000 08:03 2885986 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7e15000-7ffff7e19000 r--p 00214000 08:03 2885986 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7e19000-7ffff7e1b000 rw-p 00218000 08:03 2885986 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7e1b000-7ffff7e28000 rw-p 00000000 00:00 0 //mmap出的匿名映射 对于一些较大的未初始化全局数组就是这样映射的
7ffff7fa4000-7ffff7fa7000 rw-p 00000000 00:00 0 //mmap出的匿名映射
7ffff7fbb000-7ffff7fbd000 rw-p 00000000 00:00 0 //mmap出的匿名映射
7ffff7fbd000-7ffff7fc1000 r--p 00000000 00:00 0 [vvar]
7ffff7fc1000-7ffff7fc3000 r-xp 00000000 00:00 0 [vdso]
7ffff7fc3000-7ffff7fc5000 r--p 00000000 08:03 2885983 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffff7fc5000-7ffff7fef000 r-xp 00002000 08:03 2885983 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffff7fef000-7ffff7ffa000 r--p 0002c000 08:03 2885983 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffff7ffb000-7ffff7ffd000 r--p 00037000 08:03 2885983 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffff7ffd000-7ffff7fff000 rw-p 00039000 08:03 2885983 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffffffdd000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

vsyscall–>vdso

具体看:https://zhuanlan.zhihu.com/p/436454953

virtual dynamic shared object

可以直接访问部分内核的共享页,共享页每隔一定时间刷新一次,对于一些只需要读内核的系统调用不需要陷入内核,vsyscall地址固定但是已经弃用了

man vdso手册后面给出了各个架构vdso支持的函数

execve后

进程只有少量内存映射

  • 静态链接:代码、数据、堆栈、堆区
  • 动态链接:代码、数据、堆栈、堆区、INTERP (ld.so)

image-20230924133834423

刚开始没有libc,动态加载器ld利用一系列系统调用加载libc.so

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
int mprotect(void *addr, size_t length, int prot);

image-20230924141641580

elf告诉操作系统进程如何加载,操作系统利用mmap去映射磁盘,懒加载(使用时触发缺页中断)