这次我把所有代码都写出来方便大家复制(可以直接复制我的代码粘贴到终端执行)
开始之前首先先拉取lab1的内容(一定先干这个,不然做不了实验!!!!!!!!!!!!!!!!)
先切换到这个目录下
cd 20221105894-lab
变为:
输入
git pull
git checkout origin/lab1
之后刷新一下你的 学号-lab文件夹,如果改变了就好了,如果没有变为lab1的数据就换成
git checkout lab1
如果还是没变化私信我
做作业直接看实战演练(前提必须把lab1数据获取了!!!)
做作业直接看实战演练(前提必须把lab1数据获取了!!!)
做作业直接看实战演练(前提必须把lab1数据获取了!!!)
做作业直接看实战演练(前提必须把lab1数据获取了!!!)
做作业直接看实战演练(前提必须把lab1数据获取了!!!)
做作业直接看实战演练(前提必须把lab1数据获取了!!!)
一.提取文档内容
- 操作系统启动与内核
- 启动流程与硬件软件关系:操作系统启动是一个复杂过程,涉及硬件和软件相互依存。计算机由硬件和软件组成,操作系统管理硬件资源。硬件需软件控制,软件依赖硬件载入,早期工程师将此纠结过程称为 “bootstrapping”。操作系统内核是核心部分,需与硬件交互,其代码不能在磁盘或易失性内存中,通常置于非易失性存储器(如 ROM 或 FLASH)。但存在存储空间有限、限制多操作系统启动和不利于移植等问题123。
- Bootloader 的作用与阶段:为解决上述问题,引入 Bootloader。它分为 stagel 和 stage2 两部分,stagel 初始化硬件设备,在 ROM 或 FLASH 上运行,为 stage2 准备 RAM 空间并复制代码、设置堆栈后跳转;stage2 在 RAM 中运行,用 C 语言实现复杂功能,包括初始化硬件、加载内核镜像到 RAM、设置启动参数并将控制权转交给内核456。
- gxemul 中的简化流程:在 gxemul 仿真器中,启动流程简化,它提供了 bootloader 功能,可直接加载 elf 格式内核,将内核加载到内存后跳转至入口即可完成启动78。
- 编译链接相关知识
- 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。
- 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 等)返回,并且在函数调用前后,编译器会根据函数调用约定进行寄存器值的保存和恢复等操作,以确保函数调用的正确性和程序状态的一致性。