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停到第一条指令和手册完全一样(对于模拟也就是“抄”手册)
2.reset后执行事先在cs:ip处的ROM存储的厂商提供的firmware(固件),ffff0
处通常是一条向 firmware 跳转的 jmp 指令
比如我们qemu -S停在第一条指令
fireware里是传统的bios或者目前流行的uefi
- Legacy BIOS (Basic I/O System)
- UEFI (Unified Extensible Firmware Interface)
- 区别 https://www.zhihu.com/question/21672895
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
- 主引导记录的有效标志,通过这个确定一个设备是可引导的
- 启动代码 446字节
Windows为什么一般C盘是系统盘?
早期的Windows的A,B盘都是软盘,BIOS先去软盘里面读前512个
bytes
,看最后两个byte是否为55aa
,如果是就加载这块磁盘,否则读下一块,如果都不是就启动失败。读完A/B盘之后引导到C盘
做内核pwn时的可引导压缩镜像bzImage前512字节 bzImage详细介绍: https://gohalo.me/post/kernel-compile.html
emm,为什么会是pe格式,难懂哦
这512字节的Bootloader进行一系列初始化包括把操作系统代码载入到RAM,随后将控制权交给操作系统
操作系统内核的启动:CPU Reset → Firmware → Boot loader → Kernel_start()→ …
状态机视角理解fork execve exit
进程就是状态机
fork:复制一个一模一样的状态机(除了返回值和pid)
fork 状态机复制包括持有的所有操作系统对象
fork完后进入并发,由操作系统调度
|
grxer@Ubuntu22 ~/tmp> ./a.out |
造成这种奇怪现象的原因终端的stdout是linebuffer,而管道是linebuffer
linebuffer
:遇到\n
将缓冲区的内容输出
fullbuffer
:到达设定容量后才会将缓冲区的内容输出
对于fullbuffer在fork时缓冲区里是有“Hello”的,会被复制到新的状态机
execve:将当前运行的状态机重置成另一个程序的初始状态
execve “重置” 状态机,但继承持有的所有操作系统对象(文件描述符等)
|
grxer@Ubuntu22 ~/tmp> ./a.out |
execve状态机被重置,环境变量变了,printf没被执行
grxer@Ubuntu22 ~/tmp> strace ./a.out &| grep execve |
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
随后由init进程利用fork execve exit三个系统调用进行长久的状态机复制,重置,终止,配合其他系统调用创造整个操作系统世界
进程地址空间
测试程序动态链接
int main(){} |
地址空间
555555554000-555555555000 r--p 00000000 08:03 4983575 /home/grxer/tmp/a.out |
vsyscall–>vdso
具体看:https://zhuanlan.zhihu.com/p/436454953
virtual dynamic shared object
可以直接访问部分内核的共享页,共享页每隔一定时间刷新一次,对于一些只需要读内核的系统调用不需要陷入内核,vsyscall地址固定但是已经弃用了
man vdso手册后面给出了各个架构vdso支持的函数
execve后
进程只有少量内存映射
- 静态链接:代码、数据、堆栈、堆区
- 动态链接:代码、数据、堆栈、堆区、INTERP (ld.so)
刚开始没有libc,动态加载器ld利用一系列系统调用加载libc.so
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); |
elf告诉操作系统进程如何加载,操作系统利用mmap去映射磁盘,懒加载(使用时触发缺页中断)