(二)系统初始化

目录

x86架构概述

计算机的工作模式

8086处理器原理

32位处理器

系统交互

常用汇编指令

从BIOS到bootloader

BIOS 时期

bootloader 时期

从实模式切换到保护模式

kernel.img 运行

启动过程总结

内核初始化

1-创建样板进程,及各个模块初始化  【各个职能部门的创建】

2-用户态祖先进场创建-1号进程

3-内核态祖先进程的创建-2号进程

总结

系统调用过程

glibc 对系统调用的封装

32位 DO_CALL (位于 i386 目录下 sysdep.h)

64位 DO_CALL (位于 x86_64 目录下 sysdep.h)

系统调用表 sys_call_table

总结-64位系统调用分析


x86架构概述

计算机的工作模式

CPU 包括三个部分,运算单元、数据单元 控制单元

  • 运算单元:负责计算
  • 数据单元:暂存数据,包括缓存和寄存器组
  • 控制单元:控制中心 取址执行

当前指令分两部分:一部分做什么操作 一部分操作那些数据

  • 数据单元根据数据的地址,从数据段里读到数据寄存器里,就可以参与运算了。运算单元做完运算,产生的结果会暂存在数据单元的数据寄存器里。最终,会有指令将数据写回内存中的数据段。

总线上主要有两类数据

  • 一个是地址数据,也就是我想拿内存中哪个位置的数据,这类总线叫地址总线(Address Bus);
  • 另一类是真正的数据,这类总线叫数据总线(Data Bus)

数据总线的位数,决定了一次能拿多少个数据进来。

8086处理器原理

数据单元:

  • 8086内部有8个16位的通用寄存器:AX、BX、CX、DX、SP、BP、SI、DI。
  • 这些寄存器主要用于在计算过程中暂存数据。
  • 这些寄存器比较灵活,其中 AX、BX、CX、DX 可以分成两个 8 位的寄存器来使用
  • 这样,比较长的数据也能暂存,比较短的数据也能暂存。

控制单元:

IP 寄存器就是指令指针寄存器(Instruction Pointer Register).指向代码段中下一条指令的位置。

  • -CPU 会根据它来不断地将指令从内存的代码段中,加载到 CPU 的指令队列中,然后交给运算单元去执行。

每个进程都分代码段和数据段,为了指向不同进程的地址空间,有四个 16 位的段寄存器。分别是CS、DS、SS、ES。

  • -CS就是代码段寄存器,通过它可以找到代码在内存中的位置
  • -CSDS 是数据段的寄存器,通过它可以找到数据在内存中的位置
  • -CSSS是栈寄存器
  • -CSES是附加段寄存器,存放当前执行程序中一个辅助数据段的段地址。 段寄存器 偏移地址寄存器

如果运算中需要加载内存中的数据,需要通过DS找到内存中 的数据,加载到通用寄存器中

  • 对于一个段,有一个起始的地址,而段内的具体位置,称为偏移量(Offset),在CS和DS中都存着一个段的起始地址。
  • 代码段的偏移量在IP寄存器中,数据段的偏移量会放到通用寄存器中。

起始地址都是16位的,IP寄存器和通用寄存器都是16位的,偏移量也是16位的,但8086的地址总线地址是20位

  • 凑够20位方法:起始地址16+偏移量,
  • 也就是把CS和DS中的值左移4位,变成20位,加上16位的偏移量,这样就可以得到20位的数据地址

从这个计算方式可以算出,对于只有20位地址8086来讲,能够分出的地址也就2^20=1M,超过这个空间就访问不到了。

  • 如果你想访问1M+X的地方,这个位置已经超出20位了,由于地址总线只有20位,在总线上超过20位的部分是发不出去的,所以发出去的还是X,最后还是会访问1M内的X的位置。

那么一个段最大是多大,因为偏移量只能是16位的,所以一个段最大的大小为2^16=64K。

32位处理器

在32位的CPU中,有32根地址总线,可以访问2^32=4G的内存

x86架构是开放的,因此32位的CPU需要兼容原来的架构

在开放架构的基础上,如何保持兼容?

1. 通用寄存器 - 将8个16位的通用寄存器扩展到8个32位的通用寄存器,但依然保留16位和8位的使用方式

  • - 高16位不能分成两个8位使用,因为这是不兼容的

2. IP寄存器 - 指向下一条指令的指令指针寄存器IP

  • -会扩展成32位的,同样兼容16位

3. 段寄存器 - CS、DS、SS和ES仍然是16位,但不再是段的起始地址,段的起始地址放在内存的某个地方(表格)

  • - 表格中的一项是段描述符,里面才是段真正的起始地址 - 而段寄存器里面保存的是这个表格中某一项,称为选择子
  • - 获取段起始地址的流程:先间接地从段寄存器中找到表格中的一项,再从表格中的一项拿到段真正的起始地址
  • - 为了快速拿到段的起始地址,段寄存器会从内存中拿到CPU的描述符高速缓存器中
  • - 这种模式与8086的模式不兼容,但非常灵活,可以保持未来的兼容性

实模式 VS 保护模式

  1. 在32位的架构下,将前一种模式称为实模式(Real Pattern),后一种模式称为保护模式(Protected Pattern)
  2. 系统刚刚启动的时候,CPU处于实模式,此时和原来的模式是兼容的。即32位的CPU,也支持在原来的模式下运行,
  3. 当需要更多内存时,可以遵循一定的规则,进行一系列操作,然后切换到保护模式,就能够用32位CPU更强大的能力
  4. 如果不能无缝兼容,但通过切换模式兼容,也是可以接受的

系统交互

常用汇编指令

move a b :把b值赋给a,使a=b


call和ret   :call调用子程序,子程序以ret结尾
jmp          :无条件跳
int            :中断指令

or    :或运算
xor  :异或运算
shl   :算术左移
ahr  :算术右移


push xxx :xxx入栈
pop xxx  : xxx出栈

 

add a b :a=a+b
inc         : 加1
dec        : 减1
sub a b : a=a-b
cmp      : 减法比较,修改标志位

从BIOS到bootloader

BIOS 时期

在主板上,有一个东西叫ROM(Read Only Memory,只读存储器)。

上面早就固化了一些初始化的程序,也就是BIOS(Basic Input and Output System,基本输入输出系统)。  

在x86系统中,将1M空间最上面的0xF0000到0xFFFFF这64K映射给ROM

当电脑刚加电的时候,会做一些重置的工作,

将CS设置为0xFFFF,将IP设置为0x0000,所以第一条指令就会指向0xFFFF0,正是在ROM的范围内。

在这里,有一个JMP命令会跳到ROM中做初始化工作的代码,于是,BIOS开始进行初始化的工作

  • 1.BIOS 要检查一下系统的硬件
  • 2.要建立一个中断向量表和中断服务程序
  • 3.检查正常则屏幕显示系统BIOS信息

bootloader 时期

BIOS初始化之后,我们需要操作系统。那么操作系统会在哪里呢?一般都会安装在硬盘上,在BIOS的界面上。你会看到一个启动盘的选项。一般在第一个扇区,占 512 字节,而且以 0xAA55 结束。

这是一个约定,当满足这个条件的时候,就说明这是一个启动盘,在 512 字节以内会启动相关的代码(这个扇区通常称为MBR(Master Boot Record主引导记录 / 扇区)

1-BIOS完成任务后,会将boot.img从硬盘加载到内存中的0x7c00来运行 

2-由于512字节实在有限boot.img做不了太多的事情。它能做最重要一个事情就是加载grub2另一个镜像core.img

  • boot.img 先加载的是 core.img 的第一个扇区。如从硬盘启动,这个扇区里是diskboot.img,对应代码是 diskboot.S。
  • boot.img 将控制权交给 diskboot.img 后,diskboot.img 的任务就是将 core.img 的其他部分加载进来,

------先是解压缩程序 lzma_decompress.img,再往下是 kernel.img,最后是各个模块 module对应的映像。

这里需要注意,它不是 Linux 的内核,而是 grub 的内核。
lzma_decompress.img 对应的代码是 startup_raw.S,本来 kernel.img 是压缩过的,现在执行的时候,需要解压缩。

在真正的解压缩之前,lzma_decompress.img 需要调用 real_to_prot,切换到保护模式,这样就能在更大的寻址空间里面,加载更多的东西

从实模式切换到保护模式

第一项是启用分段,

  • 就是在内存里面建立段描述符表,将寄存器里面段寄存器变成段选择子,指向某个段描述符,实现不同进程的切换

第二项是启动分页。

  • 能够管理的内存变大了,就需要将内存分成相等大小的块

第三项打开 Gate A20,也就是第 21根地址线的控制线。

  • (在实模式 8086 下面,一共就 20 个地址线,可访问 1M 的地址空间。如果超过了这个限度怎么办呢?当然是绕回来了。在保护模式下,第 21 根要起作用了,于是我们就需要打开 Gate A20。 )
  • 切换保护模式的函数 DATA32 call real_to_prot 会打开 Gate A20,也就是第 21 根地址线的控制线

kernel.img 运行

kernel.img 对应的代码是 startup.S 以及一堆 c 文件

在 startup.S 中会调用 grub_main,这是grub kernel 的主函数。

正常启动,grub_main 最后会调用 grub_command_execute (“normal”, 0, 0),最终会调用 grub_normal_execute() 函数

  • 在这个函数里面,grub_load_config() 开始解析, grub.conf 文件里的配置信息。
  • 在这个函数里面,grub_show_menu() 会显示出让你选择的那个操作系统的列表
  • 一旦选定那个操作系统,就要开始调用 grub_menu_execute_entry()

开始执行选择的那一项,例如里面的linux16命令,表示装载指定的内核文件,并传递内核启动参数,

于是grub_cmd_linux()函数被调用,首先会读取linux内核头部的数据结构,加载到内存中来,检查通过,会加载整个linux内核镜像到内存

当都做完,调用grub_command_execute("boot",0,0),开始真正的启动内核。

启动过程总结

内核初始化

 内核初始化, 运行 `start_kernel()` 函数(位于 init/main.c), 初始化做三件事

  •     - 创建样板进程, 及各个模块初始化
  •     - 创建【管理/创建用户态进程】的进程
  •     - 创建【管理/创建内核态进程】的进程

1-创建样板进程,及各个模块初始化  【各个职能部门的创建】

  • 1- (项目管理初始化)创建第一个进程, 0号进程. ` set_task_stack_end_magic(&init_task)                                                                                                                              struct task_struct init_task = INIT_TASK(init_task)`
  • 2- (办事大厅初始化)初始化中断, `trap_init()`. 系统调用也是通过发送中断进行, 由 `set_system_intr_gate()` 完成.
  • 3- (会议室管理系统初始化) 初始化内存管理模块, `mm_init()`
  • 4- (项目管理流程初始化)    初始化进程调度模块, `sched_init()`
  • 5- (项目资料库初始化)        初始化基于内存的文件系统 rootfs, `vfs_caches_init()` 
  • 6- (其他初始化)                   调用 `rest_init()` 完成其他初始化工作               

0 号进程。这是唯一一个没有通过 fork 或者kernel_thread 产生的进程,是进程列表的第一个。

VFS(虚拟文件系统)将各种文件系统抽象成统一接口

2-用户态祖先进场创建-1号进程

创建【管理/创建用户态进程】的进程:1号进程 

1- rest_init()通过 kernel_thread(kernel_init,...) 创建 1号进程(工作在用户态).

2- 权限管理

  •  x86 提供 4个 Ring 分层权限
  • 操作系统: Ring0-内核态(访问核心资源); Ring3-用户态(普通程序)

3- 用户态调用系统调用:

  • 用户态-系统调用-保存寄存器-内核态执行系统调用-恢复寄存器-返回用户态 

4- 新进程执行 kernel_init 函数, 先运行 ramdisk 的 /init 程序(位于内存中)

  •  首先加载 ELF 文件
  •  设置用于保存用户态寄存器的结构体
  •  返回进入用户态
  •   /init 加载存储设备的驱动

5- kernel_init 函数启动存储设备文件系统上的 init

3-内核态祖先进程的创建-2号进程

创建【管理/创建内核态进程】的进程: 2号进程 

- `rest_init()` 通过 `kernel_thread(kthreadd,...)` 创建 2号进程(工作在内核态).

- `kthreadd` 负责所有内核态线程的调度和管理

总结

系统调用过程

glibc 对系统调用的封装

int open(const char *pathname, int flags, mode_t mode)glibc 里面的 open 函数

make-syscall.sh  syscall-template.S

本节解析 glibc 函数如何调用到内核的 open

用户进程调用 open 函数

  •     - glibc 的 syscal.list 列出 glibc 函数对应的系统调用
  •     - glibc 的脚本 make_syscall.sh 根据 syscal.list 生成对应的宏定义(函数映射到系统调用)
  •     - glibc 的 syscal-template.S 使用这些宏, 定义了系统调用的调用方式(也是通过宏)
  •     - 其中会调用 DO_CALL (也是一个宏), 32位与 64位实现不同

32位 DO_CALL (位于 i386 目录下 sysdep.h)

  • 1- 将调用参数放入寄存器中, 由系统调用名得到系统调用号, 放入 eax
  • 2- 执行 ENTER_KERNEL(一个宏), 对应 int $0x80 触发软中断, 进入内核
  • 3- 调用软中断处理函数 entry_INT80_32(内核启动时, 由 trap_init() 配置)
  • 4- entry_INT80_32 将用户态寄存器存入 pt_regs 中(保存现场以及系统调用参数), 调用 do_syscall_32_iraq_on 
  • 5- do_syscall_32_iraq_on 从 pt_regs 中取系统调用号(eax), 从系统调用表得到对应实现函数, 取 pt_regs 中存储的参数, 调用系统调用
  • 6- entry_INT80_32 调用 INTERRUPT_RUTURN(一个宏)对应 iret 指令, 系统调用结果存在 pt_regs 的 eax 位置, 根据 pt_regs 恢复用户态进程

64位 DO_CALL (位于 x86_64 目录下 sysdep.h)

 - 通过系统调用名得到系统调用号, 存入 rax; 不同中断, 执行 syscall 指令
    - MSR(特殊模块寄存器), 辅助完成某些功能(包括系统调用)
    - trap_init() 会调用 cpu_init->syscall_init 设置该寄存器
    - syscall 从 MSR 寄存器中, 拿出函数地址进行调用, 即调用 entry_SYSCALL_64
    - entry_SYSCALL_64 先保存用户态寄存器到 pt_regs 中
    - 调用 entry_SYSCALL64_slow_pat->do_syscall_64
    - do_syscall_64 从 rax 取系统调用号, 从系统调用表得到对应实现函数, 取 pt_regs 中存储的参数, 调用系统调用
    - 返回执行 USERGS_SYSRET64(一个宏), 对应执行 swapgs 和 sysretq 指令; 系统调用结果存在 pt_regs 的 ax 位置, 根据 pt_regs 恢复用户态进程

系统调用表 sys_call_table

    - 32位 定义在 arch/x86/entry/syscalls/syscall_32.tbl 
    - 64位 定义在 arch/x86/entry/syscalls/syscall_64.tbl
    - syscall_*.tbl 内容包括: 系统调用号, 系统调用名, 内核实现函数名(以 sys 开头)
    - 内核实现函数的声明: include/linux/syscall.h
    - 内核实现函数的实现: 某个 .c 文件, 例如 sys_open 的实现在 fs/open.c
        - .c 文件中, 以宏的方式替代函数名, 用多层宏构建函数头
    - 编译过程中, 通过 syscall_*.tbl 生成 unistd_*.h 文件
        - unistd_*.h 包含系统调用与实现函数的对应关系
    - syscall_*.h include 了 unistd_*.h 头文件, 并定义了系统调用表(数组)

总结-64位系统调用分析

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章