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

前情提要

上一节中,我们开启了内存分页,这一节中,我们将加载内核,内核是用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.cint 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 VABI 版本:                          0类型:                              EXEC (可执行文件)系统架构:                          Intel 80386版本:                              0x1入口点地址:               0xc0001500程序头起点:          52 (bytes into file)Start of section headers:          8636 (bytes into file)标志:             0x0Size of this header:               52 (bytes)Size of program headers:           32 (bytes)Number of program headers:         7Size of section headers:           40 (bytes)Number of section headers:         9Section 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 AlignLOAD           0x000000 0x08048000 0x08048000 0x00130 0x00130 R   0x1000LOAD           0x000500 0xc0001500 0xc0001500 0x00017 0x00017 R E 0x1000LOAD           0x001000 0xc0002000 0xc0002000 0x00048 0x00048 R   0x1000LOAD           0x002000 0xc0004000 0xc0004000 0x0000c 0x0000c RW  0x1000NOTE           0x000114 0x08048114 0x08048114 0x0001c 0x0001c R   0x4GNU_PROPERTY   0x000114 0x08048114 0x08048114 0x0001c 0x0001c R   0x4GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10Section 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, eaxxor ebx, ebx		;ebx记录程序头表地址xor ecx, ecx		;cx记录程序头表中的program header数量xor edx, edx		;dx 记录program header尺寸,即e_phentsizemov 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_ADDRmov 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 .PTNULLpush dword [ebx + 16]		          ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:sizemov eax, [ebx + 4]			          ; 距程序头偏移量为4字节的位置是p_offsetadd 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 headerloop .each_segmentret
; 诸字节拷贝 mem_cpy(dst,src,size)
mem_cpy:		      cldpush ebpmov ebp, esppush ecx		       ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份mov edi, [ebp + 8]	   ; dstmov esi, [ebp + 12]	   ; srcmov ecx, [ebp + 16]	   ; sizerep movsb		       ; 逐字节拷贝;恢复环境pop ecx		pop ebpret
; 读取硬盘
rd_disk_m_32:; eax=LBA扇区号; ebx=将数据写入的内存地址; ecx=读入的扇区数mov esi,eax	            ; 备份eaxmov di,cx		        ; 备份扇区数到di;第1步:设置要读取的扇区数mov dx,0x1f2mov al,clout dx,al            ;读取的扇区数mov eax,esi	         ;恢复ax;第2步:将LBA地址存入0x1f3 ~ 0x1f6;LBA地址7~0位写入端口0x1f3mov dx,0x1f3                       out dx,al                          ;LBA地址15~8位写入端口0x1f4mov cl,8shr eax,clmov dx,0x1f4out dx,al;LBA地址23~16位写入端口0x1f5shr eax,clmov dx,0x1f5out dx,alshr eax,cland al,0x0f	   ; lba第24~27位or al,0xe0	   ; 设置7~4位为1110,表示lba模式mov dx,0x1f6out dx,al;第3步:向0x1f7端口写入读命令,0x20 mov dx,0x1f7mov al,0x20                        out dx,al;第4步:检测硬盘状态
.not_ready:		   ; 测试0x1f7端口(status寄存器)的的BSY位nopin al,dxand al,0x88	   ; 第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙cmp al,0x08jnz .not_ready ; 若未准备好,继续等。;第5步:从0x1f0端口读数据mov ax, di	   ; 以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,; 在此先用这种方法,在后面内容会用到insw和outsw等mov dx, 256	   ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256mul dxmov cx, ax	   mov dx, 0x1f0
.go_on_read:in ax,dx		mov [ebx], axadd 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_readret

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

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

// os/src/kernel/main.cint 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.hqwc.cn/news/536297.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

map和set的介绍和使用

文章目录 map和set关联式容器键值对set介绍模板参数 map介绍模板参数为什么map支持下标访问 multiset介绍 multimap map和set 关联式容器 我们在之前讲过STL的一些基础容器,例如vector,list,deque,forward_list等 这些其实统一…

JavaEE:网络初识

路由器VS交换机 组件局域网的方式是通过路由器或者交换机 交换机:上面有很多对接口,所有的口都是等价的,电脑可以连到任意的口上,连上的电脑就构成了局域网 路由器:又叫WiFi/猫 猫:调制解调器&#xff0…

全网最最最详细“Jupyter command ‘jupyter-notebook‘ not found.“的解决方案

"Jupyter command jupyter-notebook not found."。这通常意味着 jupyter-notebook 命令在当前的虚拟环境中未安装或未正确安装,因此系统无法识别此命令。 原因分析 未安装 Jupyter Notebook: 可能你的虚拟环境中还没有安装 Jupyter Notebook。虽然 Jupyt…

生存预后不显著?最佳阈值来帮你!| 附完整代码 + 注释

大家在进行生存预后分析时发现结果不显著,是不是当头一棒!两眼一黑!难不成这就代表我们的研究没意义吗?NONONO!别慌!说不定还有救!快来看看最佳阈值能不能捞你一把! 对生存分析感兴趣…

18. 查看帖子详情

文章目录 一、建立路由二、开发GetPostDetailHandler三、编写logic四、编写dao层五、编译测试运行 一、建立路由 router/route.go v1.GET("/post/:id", controller.GetPostDetailHandler)二、开发GetPostDetailHandler controller/post.go func GetPostDetailHand…

matplotlib系统学习记录

日期:2024.03.12 内容:将matplotlib的常用方法做一个记录,方便后续查找。 基本使用 # demo01 from matplotlib import pyplot as plt # 设置图片大小,也就是画布大小 fig plt.figure(figsize(20,8),dpi80)#图片大小,清晰度# 准…

机试:蛇形矩阵

问题描述: 代码示例: //蛇形矩阵 #include <bits/stdc.h> using namespace std;int main(){int n;cout << "输入样例" << endl; cin >> n;int k 1; for(int i 0; i < n; i){if( i %2 0){//单数行for(int j 0; j < n; j){ cout &…

网络计算机

TCP/IP四层模型 应用层&#xff1a;位于传输层之上&#xff0c;主要提供两个设备上的应用程序之间信息交换的服务&#xff0c;它定义了信息交换的格式&#xff0c;消息会交给下一层传输层来传递。我们把应用层交互的数据单元称为报文。应用层工作在操作系统的用户态&#xff0…

JS向指定位置添加元素

内容参考来源 splice方法 splice() 方法向/从数组中添加/删除项目&#xff0c;然后返回被删除的项目。 //在数组指定位置插入 var fruits ["Banana", "Orange", "Apple", "Mango"]; fruits.splice(2, 0, "Lemon", "…

26 easy 35. 搜索插入位置

//给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 // // 请必须使用时间复杂度为 O(log n) 的算法。 // // // // 示例 1: // // //输入: nums [1,3,5,6], …

C++之继承

目录 一、继承的关系 二、继承方式和子类权限 三、子类构造函数 四、继承的种类 一、继承的关系 继承一定要的关系&#xff1a;子类是父类 学生是人 狗是动物 继承的实现形式&#xff1a; class 子类名&#xff1a;继承方式 父类名 { 成员变量&#xff1a; 成员函数&a…

2024-03-13 作业

网络编程&#xff1a; 1.思维导图&#xff1a; 2.上课写的代码&#xff1a; 2.1网络字节序与主机字节序转换 运行代码&#xff1a; #include <myhead.h> int main() {int num 0x12345678;short int value 0x1234;int num_n htonl(num);int value_n htons(value);…