首页 > 其他分享 >希冀 操作系统 实验lab1 内核、Boot和printf(保姆级教程 建议点赞收藏 未完成)

希冀 操作系统 实验lab1 内核、Boot和printf(保姆级教程 建议点赞收藏 未完成)

时间:2024-11-20 10:16:36浏览次数:3  
标签:文件 函数 ELF readelf Boot 地址 lab1 内核 点赞

这次我把所有代码都写出来方便大家复制(可以直接复制我的代码粘贴到终端执行)

开始之前首先先拉取lab1的内容(一定先干这个,不然做不了实验!!!!!!!!!!!!!!!!)

先切换到这个目录下

cd 20221105894-lab

变为:

输入

git pull
git checkout origin/lab1

 之后刷新一下你的 学号-lab文件夹,如果改变了就好了,如果没有变为lab1的数据就换成

git checkout lab1

 如果还是没变化私信我

做作业直接看实战演练(前提必须把lab1数据获取了!!!)

做作业直接看实战演练(前提必须把lab1数据获取了!!!)

做作业直接看实战演练(前提必须把lab1数据获取了!!!)

做作业直接看实战演练(前提必须把lab1数据获取了!!!)

做作业直接看实战演练(前提必须把lab1数据获取了!!!)

做作业直接看实战演练(前提必须把lab1数据获取了!!!)

一.提取文档内容 

  1. 操作系统启动与内核
    • 启动流程与硬件软件关系:操作系统启动是一个复杂过程,涉及硬件和软件相互依存。计算机由硬件和软件组成,操作系统管理硬件资源。硬件需软件控制,软件依赖硬件载入,早期工程师将此纠结过程称为 “bootstrapping”。操作系统内核是核心部分,需与硬件交互,其代码不能在磁盘或易失性内存中,通常置于非易失性存储器(如 ROM 或 FLASH)。但存在存储空间有限、限制多操作系统启动和不利于移植等问题123。
    • Bootloader 的作用与阶段:为解决上述问题,引入 Bootloader。它分为 stagel 和 stage2 两部分,stagel 初始化硬件设备,在 ROM 或 FLASH 上运行,为 stage2 准备 RAM 空间并复制代码、设置堆栈后跳转;stage2 在 RAM 中运行,用 C 语言实现复杂功能,包括初始化硬件、加载内核镜像到 RAM、设置启动参数并将控制权转交给内核456。
    • gxemul 中的简化流程:在 gxemul 仿真器中,启动流程简化,它提供了 bootloader 功能,可直接加载 elf 格式内核,将内核加载到内存后跳转至入口即可完成启动78。
  2. 编译链接相关知识
    • Makefile 解读:Makefile 指导程序构建,通过阅读可了解源代码生成可执行文件的过程。实验代码顶层 Makefile 定义了变量、规则等,如 modules 定义内核模块,objects 表示编译依赖的.o 文件。它还包含了构建项目的规则、依赖关系以及清理文件的方法,引用的 include.mk 文件定义了交叉编译相关变量9102。
    • ELF 文件结构与功能:ELF 是 Unix 常用文件格式,包括可重定位、可执行和共享对象文件。其结构包含 ELF Header、Program Header Table、Section Header Table、Segments 和 Sections 等部分。连接器通过目标文件中的信息(记录在 ELF 文件中)连接多个目标文件,可通过阅读解析程序了解其详细结构,如完成 readelf.c 中代码以输出 elf 文件的 section header 信息。最终生成的内核为 ELF 格式,由模拟器载入,通过 Linker Script 可控制各段加载地址111213。
  3. MIPS 架构相关内容
    • 内存布局与内核位置:32 位 MIPS CPU 程序地址空间分为 4 个区域,包括 User Space、Kernel Space Unmapped Cached、Kernel Space Unmapped Uncached 和 Kernel Space Mapped Cached。载入内核时,因未建立虚存机制,只能选用无需 MMU 的内存空间,即 kseg0 或 kseg1,而内核通常放在 kseg0,具体位置可在 include/mmu.h 中查看141516。
    • 汇编与 C 语言关系:在操作系统编程中,MIPS 汇编与 C 语言联系紧密。循环和判断语句在 MIPS 汇编中通过判断加跳转实现,函数调用时,编译器在函数前后添加压栈和弹栈操作保存函数状态,调用函数将参数存于 a0 - a3 寄存器,返回值存于 v0 - v1 寄存器。同时,MIPS 有 32 个通用寄存器,遵循一定使用约定,如 s0 - s7 和 fp(或 s8)寄存器在函数调用前后值不变(fp 在调用位置无关函数时有特例),ra 寄存器存放函数返回地址。此外,还有特殊的 PC 寄存器,可用于调试内核171819。

Exercise 

Exercise 1.1

请修改 include.mk 文件,使交叉编译器的路径正确。之后执行 make指令,如果配置一切正确,则会在 gxemul 目录下生成 vmlinux 的内核文件。

在 20221105894-lab目录下找到include.mk 文件  把CROSS_COMPILE 后面的路径改成我下面的

# Common includes in Makefile
#
# Copyright (C) 2007 Beihang University
# Written By Zhu Like ( [email protected] )


CROSS_COMPILE :=  /opt/eldk/usr/bin/mips_4KC-
CC			  := $(CROSS_COMPILE)gcc
CFLAGS		  := -O -G 0 -mno-abicalls -fno-builtin -Wa,-xgot -Wall -fPIC
LD			  := $(CROSS_COMPILE)ld

然后 在20221105894-lab目录下输入

make

 

会在 gxemul 目录下生成 vmlinux 的内核文件

 

Exercise1.2

阅读./readelf 文件夹中 kerelf.h、readelf.c 以及 main.c 三个文件中的代码,并完成 readelf.c 中缺少的代码,readelf 函数需要输出 elf 文件的所有 sectionheader 的序号和地址信息,对每个 section header,输出格式为:”%d:0x%x\n”,两个标识符分别代表序号和地址。

打开readelf.c   直接复制我的代码 覆盖即可

/* This is a simplefied ELF reader.
 * You can contact me if you find any bugs.
 *
 * Luming Wang<[email protected]>
 */

#include "kerelf.h"
#include <stdio.h>
/* Overview:
 *   Check whether it is a ELF file.
 *
 * Pre-Condition:
 *   binary must longer than 4 byte.
 *
 * Post-Condition:
 *   Return 0 if `binary` isn't an elf. Otherwise
 * return 1.
 */
int is_elf_format(u_char *binary)
{
        Elf32_Ehdr *ehdr = (Elf32_Ehdr *)binary;
        if (ehdr->e_ident[EI_MAG0] == ELFMAG0 &&
                ehdr->e_ident[EI_MAG1] == ELFMAG1 &&
                ehdr->e_ident[EI_MAG2] == ELFMAG2 &&
                ehdr->e_ident[EI_MAG3] == ELFMAG3) {
                return 1;
        }

        return 0;
}

/* Overview:
 *   read an elf format binary file. get ELF's information
 *
 * Pre-Condition:
 *   `binary` can't be NULL and `size` is the size of binary.
 *
 * Post-Condition:
 *   Return 0 if success. Otherwise return < 0.
 *   If success, output address of every section in ELF.
 */
int readelf(u_char *binary, int size)
{
        Elf32_Ehdr *ehdr = (Elf32_Ehdr *)binary;

        int Nr;

        Elf32_Shdr *shdr = NULL;

        u_char *ptr_sh_table = NULL;
        Elf32_Half sh_entry_count;
        Elf32_Half sh_entry_size;


        // check whether `binary` is a ELF file.
        if (size < 4 || !is_elf_format(binary)) {
                printf("not a standard elf format\n");
                return 0;
        }

        // get section table addr, section header number and section header size.

        // for each section header, output section number and section addr.
    
        ptr_sh_table = binary + ehdr->e_shoff; 
        sh_entry_count = ehdr->e_shnum;      
        sh_entry_size = ehdr->e_shentsize;   
        for (Nr = 0; Nr < sh_entry_count; Nr++) {
                shdr = (Elf32_Shdr *)(ptr_sh_table + Nr * sh_entry_size);
                printf("%d:0x%x\n", Nr, shdr->sh_addr);
        }


        return 0;
}

Exercise 1.3

填写 tools/scse0 3.lds 中空缺的部分,将内核调整到正确的位置上。

OUTPUT_ARCH(mips)
/*
Set the architecture to mips.
*/
ENTRY(_start)
/*
Set the ENTRY point of the program to _start.
*/
SECTIONS
{

/*To do:
  fill in the correct address of the key section
  such as text data bss ...
*/

 /* 定义程序的起始地址为 0x80000000 */
    . = 0x80000000;

    /* .text 段:代码段 */
    .text : {
        _text = .;             /* 记录 .text 段的起始地址 */
        *(.text)               /* 包含所有 .text 段的数据 */
        *(.text.*)             /* 包含名称以 .text 开头的其他段 */
        _etext = .;            /* 记录 .text 段的结束地址 */
    }

    /* .rodata 段:只读数据段 */
    .rodata : {
        _rodata = .;           /* 记录 .rodata 段的起始地址 */
        *(.rodata)             /* 包含所有 .rodata 段的数据 */
        *(.rodata.*)           /* 包含名称以 .rodata 开头的其他段 */
        _erodata = .;          /* 记录 .rodata 段的结束地址 */
    }

    /* .data 段:初始化数据段 */
    .data : {
        _data = .;             /* 记录 .data 段的起始地址 */
        *(.data)               /* 包含所有 .data 段的数据 */
        *(.data.*)             /* 包含名称以 .data 开头的其他段 */
        _edata = .;            /* 记录 .data 段的结束地址 */
    }

    /* .bss 段:未初始化数据段 */
    .bss : {
        _bss = .;              /* 记录 .bss 段的起始地址 */
        *(COMMON)              /* 包含未初始化的全局变量 */
        *(.bss)                /* 包含所有 .bss 段的数据 */
        *(.bss.*)              /* 包含名称以 .bss 开头的其他段 */
        _ebss = .;             /* 记录 .bss 段的结束地址 */
    }
end = . ;
}

Exercise 1.4

完成 boot/start.S 中空缺的部分。设置栈指针,跳转到 main 函数。使用/OSLAB/gxemul -E testmips -C R3000 -M 64 elf-fle 运行(其中 elf-fle 是你编译生成的 vmlinux 文件的路径)。 

#include <asm/regdef.h>
#include <asm/cp0regdef.h>
#include <asm/asm.h>


			.section .data.stk
KERNEL_STACK:
			.space 0x8000    /* 定义8KB大小的内核栈 */


			.text
LEAF(_start) /* LEAF 是一个宏,定义不调用其他函数的入口 */

	.set	mips2          /* 使用 MIPS II 指令集 */
	.set	reorder        /* 允许重新排序指令 */

	/* 禁用中断 */
	mtc0	zero, CP0_STATUS

	/* 禁用 watch 异常 */
	mtc0    zero, CP0_WATCHLO
	mtc0    zero, CP0_WATCHHI

	/* 禁用内核模式下的缓存 */
	mfc0	t0, CP0_CONFIG
	and	t0, ~0x7       /* 清除低三位 */
	ori	t0, 0x2        /* 设置为无缓冲模式 */
	mtc0	t0, CP0_CONFIG

	/* 设置栈指针 */
	la	sp, KERNEL_STACK  /* 加载栈基地址到 sp */
	li	t0, 0x8000        /* 栈大小为 8KB */
	subu	sp, sp, t0       /* sp = 栈基地址 - 栈大小 */

	/* 跳转到 main 函数 */
	la	t9, main         /* 加载 main 函数地址到 t9 寄存器 */
	jr	t9               /* 跳转到 t9 指向的地址 */
	nop                  /* 延迟槽指令 */

loop:
	j	loop             /* 无尽循环,作为默认操作 */
	nop
END(_start)             /* 定义 _start 的结束 */

 在 20221105894-lab目录下 输入

gxemul -E testmips -C R300 -M 64 gxemul/vmlinux

 

Think

Thinking 1.1

也许你会发现我们的 readelf 程序是不能解析之前生成的内核文件(内核文件是可执行文件)的,而我们之后将要介绍的工具 readelf 则可以解析,这是为什么呢?(提示:尝试使用 readelf -h,观察不同)

答:

两者对 ELF 文件格式的处理方式或支持程度不同。通过使用 readelf -h 命令查看文件头信息,可能会发现内核文件的某些属性或格式与我们编写的 readelf 程序预期不匹配。例如,内核文件可能采用了特殊的 ELF 文件格式版本、包含了特定的节(section)或段(segment),而我们的程序没有正确处理这些情况。系统自带的 readelf 工具则具备更全面的兼容性和对各种 ELF 文件变体的正确解析能力。

Thinking 1.2

main 函数在什么地方?我们又是怎么跨文件调用函数的呢?

答:

  • main 函数位置:在 C 语言程序中,main 函数是程序的入口点。在操作系统启动过程中,经过一系列的初始化操作后,最终会跳转到 main 函数开始执行用户空间的程序逻辑。具体来说,在文档所涉及的实验环境中,通过 bootloader 的工作(如在 gxemul 仿真器中,其启动流程简化后会加载内核到内存并跳转执行),最终会将控制权交给内核中的 main 函数(通常位于 init.c 或类似文件中)。
  • 跨文件调用函数:在 C 语言中跨文件调用函数是通过函数声明和链接来实现的。在一个文件中调用另一个文件中的函数时,需要在调用文件中包含被调用函数的声明(可以通过头文件引入声明),以便编译器知道函数的参数和返回值类型等信息。在链接阶段,连接器会将各个目标文件(.o 文件)中定义的函数和变量进行链接,解析函数调用关系,将调用指令与函数的实际实现地址关联起来。例如,如果在一个文件中调用了另一个文件中定义的函数foo,在编译时会生成对foo函数的调用指令,链接器会在链接过程中找到foo函数的定义所在的目标文件,并将调用指令中的地址修正为foo函数的实际入口地址,从而实现跨文件的函数调用。同时,对于函数调用过程中参数的传递和返回值的获取,遵循一定的规则,如在 MIPS 体系结构中,参数通常通过寄存器(a0 - a3 等)传递,返回值通过寄存器(v0 - v1 等)返回,并且在函数调用前后,编译器会根据函数调用约定进行寄存器值的保存和恢复等操作,以确保函数调用的正确性和程序状态的一致性。

总结

标签:文件,函数,ELF,readelf,Boot,地址,lab1,内核,点赞
From: https://blog.csdn.net/m0_74172897/article/details/143845361

相关文章