xv6系统Bootloader启动分析

2024-03-14 09:38

本文主要是介绍xv6系统Bootloader启动分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Bootloader启动分析

参考xv6的附录B
https://github.com/ranxian/xv6-chinese/blob/master/content/AppendixB.md

计算机启动后硬件的动作

一直很好奇计算器按下电源后发生了什么?基本上分为三步
BIOS引导-》bootloader加载内核到内存-》控制权交给内核
源码在此https://github.com/mit-pdos/xv6-public/blob/master/bootasm.S

bootloader简易的实现及其编译

这里我们先看下如何19行代码实现一个简易的bootloader

计算机通电启动后 BIOS会从引导设备读取512个字节,如果在这512个字节的末尾检测到一个双字节“magic number”(0x55AA),则将这512个字节的数据作为代码加载并运行。这512个字节的数据就叫做bootloader。
这里使用19行代码实现一个小的操作系统并输出“Hello world!”的bootlaoder。
参考: http://50linesofco.de/post/2018-02-28-writing-an-x86-hello-world-bootloader-with-assembly

.code16 #告诉操作系统要用16位
.global init #设置启动点init:mov $msg, %si # loads the address of msg into si  #把msg放入寄存器mov $0xe, %ah # loads 0xe (function number for int 0x10) into ah  #
print_char:lodsb # loads the byte from the address in si into al and increments sicmp $0, %al # compares content in AL with zeroje done # if al == 0, go to "done"int $0x10 # 使用中断将字符输出到屏幕jmp print_char # repeat with next byte
done:hlt # stop executionmsg: .asciz "Hello world!"

编译出的bootloader没有512字节

as -o boot.o boot.s
ld -o boot.bin --oformat binary -e init boot.o
ls -lh .3 boot.bin
784 boot.o
152 boot.s

使用0填充至510字节 并在末尾加上magic number 0xaa55

.fill 510-(.-init), 1, 0 # add zeroes to make it 510 bytes long
.word 0xaa55 # magic bytes that tell BIOS that this is bootable
as -o boot.o boot.s
ld -o boot.bin --oformat binary -e init boot.o
ls -lh .512 boot.bin
1.3k boot.o176 boot.s

使用qemu调起bootloader

sudo apt-get install qemu
qemu-system-x86_64 boot.bin

BIOS引导

CPU通电后的第一条指令位于内存F000:FFF0位置。此时CPU工作于实时模式,该模式会通过段寄存器CS与指令寄存器IP共同寻找指令所在的物理地址。计算方法是CS里的内容左移4位再加上IP里的内容,得到实际物理地址,这里BIOS第一条指令的物理地址是0xffff0。这条指令是:

ljmp   $0xf000,$0xe05b

  跳转到物理地址0xfe05b位置,执行后续的指令。这个也比较好理解,因为0xffff0比较接近0xfffff这个物理内存地址的最顶端,这么少的内存空间做不了什么事,这时候就转移一下代码的所在位置。然后,BIOS会进行一系列的硬件初始化工作。当这些工作都完成了,计算机的硬件都处在一个基础的就绪状态,就可以进行操作系统的引导了。xv6作为一个精简的unix操作系统,其boot loader在可启动磁盘上的第一个扇区,即第一个512字节的区域。BIOS会把这段代码拷贝到物理地址0x7c00到0x7dff的内存空间中。这段代码就叫做boot loader,主要用于引导操作系统内核。

boot loader

  BIOS设置cs寄存器为0x0,ip寄存器为0x7c00,开始执行boot loader程序。该程序可分为两部分,第一部分是汇编语言编写,一部分是c语言编写:
  基本流程如下:

   CPU初始化操作打开A20GateGDT的设置**实模式切换为32位的保护模式(内存管理,进程管理,硬件管理)现在主流计算都用的是分段式管理初始化栈寄存器跳转到boot/main.c
#include <inc/mmu.h># Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00..set PROT_MODE_CSEG, 0x8         # kernel code segment selector
.set PROT_MODE_DSEG, 0x10        # kernel data segment selector
.set CR0_PE_ON,      0x1         # protected mode enable flag.globl start
start:.code16                     # Assemble for 16-bit modecli                         # Disable interruptscld                         # String operations increment# Set up the important data segment registers (DS, ES, SS).xorw    %ax,%ax             # Segment number zeromovw    %ax,%ds             # -> Data Segmentmovw    %ax,%es             # -> Extra Segmentmovw    %ax,%ss             # -> Stack Segment# Enable A20:#   For backwards compatibility with the earliest PCs, physical#   address line 20 is tied low, so that addresses higher than#   1MB wrap around to zero by default.  This code undoes this.
seta20.1:inb     $0x64,%al   #从端口取一个字节的数据            # Wait for not busytestb   $0x2,%aljnz     seta20.1   #不为0则跳转movb    $0xd1,%al               # 0xd1 -> port 0x64outb    %al,$0x64
# 向0x64写入命令0xd1,该命令用于指示即将向键盘控制器的输出端口写一个字节的数据。
seta20.2:inb     $0x64,%al               # Wait for not busytestb   $0x2,%aljnz     seta20.2movb    $0xdf,%al               # 0xdf -> port 0x60outb    %al,$0x60
# 再检查0x64,判断键盘控制器是否忙碌。等不忙碌后,就可以向0x60写入数据0xdf。该数据代表开A20。# Swit from real to protected mode, using a bootstrap GDT# and segment translation that makes virtual addresses ch# identical to their physical addresses, so that the # effective memory map does not change during the switch.lgdt    gdtdescmovl    %cr0, %eaxorl     $CR0_PE_ON, %eaxmovl    %eax, %cr0# Jump to next instruction, but in 32-bit code segment.# Switches processor into 32-bit mode.ljmp    $PROT_MODE_CSEG, $protcseg.code32                     # Assemble for 32-bit mode
protcseg:# Set up the protected-mode data segment registersmovw    $PROT_MODE_DSEG, %ax    # Our data segment selectormovw    %ax, %ds                # -> DS: Data Segmentmovw    %ax, %es                # -> ES: Extra Segmentmovw    %ax, %fs                # -> FSmovw    %ax, %gs                # -> GSmovw    %ax, %ss                # -> SS: Stack Segment# Set up the stack pointer and call into C.movl    $start, %espcall bootmain# If bootmain returns (it shouldn't), loop.
spin:jmp spin  
#GDT全局描述符表 操作部分
# Bootstrap GDT
.p2align 2                                # force 4 byte alignment
gdt:SEG_NULL              # null segSEG(STA_X|STA_R, 0x0, 0xffffffff) # code segSEG(STA_W, 0x0, 0xffffffff) # data seggdtdesc:.word   0x17                            # sizeof(gdt) - 1.long   gdt                             # address gdt

把kernel加载到内存

这部分boot/main.c代码的主要作用是加载内核文件(elf)到内存中。

加载ELF头部与程序头表
  kernel是一个ELF格式的可执行文件,它遵守标准的ELF格式。我们暂时关心的就是ELF头部与程序头表,通过把它们从磁盘里加载到内存中,就可以让内核正式接管计算机了!

  kernel文件的ELF头部从启动磁盘的第二个扇区开始。前面已经说到,第一个扇区512字节就是boot loader。ELF头部与程序头表大小是4KB。
  
  内存管理单元(英语:memory management unit,缩写为MMU)

#include <inc/x86.h>
#include <inc/elf.h>/*********************************************************************** This a dirt simple boot loader, whose sole job is to boot* an ELF kernel image from the first IDE hard disk.** DISK LAYOUT*  * This program(boot.S and main.c) is the bootloader.  It should*    be stored in the first sector of the disk.**  * The 2nd sector onward holds the kernel image.**  * The kernel image must be in ELF format.** BOOT UP STEPS*  * when the CPU boots it loads the BIOS into memory and executes it**  * the BIOS intializes devices, sets of the interrupt routines, and*    reads the first sector of the boot device(e.g., hard-drive)*    into memory and jumps to it.**  * Assuming this boot loader is stored in the first sector of the*    hard-drive, this code takes over...**  * control starts in boot.S -- which sets up protected mode,*    and a stack so C code then run, then calls bootmain()**  * bootmain() in this file takes over, reads in the kernel and jumps to it.**********************************************************************/#define SECTSIZE    512
#define ELFHDR      ((struct Elf *) 0x10000) // scratch spacevoid readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);void
bootmain(void)
{struct Proghdr *ph, *eph;// read 1st page off diskreadseg((uint32_t) ELFHDR, SECTSIZE*8, 0);// is this a valid ELF?if (ELFHDR->e_magic != ELF_MAGIC)goto bad;//Program Header Table。这个表格存放着程序中所有段的信息。通过这个表我们才能找到要执行的代码段,数据段等等。所以我们要先获得这个表。// load each program segment (ignores ph flags)ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);//程序头表eph = ph + ELFHDR->e_phnum;//e_phnum 程序头表的表项的数目for (; ph < eph; ph++)// p_pa is the load address of this segment (as well// as the physical address)readseg(ph->p_pa, ph->p_memsz, ph->p_offset);// call the entry point from the ELF header// note: does not return!((void (*)(void)) (ELFHDR->e_entry))();//系统转移控制权到的虚拟地址,从而开始进程。bad:outw(0x8A00, 0x8A00);outw(0x8A00, 0x8E00);while (1)/* do nothing */;
}// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{uint32_t end_pa;end_pa = pa + count;// round down to sector boundarypa &= ~(SECTSIZE - 1);// translate from bytes to sectors, and kernel starts at sector 1offset = (offset / SECTSIZE) + 1;// If this is too slow, we could read lots of sectors at a time.// We'd write more to memory than asked, but it doesn't matter --// we load in increasing order.while (pa < end_pa) {// Since we haven't enabled paging yet and we're using// an identity segment mapping (see boot.S), we can// use physical addresses directly.  This won't be the// case once JOS enables the MMU.readsect((uint8_t*) pa, offset);pa += SECTSIZE;offset++;}
}void
waitdisk(void)
{// wait for disk reaadywhile ((inb(0x1F7) & 0xC0) != 0x40)/* do nothing */;
}void
readsect(void *dst, uint32_t offset)
{// wait for disk to be readywaitdisk();outb(0x1F2, 1);     // count = 1outb(0x1F3, offset);outb(0x1F4, offset >> 8);outb(0x1F5, offset >> 16);outb(0x1F6, (offset >> 24) | 0xE0);outb(0x1F7, 0x20);  // cmd 0x20 - read sectors// wait for disk to be readywaitdisk();// read a sectorinsl(0x1F0, dst, SECTSIZE/4);
}

先给出IDE的IO接口对应的寄存器参数:
1F2 - 扇区计数。这里面存放你要操作的扇区数量
1F3 - 扇区LBA地址的0-7位
1F4 - 扇区LBA地址的8-15位
1F5 - 扇区LBA地址的16-23位
1F6 (低4位) - 扇区LBA地址的24-27位
1F6 (第4位) - 0表示选择主盘,1表示选择从盘
1F6 (5-7位) - 必须为1
1F7 (写) - 命令寄存器
1F7 (读) - 状态寄存器
bit 7 = 1 控制器忙
bit 6 = 1 驱动器就绪
bit 5 = 1 设备错误
bit 4 N/A
bit 3 = 1 扇区缓冲区错误
bit 2 = 1 磁盘已被读校验
bit 1 N/A
bit 0 = 1 上一次命令执行失败


打印寄存器 这里就可以看到xv6使用的寄存器和当前对应的值
(gdb) info reg
eax 0x112800 1124352
ecx 0x0 0
edx 0x9d 157
ebx 0x10094 65684
esp 0x7bec 0x7bec
ebp 0x7bf8 0x7bf8
esi 0x10094 65684
edi 0x0 0
eip 0x10000c 0x10000c
eflags 0x46 [ PF ZF ]
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x10 16
gs 0x10 16

参考:
http://blog.csdn.net/qq_25426415/article/details/54583835

elf格式:https://mudongliang.github.io/2015/10/31/linuxelf.html

这篇关于xv6系统Bootloader启动分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/807989

相关文章

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

Python 迭代器和生成器概念及场景分析

《Python迭代器和生成器概念及场景分析》yield是Python中实现惰性计算和协程的核心工具,结合send()、throw()、close()等方法,能够构建高效、灵活的数据流和控制流模型,这... 目录迭代器的介绍自定义迭代器省略的迭代器生产器的介绍yield的普通用法yield的高级用法yidle

Redis在windows环境下如何启动

《Redis在windows环境下如何启动》:本文主要介绍Redis在windows环境下如何启动的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Redis在Windows环境下启动1.在redis的安装目录下2.输入·redis-server.exe

解决SpringBoot启动报错:Failed to load property source from location 'classpath:/application.yml'

《解决SpringBoot启动报错:Failedtoloadpropertysourcefromlocationclasspath:/application.yml问题》这篇文章主要介绍... 目录在启动SpringBoot项目时报如下错误原因可能是1.yml中语法错误2.yml文件格式是GBK总结在启动S

利用Python快速搭建Markdown笔记发布系统

《利用Python快速搭建Markdown笔记发布系统》这篇文章主要为大家详细介绍了使用Python生态的成熟工具,在30分钟内搭建一个支持Markdown渲染、分类标签、全文搜索的私有化知识发布系统... 目录引言:为什么要自建知识博客一、技术选型:极简主义开发栈二、系统架构设计三、核心代码实现(分步解析

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

kotlin中const 和val的区别及使用场景分析

《kotlin中const和val的区别及使用场景分析》在Kotlin中,const和val都是用来声明常量的,但它们的使用场景和功能有所不同,下面给大家介绍kotlin中const和val的区别,... 目录kotlin中const 和val的区别1. val:2. const:二 代码示例1 Java

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整