手写简易操作系统(七)--加载操作系统内核

news2024/11/18 23:47:21

前情提要

上一节中,我们开启了内存分页,这一节中,我们将加载内核,内核是用C语言写的,C语言编译完了是一段ELF可加载程序,所以我们需要学会解析ELF格式文件,并将内核加载到内存

一、ELF格式

程序中最重要的部分就是段(segment)和节(section),它们是真正的程序体,是真真切切的程序资源,所以下面的说明咱们以它们为例。程序中有很多段,如代码段和数据段等,同样也有很多节,段是由节来组成的,多个节经过链接之后就被合并成一个段了。

ELF格式的作用体现在两方面,一是链接阶段,另一方面是运行阶段,故它们在文件中组织布局咱们也从这两方面展示。

image-20240313192716756

这部分比较权威的资料可以看 /usr/include/elf.h 中这个文件

首先我们看ELF Header

1.1、ELF header

这个头是我从 /usr/include/elf.h 中节选的

typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Off;

typedef struct
{
  unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
  Elf32_Half	e_type;			/* Object file type */
  Elf32_Half	e_machine;		/* Architecture */
  Elf32_Word	e_version;		/* Object file version */
  Elf32_Addr	e_entry;		/* Entry point virtual address */
  Elf32_Off	    e_phoff;		/* Program header table file offset */
  Elf32_Off	    e_shoff;		/* Section header table file offset */
  Elf32_Word	e_flags;		/* Processor-specific flags */
  Elf32_Half	e_ehsize;		/* ELF header size in bytes */
  Elf32_Half	e_phentsize;	/* Program header table entry size */
  Elf32_Half	e_phnum;		/* Program header table entry count */
  Elf32_Half	e_shentsize;	/* Section header table entry size */
  Elf32_Half	e_shnum;		/* Section header table entry count */
  Elf32_Half	e_shstrndx;		/* Section header string table index */
} Elf32_Ehdr;

其中 e_ident数组功能为

image-20240313193422025

e_type表示elf目标文件类型

elf目标文件类型取值意义
ET_NONE0位置目标文件类型
ET_REL1可重复定位文件
ET_EXEC2可执行文件
ET_DYN3动态共享目标文件
ET_CORE4core文件,即程序崩溃时其内存映像的转储格式
ET_LOPROC0xff00特定处理器文件的扩展下边界
ET_HIPROC0xffff特定处理器文件的扩展上边界

e_machine表明elf文件在何种硬件平台上才能运行

elf体系结构类型取值意义
EM_NONE0未指定
EM_M321AT&T WE 32100
EM_SPARC2SPARC
EM_3863Intel 80386
EM_68K4Motorola 68000
EM_88K5Motorola 88000
EM_8607Intel 80860
EM_MIPS8MIPS RS3000

e_version 用来表示版本信息。

e_entry 用来指明操作系统运行该程序时,将控制权转交到的虚拟地址。

e_phoff 用来指明程序头表(program header table)在文件内的字节偏移量。如果没有程序头表,该值为0。

e_shoff 用来指明节头表(section header table)在文件内的字节偏移量。若没有节头表,该值为0。

e_flags 用来指明与处理器相关的标志

e_ehsize 用来指明elf header的字节大小。

e_phentsize 用来指明程序头表(program header table)中每个条目(entry)的字节大小,即每个用来描述段信息的数据结构的字节大小,该结构是后面要介绍的struct Elf32_Phdr。

e_phnum 用来指明程序头表中条目的数量。实际上就是段的个数。

e_shentsize 用来指明节头表(section header table)中每个条目(entry)的字节大小,即每个用来描述节信息的数据结构的字节大小。

e_shnum 用来指明节头表中条目的数量。实际上就是节的个数。

e_shstrndx 用来指明string name table在节头表中的索引index。

1.2、ELF Phdr

接下来再给大家介绍下程序头表中的条目的数据结构,这是用来描述各个段的信息用的,其结构名为struct Elf32_Phdr。struct Elf32_Phdr结构的功能类似GDT中段描述符的作用,段描述符用来描述物理内存中的一个内存段,而struct Elf32_Phdr是用来描述位于磁盘上的程序中的一个段,它被加载到内存后才属于GDT中段描述符所指向的内存段的子集。

typedef struct
{
  Elf32_Word	p_type;			/* Segment type */
  Elf32_Off		p_offset;		/* Segment file offset */
  Elf32_Addr	p_vaddr;		/* Segment virtual address */
  Elf32_Addr	p_paddr;		/* Segment physical address */
  Elf32_Word	p_filesz;		/* Segment size in file */
  Elf32_Word	p_memsz;		/* Segment size in memory */
  Elf32_Word	p_flags;		/* Segment flags */
  Elf32_Word	p_align;		/* Segment alignment */
} Elf32_Phdr;

p_type 用来指明程序中该段的类型,其值为

image-20240313194726947

p_offset 用来指明本段在文件内的起始偏移字节。

p_vaddr 用来指明本段在内存中的起始虚拟地址。

p_paddr 仅用于与物理地址相关的系统中,System V忽略用户程序中所有的物理地址,此项暂时保留。

p_filesz 用来指明本段在文件中的大小。

p_memsz 用来指明本段在内存中的大小。

p_flags 用来指明与本段相关的标志,此标志取值范围见下表

image-20240313194951012

p_align 用来指明本段在文件和内存中的对齐方式。如果值为0或1,则表示不对齐。否则p_align应该是2的幂次数。

二、编写内核

哈哈哈,下面的程序就是一个内核

// os/src/kernel/main.c

int main(void) {
    while (1) ;
    return 0;
}

这里只是将其当做进入C程序的跳板,首先编译

# 编译main.c 
# -m32 编译为32位程序
gcc -m32 -c -o devel/main.o src/kernel/main.c

链接

# 链接 
# -melf_i386 链接为elf_i386类型
# -Ttext 0xc0001500 指定入口地址
# -e main 指定入口函数
ld -melf_i386 -Ttext 0xc0001500 -e main devel/main.o -o bin/kernel.bin 

我们查看一下编译好的kernel.bin

yj@ubuntu:~/os$ readelf -e bin/kernel.bin 
ELF 头:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF32
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Intel 80386
  版本:                              0x1
  入口点地址:               0xc0001500
  程序头起点:          52 (bytes into file)
  Start of section headers:          8636 (bytes into file)
  标志:             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         7
  Size of section headers:           40 (bytes)
  Number of section headers:         9
  Section header string table index: 8

节头:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .note.gnu.propert NOTE            08048114 000114 00001c 00   A  0   0  4
  [ 2] .text             PROGBITS        c0001500 000500 000017 00  AX  0   0  1
  [ 3] .eh_frame         PROGBITS        c0002000 001000 000048 00   A  0   0  4
  [ 4] .got.plt          PROGBITS        c0004000 002000 00000c 04  WA  0   0  4
  [ 5] .comment          PROGBITS        00000000 00200c 00002b 01  MS  0   0  1
  [ 6] .symtab           SYMTAB          00000000 002038 0000e0 10      7   9  4
  [ 7] .strtab           STRTAB          00000000 002118 000051 00      0   0  1
  [ 8] .shstrtab         STRTAB          00000000 002169 000050 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

程序头:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0x00130 0x00130 R   0x1000
  LOAD           0x000500 0xc0001500 0xc0001500 0x00017 0x00017 R E 0x1000
  LOAD           0x001000 0xc0002000 0xc0002000 0x00048 0x00048 R   0x1000
  LOAD           0x002000 0xc0004000 0xc0004000 0x0000c 0x0000c RW  0x1000
  NOTE           0x000114 0x08048114 0x08048114 0x0001c 0x0001c R   0x4
  GNU_PROPERTY   0x000114 0x08048114 0x08048114 0x0001c 0x0001c R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10

 Section to Segment mapping:
  段节...
   00     .note.gnu.property 
   01     .text 
   02     .eh_frame 
   03     .got.plt 
   04     .note.gnu.property 
   05     .note.gnu.property 
   06     

三、将内核载入内存

上面的ELF文件格式其实已经解释了一个程序是由什么构成的,下面我们就要解释如何加载一段程序了

1、将硬盘中的程序加载进内存,这一步我们加载到了KERNEL_BIN_BASE_ADDR这个地址

2、分析程序的ELF文件头,找到e_phnum有几个segment需要加载,找到e_phoff第一个segment在文件中的偏移量

3、根据e_phoff找到第一个program header,根据里面的内容将相应的程序加载到对应的虚拟内存中

4、由于program header是连续的,所以,上一个segment加载完了就可以找下一个program header,再加载一段segment

loader.s需要加三个函数

; 将kernel.bin的segment拷贝到编译地址
kernel_init:
    xor eax, eax
    xor ebx, ebx		;ebx记录程序头表地址
    xor ecx, ecx		;cx记录程序头表中的program header数量
    xor edx, edx		;dx 记录program header尺寸,即e_phentsize

    mov dx,  [KERNEL_BIN_BASE_ADDR + 42]  ; 偏移文件42字节处的属性是e_phentsize,表示program header大小
    mov ebx, [KERNEL_BIN_BASE_ADDR + 28]  ; 偏移文件开始部分28字节的地方是e_phoff,表示第1个program header在文件中的偏移量
					                      ; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值
    add ebx, KERNEL_BIN_BASE_ADDR
    mov cx, [KERNEL_BIN_BASE_ADDR + 44]   ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header
.each_segment:
    cmp byte [ebx + 0], PT_NULL		      ; 若p_type等于 PT_NULL,说明此program header未使用。
    je .PTNULL

    push dword [ebx + 16]		          ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size
    mov eax, [ebx + 4]			          ; 距程序头偏移量为4字节的位置是p_offset
    add eax, KERNEL_BIN_BASE_ADDR	      ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址
    push eax				              ; 压入函数memcpy的第二个参数:源地址
    push dword [ebx + 8]			      ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr,这就是目的地址
    call mem_cpy				          ; 调用mem_cpy完成段复制
    add esp,12				              ; 清理栈中压入的三个参数
.PTNULL:
    add ebx, edx				          ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header
    loop .each_segment
    ret
; 诸字节拷贝 mem_cpy(dst,src,size)
mem_cpy:		      
    cld
    push ebp
    mov ebp, esp
    push ecx		       ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
    mov edi, [ebp + 8]	   ; dst
    mov esi, [ebp + 12]	   ; src
    mov ecx, [ebp + 16]	   ; size
    rep movsb		       ; 逐字节拷贝

    ;恢复环境
    pop ecx		
    pop ebp
    ret
; 读取硬盘
rd_disk_m_32:
							; eax=LBA扇区号
							; ebx=将数据写入的内存地址
							; ecx=读入的扇区数
    mov esi,eax	            ; 备份eax
    mov di,cx		        ; 备份扇区数到di

;第1步:设置要读取的扇区数
    mov dx,0x1f2
    mov al,cl
    out dx,al            ;读取的扇区数
    mov eax,esi	         ;恢复ax

;第2步:将LBA地址存入0x1f3 ~ 0x1f6
    ;LBA地址7~0位写入端口0x1f3
    mov dx,0x1f3                       
    out dx,al                          
    ;LBA地址15~8位写入端口0x1f4
    mov cl,8
    shr eax,cl
    mov dx,0x1f4
    out dx,al
    ;LBA地址23~16位写入端口0x1f5
    shr eax,cl
    mov dx,0x1f5
    out dx,al

    shr eax,cl
    and al,0x0f	   ; lba第24~27位
    or al,0xe0	   ; 设置7~4位为1110,表示lba模式
    mov dx,0x1f6
    out dx,al

;第3步:向0x1f7端口写入读命令,0x20 
    mov dx,0x1f7
    mov al,0x20                        
    out dx,al

;第4步:检测硬盘状态
.not_ready:		   ; 测试0x1f7端口(status寄存器)的的BSY位

    nop
    in al,dx
    and al,0x88	   ; 第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
    cmp al,0x08
    jnz .not_ready ; 若未准备好,继续等。

;第5步:从0x1f0端口读数据
    mov ax, di	   ; 以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,
			       ; 在此先用这种方法,在后面内容会用到insw和outsw等
    mov dx, 256	   ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256
    mul dx
    mov cx, ax	   
    mov dx, 0x1f0
.go_on_read:
    in ax,dx		
    mov [ebx], ax
    add ebx, 2
			  ; 由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。
			  ; loader的栈指针为0x900,bx为指向的数据输出缓冲区,且为16位,
			  ; 超过0xffff后,bx部分会从0开始,所以当要读取的扇区数过大,待写入的地址超过bx的范围时,
			  ; 从硬盘上读出的数据会把0x0000~0xffff的覆盖,
			  ; 造成栈被破坏,所以ret返回时,返回地址被破坏了,已经不是之前正确的地址,
			  ; 故程序出会错,不知道会跑到哪里去。
			  ; 所以改为ebx代替bx指向缓冲区,这样生成的机器码前面会有0x66和0x67来反转。
			  ; 0X66用于反转默认的操作数大小! 0X67用于反转默认的寻址方式.
			  ; cpu处于16位模式时,会理所当然的认为操作数和寻址都是16位,处于32位模式时,
			  ; 也会认为要执行的指令是32位.
			  ; 32位模式下用32字节的操作数)时,编译器会在指令前帮我们加上0x66或0x67,
			  ; 临时改变当前cpu模式到另外的模式下.
			  ; 假设当前运行在16位模式,遇到0X66时,操作数大小变为32位.
			  ; 假设当前运行在32位模式,遇到0X66时,操作数大小变为16位.
			  ; 假设当前运行在16位模式,遇到0X67时,寻址方式变为32位寻址
			  ; 假设当前运行在32位模式,遇到0X67时,寻址方式变为16位寻址.

    loop .go_on_read
    ret

详细的代码可以看github https://github.com/lyajpunov/os.git

为了看一下是否进入到了内核,我们可以修改一下内核

// os/src/kernel/main.c

int main(void) {
    __asm__ __volatile__ ("movb $'M', %%gs:480" : : : "memory");
    __asm__ __volatile__ ("movb $'A', %%gs:482" : : : "memory");
    __asm__ __volatile__ ("movb $'I', %%gs:484" : : : "memory");
    __asm__ __volatile__ ("movb $'N', %%gs:486" : : : "memory");
    while (1) ;
    return 0;
}

加了一点内联汇编,为了能够在屏幕上输出字符。

可以看一下仿真结果

image-20240313212441097

结束语

今天我们终于进入到内核的编写了,非常的艰辛,前期的准备工作异常的多,希望大家没有厌倦,我已经将这个代码上传到了github,地址为https://github.com/lyajpunov/os.git。
有一些程序,因为会零零散散的,所以我建议直接看github上的代码。想要看哪一节的直接 git log 看历史记录。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(黑马出品_高级篇_04)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式

(黑马出品_高级篇_04)SpringCloudRabbitMQDockerRedis搜索分布式 微服务技术——可靠性消息服务 今日目标服务异步通信-高级篇1.消息可靠性1.1.生产者消息确认1.1.1.修改配置1.1.2.定义Return回调1.1.3.定义ConfirmCallbac…

【LeetCode热题100】2. 两数相加(链表)

一.题目要求 给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。 请你将两个数相加,并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外,这两个数…

论文阅读——RemoteCLIP

RemoteCLIP: A Vision Language Foundation Model for Remote Sensing 摘要——通用基础模型在人工智能领域变得越来越重要。虽然自监督学习(SSL)和掩蔽图像建模(MIM)在构建此类遥感基础模型方面取得了有希望的结果,但…

深度学习系列62:Agent入门

1 anget介绍和openai标准接口 agent的核心是其代理协同工作的能力。每个代理都有其特定的能力和角色,你需要定义代理之间的互动行为,即当一个代理从另一个代理收到消息时该如何回复。 agent目前大多使用openai标准接口调用LLM服务,说明如下。…

Java集合基础知识总结(绝对经典)

List接口继承了Collection接口,定义一个允许重复项的有序集合。该接口不但能够对列表的一部分进行处理,还添加了面向位置的操作。 实际上有两种list:一种是基本的ArrayList,其优点在于随机访问元素,另一种是更强大的L…

PYTHON 自动化办公:更改图片后缀

1、前言 在之前的文章中,介绍了图片的压缩技术,这里讲解如何利用python批量将图片改为指定后缀的格式。当然,也可以为深度学习批量更改文件后缀,例如在分割中,可能需要img和mask的图片名称完全一致等等 PYTHON 自动化…

掌握FilterOutputStream类!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java IO相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好…

谈谈Darknet53为啥这么难训练

在我使用Imagenet2012对Darknet53进行预训练的时候,往往训练到一半,就会出现过拟合,导致无法继续向下训练,尝试了很多方法,最后发现问题出现在下图红框的部分。 得出这个结论是因为当我使用Resnet中,包含有…

力扣654 最大二叉树 Java版本

文章目录 题目描述解题思路代码 题目描述 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点,其值为 nums 中的最大值。 递归地在最大值 左边 的 子数组前缀上 构建左子树。 递归地在最大值 右边 的 子数组后缀上…

【DVWA】19. Insecure CAPTCHA 不安全的验证码(全等级)

文章目录 1. Low1) 源码分析2&#xff09;实操 2. Medium1) 源码分析2&#xff09;实操 3. High1) 源码分析2&#xff09;实操 4. Impossible1) 源码分析 1. Low 1) 源码分析 <?phpif( isset( $_POST[ Change ] ) && ( $_POST[ step ] 1 ) ) {// Hide the CAPTC…

WPF图表库LiveCharts的使用

这个LiveCharts非常考究版本&#xff0c;它有非常多个版本&#xff0c;.net6对应的是LiveChart2 我这里的wpf项目是.net6&#xff0c;所以安装的是这三个&#xff0c;搜索的时候要将按钮“包括愈发行版”打勾 git&#xff1a;https://github.com/beto-rodriguez/LiveCharts2?…

BUGKU-WEB never_give_up

题目描述 题目截图如下&#xff1a; 进入场景看看&#xff1a; 解题思路 F12查看请求和响应&#xff0c;查找线索 相关工具 base64解码URL解码Burp Suit抓包 解题步骤 F12查看请求和响应&#xff0c;发现一行注释包含一个文件名称【1p.html】&#xff0c;这应该就是提…

GaN HEMTs在电力电子应用中的交叉耦合与基板电容分析与建模

来源&#xff1a;Analysis and Modeling of Cross-Coupling and Substrate Capacitances in GaN HEMTs for Power-Electronic Applications&#xff08; TED 17年&#xff09; 摘要 本文提出了一种考虑了基板电容与场板之间交叉耦合效应的场板AlGaN/GaN高电子迁移率晶体管(HE…

RabbitMQ自学笔记——消息可靠性问题

1.发送者的可靠性 1.1生产者重连 有时由于网络波动等原因&#xff0c;发送方一次可能没有连接上RabbitMQ&#xff0c;我们可以配置发送方的连接失败重试机制。但需要注意的是&#xff1a;SpringAMQP提供的重试机制是阻塞式的重试&#xff0c;也就是说多次重试等待的过程中&am…

[JAVAEE]—进程和多线程的认识

文章目录 什么是线程什么是进程进程的组成什么是pcb 进程概括线程线程与进程的关系线程的特点 创建线程创建线程方法创建线程的第二种方法对比 其他的方式匿名内部类创建线程匿名内部类创建Runable的子类lambda表达式创建一个线程 多线程的优势 什么是线程 什么是进程 首先想…

iOS 判断触摸位置是否在图片的透明区域

装扮功能系列&#xff1a; Swift 使用UIScrollerView 实现装扮功能&#xff08;基础&#xff09;Swift 使用UIScrollerView 实现装扮功能&#xff08;拓展&#xff09;iOS 判断触摸位置是否在图片的透明区域 背景 在装扮功能中&#xff0c;一般都是长按使道具进入编辑状态&…

ES分布式搜索-使用RestClient操作索引库

RestClient操作索引库 1、什么是RestClient&#xff1f; ES官方提供了各种不同语言的客户端&#xff0c;用来操作ES。这些客户端的本质就是组装DSL语句&#xff0c;通过http请求发送给ES。官方文档地址&#xff1a;Elasticsearch Clients官方文档 2、利用JavaRestClient实现…

软考高级:软件架构风格-闭环控制概念和例题

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

Python 查找PDF中的指定文本并高亮显示

在处理大量PDF文档时&#xff0c;有时我们需要快速找到特定的文本信息。本文将提供以下三个Python示例来帮助你在PDF文件中快速查找并高亮指定的文本。 查找并高亮PDF中所有的指定文本查找并高亮PDF某个区域内的指定文本使用正则表达式搜索指定文本并高亮 本文将用到国产第三方…

Java学习笔记(11)

面向对象进阶 Static 静态变量 所有对象一起共享&#xff0c;就用static修饰 不属于对象&#xff0c;属于类的 可以用 类名.静态变量 “”&#xff1b;赋值 但是 对象.静态变量也可以访问到内容 Static内存图 Student这个类的字节码文件加载到方法区&#xff0c;并在内…