IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 从零开始搭建一个操作系统(4):BIOS->GRUB->Linux内核入口 -> 正文阅读

[系统运维]从零开始搭建一个操作系统(4):BIOS->GRUB->Linux内核入口

BIOS->GRUB

硬件工程师设计 CPU 时,硬性地规定在加电的瞬间,强制将 CS 寄存器的值设置为 0XF000,IP 寄存器的值设置为 0XFFF0,所以CS:IP 就指向了 0XFFFF0 这个物理地址。

这个物理地址上连接了ROM(只读内存),该ROM固化了BIOS程序,此时,BIOS启动,进入自检(加电自检)

当设备初始化和检查步骤完成之后,BIOS 会在内存中建立中断表和中断服务程序(实模式)

为了启动外部储存器中的程序,BIOS 会搜索可引导的设备。当然,Linux 通常是从硬盘中启动的。

硬盘上的第 1 个扇区(每个扇区 512 字节空间),被称为 MBR(主启动记录),其中包含有基本的 GRUB 启动程序和分区表,安装 GRUB 时会自动写入到这个扇区,当 MBR 被 BIOS 装载到 0x7c00 地址开始的内存空间中后,BIOS 就会将控制权转交给了 MBR。在当前的情况下,其实是交给了 GRUB。

GRUB 的加载分成了多个步骤,同时 GRUB 也分成了多个文件。一般MBR中只放置了boot.img,它能做的最重要的一个事情就是加载 grub2 的另一个镜像 core.img。

core.img 文件是由 GRUB 安装程序根据安装时环境信息,用其它 GRUB 的模块文件动态生成

在这里插入图片描述
如果是从硬盘启动的话,core.img 中的第一个扇区的内容就是 diskboot.img 文件。diskboot.img 文件的作用是,读取 core.img 中剩余的部分到内存中,最后将控制权交给 kernel.img 文件,最后是各个模块 module 对应的映像

正因为 GRUB2 大量使用了动态加载功能模块,这使得 core.img 文件的体积变得足够小。而 GRUB 的 core.img 文件一旦开始工作,就可以加载 Linux 系统的 vmlinuz 内核文件了。

在这之前,我们所有遇到过的程序都非常非常小,完全可以在实模式下运行,但是随着我们加载的东西越来越大,实模式这 1M 的地址空间实在放不下了,所以在真正的解压缩之前,lzma_decompress.img 做了一个重要的决定,就是调用 real_to_prot,切换到保护模式,这样就能在更大的寻址空间里面,加载更多的东西。

详解vmlinuz文件结构

内核映像文件vmlinuz

这个文件是怎么来的?其实它是由 Linux 编译生成的 bzImage 文件复制而来的。

生成 bzImage 文件需要三个依赖文件:setup.bin、vmlinux.bin,linux/arch/x86/boot/tools 目录下的 build

其中,build 只是一个 HOSTOS(正在使用的 Linux)下的应用程序,它的作用就是将 setup.bin、vmlinux.bin 两个文件拼接成一个 bzImage 文件。

setup.bin 文件是由 objcopy 命令根据 setup.elf 生成的, setup.bin 文件是由 /arch/x86/boot/ 目录下一系列对应的程序源代码文件编译链接产生(其中的 head.S 文件和 main.c 文件格外重要)

下面我们先看看 vmlinux.bin 是怎么产生的,构建 vmlinux.bin 的规则依然在 linux/arch/x86/boot/ 目录下的 Makefile 文件中

#linux/arch/x86/boot/Makefile
OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE    $(call if_changed,objcopy)

这段代码的意思是,vmlinux.bin 文件依赖于 linux/arch/x86/boot/compressed/ 目录下的 vmlinux 目标

这里有必要提一下piggy.o文件,这也是被生成vmlinux的一个对象文件,他是由piggy.S生成的,但是该文件有些特殊,他的第一个依赖文件是suffix-y,它表示内核压缩方式对应的后缀。

vmlinux(elf格式,消除了文件的符号信息和重定位信息)经过工具软件压缩后变成gz格式,它把输出方式重定向到文件,从而产生 piggy.S 汇编文件,然后加入解压信息变成了新的vmlinux

其实,vmlinux 文件就是编译整个 Linux 内核源代码文件生成的

从_start到第一个进程

CPU是无法识别压缩文件中的指令的,这个时候就要用上setup.bin 文件了,_start 正是 setup.bin 文件的入口

setup.bin 大部分代码都是 16 位实模式下的

_start:main()

	...
    //进入CPU保护模式,不会返回了       
    go_to_protected_mode();
}
	...
    //保护模式下长跳转到boot_params.hdr.code32_start
    protected_mode_jump(boot_params.hdr.code32_start,                (u32)&boot_params + (ds() << 4));
}

protected_mode_jump 是个汇编函数,跳转到 boot_params.hdr.code32_start 中的地址,调用startup_32 函数

code32_start:
long  0x100000

需要注意的是,GRUB 会把 vmlinuz 中的 vmlinux.bin 部分,放在 1MB 开始的内存空间中。通过这一跳转,正式进入 vmlinux.bin 中。

startup_32 中需要重新加载段描述符,之后计算 vmlinux.bin 文件的编译生成的地址和实际加载地址的偏移,然后重新设置内核栈,检测 CPU 是否支持长模式,接着再次计算 vmlinux.bin 加载地址的偏移,来确定对其中 vmlinux.bin.gz 解压缩的地址

如果 CPU 支持长模式的话,就要设置 64 位的全局描述表,开启 CPU 的 PAE 物理地址扩展特性。再设置最初的 MMU 页表,最后开启分页并进入长模式,跳转到 startup_64

startup_64 函数中,初始化长模式下数据段寄存器,确定最终解压缩地址,然后拷贝压缩 vmlinux.bin 到该地址,跳转到 decompress_kernel 地址处,开始解压 vmlinux.bin.gz

  .code64
  .org 0x200
SYM_CODE_START(startup_64)
  cld
  cli
  #初始化长模式下数据段寄存器
  xorl  %eax, %eax
  movl  %eax, %ds
  movl  %eax, %es
  movl  %eax, %ss
  movl  %eax, %fs
  movl  %eax, %gs
    #……重新确定内核映像加载地址的代码略过
    #重新初始化64位长模式下的栈
    leaq  rva(boot_stack_end)(%rbx), %rsp
    #……建立最新5级MMU页表的代码略过
    #确定最终解压缩地址,然后拷贝压缩vmlinux.bin到该地址
    pushq  %rsi
  leaq  (_bss-8)(%rip), %rsi
  leaq  rva(_bss-8)(%rbx), %rdi
  movl  $(_bss - startup_32), %ecx
  shrl  $3, %ecx
  std
  rep  movsq
  cld
  popq  %rsi
    #跳转到重定位的Lrelocated处
    leaq  rva(.Lrelocated)(%rbx), %rax
  jmp  *%rax
SYM_CODE_END(startup_64)

  .text
SYM_FUNC_START_LOCAL_NOALIGN(.Lrelocated)
    #清理程序文件中需要的BSS段
  xorl  %eax, %eax
  leaq    _bss(%rip), %rdi
  leaq    _ebss(%rip), %rcx
  subq  %rdi, %rcx
  shrq  $3, %rcx
  rep  stosq
    #……省略无关代码
  pushq  %rsi      
  movq  %rsi, %rdi    
  leaq  boot_heap(%rip), %rsi
    #准备参数:被解压数据的开始地址   
  leaq  input_data(%rip), %rdx
    #准备参数:被解压数据的长度   
  movl  input_len(%rip), %ecx
    #准备参数:解压数据后的开始地址     
  movq  %rbp, %r8
    #准备参数:解压数据后的长度
  movl  output_len(%rip), %r9d
    #调用解压函数解压vmlinux.bin.gz,返回入口地址
    call  extract_kernel
  popq  %rsi
    #跳转到内核入口地址 
  jmp  *%rax
SYM_FUNC_END(.Lrelocated)

上述代码中最后到了 extract_kernel 函数,它就是解压内核的函数

extract_kernel 函数根据 piggy.o 中的信息从 vmlinux.bin.gz 中解压出 vmlinux。(解压算法是编译内核的配置选项决定的)

Linux 内核入口

这个 startup_64 函数定义在 linux/arch/x86/kernel/head_64.S 文件中,它是内核的入口函数

当SMP系统加电之后,总线仲裁机制会选出多个 CPU 中的一个 CPU,称为 BSP,也叫第一个 CPU,直接执行 secondary_startup_64 函数,最后就会调用 x86_64_start_kernel 函数,最后调用了 x86_64_start_reservations 函数,其中处理了平台固件相关的东西

start_kernel 函数中调用了大量 Linux 内核功能的初始化函数

void start_kernel(void){    
    char *command_line;    
    char *after_dashes;
    //CPU组早期初始化
    cgroup_init_early();
    //关中断
    local_irq_disable();
    //ARCH层初始化
    setup_arch(&command_line);
    //日志初始化      
    setup_log_buf(0);    
    sort_main_extable();
    //陷阱门初始化    
    trap_init();
    //内存初始化    
    mm_init();
    ftrace_init();
    //调度器初始化
    sched_init();
    //工作队列初始化
    workqueue_init_early();
    //RCU锁初始化
    rcu_init();
    //IRQ 中断请求初始化
    early_irq_init();    
    init_IRQ();    
    tick_init();    
    rcu_init_nohz();
    //定时器初始化 
    init_timers();    
    hrtimers_init();
    //软中断初始化    
    softirq_init();    
    timekeeping_init();
    mem_encrypt_init();
    //每个cpu页面集初始化
    setup_per_cpu_pageset();    
    //fork初始化建立进程的 
    fork_init();    
    proc_caches_init();    
    uts_ns_init();
    //内核缓冲区初始化    
    buffer_init();    
    key_init();    
    //安全相关的初始化
    security_init();  
    //VFS数据结构内存池初始化  
    vfs_caches_init();
    //页缓存初始化    
    pagecache_init();
    //进程信号初始化    
    signals_init();    
    //运行第一个进程 
    arch_call_rest_init();
}

我们只关注一个 arch_call_rest_init 函数

void __init __weak arch_call_rest_init(void){    
    rest_init();
}
noinline void __ref rest_init(void){    struct task_struct *tsk;
    int pid;
    //建立kernel_init线程
    pid = kernel_thread(kernel_init, NULL, CLONE_FS);   
    //建立khreadd线程 
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
}

总结

写了挺多了,来总结一下吧,要不然真的看着挺混乱的。

一副经典的图
在这里插入图片描述
这篇文章是一步步分析的,所以我来个时间上的先后顺序总结。

首先是一份Linux内核源码,形成了setup.bin和vmlinux.bin文件,经过build后形成了vmlinuz内核映像文件,此时还是放在硬盘中。

当BIOS把控制权交给grub后(前面的过程就不做概述了),此时计算机是实模式,内存较小,因此只能加载boot.img文件,该文件加载core.img,后面再加载了各个img文件,其中有kernel.img文件,该文件加载vmlinuz文件;此时lzma_decompress.img让计算机进入保护模式。

GRUB 加载 vmlinuz 文件之后,会把控制权交给 vmlinuz 文件的 setup.bin 的部分中 _start,它会设置好栈,清空 bss,设置好 setup_header 结构,调用 16 位 main 切换到保护模式,最后跳转到 1MB 处的 vmlinux.bin 文件中。

从 vmlinux.bin 文件中 startup32、startup64 函数开始建立新的全局段描述符表和 MMU 页表,切换到长模式下解压 vmlinux.bin.gz。释放出 vmlinux 文件之后,由解析 elf 格式的函数进行解析,释放 vmlinux 中的代码段和数据段到指定的内存。然后调用其中的 startup_64 函数,在这个函数的最后调用 Linux 内核的第一个 C 函数。

Linux 内核第一个 C 函数重新设置 MMU 页表,随后便调用了最有名的 start_kernel 函数, start_kernel 函数中调用了大多数 Linux 内核功能性初始化函数,在最后调用 rest_init 函数建立了两个内核线程,在其中的 kernel_init 线程建立了第一个用户态进程。

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2021-08-27 12:16:30  更:2021-08-27 12:17:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 11:57:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码