这次我把所有代码都写出来方便大家复制(可以直接复制我的代码粘贴到终端执行)
开始之前首先先拉取lab1的内容(一定先干这个,不然做不了实验!!!!!!!!!!!!!!!!)
先切换到这个目录下
cd 20221105894-lab
变为:
输入
git pull
git checkout lab1
之后刷新一下你的 学号-lab文件夹,如果改变了就好了,如果没有变为lab1的数据就换成
lab1实验需要Exercise都做!!!!!!
lab1实验需要Exercise都做!!!!!!
lab1实验需要Exercise都做!!!!!!
lab1实验需要Exercise都做!!!!!!
lab1实验需要Exercise都做!!!!!!
一.提取文档内容
- 操作系统启动与内核
- 启动流程与硬件软件关系:操作系统启动是一个复杂过程,涉及硬件和软件相互依存。计算机由硬件和软件组成,操作系统管理硬件资源。硬件需软件控制,软件依赖硬件载入,早期工程师将此纠结过程称为 “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 ( zlike@cse.buaa.edu.cn )
CROSS_COMPILE := /OSLAB/compiler/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<wlm199558@126.com>
*/
#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.
shdr=(Elf32_Shdr *)(binary + ehdr -> e_shoff);
sh_entry_count=ehdr -> e_shnum;
sh_entry_size=ehdr -> e_shentsize;
// for each section header, output section number and section addr.
for(Nr = 0;Nr < sh_entry_count;++Nr){
printf("%d:0x%x\n",Nr,shdr -> sh_addr);
shdr++;
}
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 ...
*/
. = 0x80010000;
.text : {
*(.text) /* 包含所有 .text 段的数据 */
}
.data : {
*(.data) /* 包含所有 .data 段的数据 */
}
.bss : {
*(.bss) /* 包含所有 .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
.text
LEAF(_start) /*LEAF is defined in asm.h and LEAF functions don't call other functions*/
.set mips2 /*.set is used to instruct how the assembler works and control the order of instructions */
.set reorder
/* Disable interrupts */
mtc0 zero, CP0_STATUS
/* Disable watch exception. */
mtc0 zero, CP0_WATCHLO
mtc0 zero, CP0_WATCHHI
/* disable kernel mode cache */
mfc0 t0, CP0_CONFIG
and t0, ~0x7
ori t0, 0x2
mtc0 t0, CP0_CONFIG
/*To do:
set up stack
you can reference the memory layout in the include/mmu.h
*/
li sp,0x80400000
jal main
nop
loop:
j loop
nop
END(_start) /*the function defined in asm.h*/
Exercise 1.5
阅读相关代码和下面对于函数规格的说明,补全 lib/print.c中Ip_Print()函数中缺失的部分来实现字符输出。
/*
* Copyright (C) 2001 MontaVista Software Inc.
* Author: Jun Sun, jsun@mvista.com or jsun@junsun.net
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
*/
#include <print.h>
/* macros */
#define IsDigit(x) ( ((x) >= '0') && ((x) <= '9') )
#define Ctod(x) ( (x) - '0')
/* forward declaration */
extern int PrintChar(char *, char, int, int);
extern int PrintString(char *, char *, int, int);
extern int PrintNum(char *, unsigned long, int, int, int, int, char, int);
/* private variable */
static const char theFatalMsg[] = "fatal error in lp_Print!";
/* -*-
* A low level printf() function.
*/
void
lp_Print(void (*output)(void *, char *, int),
void * arg,
char *fmt,
va_list ap)
{
#define OUTPUT(arg, s, l) \
{ if (((l) < 0) || ((l) > LP_MAX_BUF)) { \
(*output)(arg, (char*)theFatalMsg, sizeof(theFatalMsg)-1); for(;;); \
} else { \
(*output)(arg, s, l); \
} \
}
char buf[LP_MAX_BUF];
char c;
char *s;
long int num;
int longFlag;
int negFlag;
int width;
int prec;
int ladjust;
char padc;
int length;
for(;;) {
{
/* scan for the next '%' */
while((*fmt)!='\0'&&(*fmt)!='%')
{
OUTPUT(arg,fmt,1);
fmt ++;
}
/* flush the string found so far */
/* are we hitting the end? */
if((*fmt)=='\0')
break;
}
/* we found a '%' */
fmt++;
/* check for long */
/* check for other prefixes */
/* check format flag */
if(*fmt=='-'){
longFlag=1;
fmt++;
}
else if(*fmt=='0')
{
padc='0';
fmt ++;
}
for(;IsDigit(*fmt);fmt ++)width =width*10+Ctod(*fmt);
if(*fmt=='.'){
fmt ++;
for(;IsDigit(*fmt);fmt++)
prec=prec*10+Ctod(*fmt);
}
if(*fmt=='1'){
longFlag=1;
fmt++;
}
longFlag=0;
width=0;
ladjust=0;
prec=0;
padc=' ';
negFlag = 0;
switch (*fmt) {
case 'b':
if (longFlag) {
num = va_arg(ap, long int);
} else {
num = va_arg(ap, int);
}
length = PrintNum(buf, num, 2, 0, width, ladjust, padc, 0);
OUTPUT(arg, buf, length);
break;
case 'd':
case 'D':
if (longFlag) {
num = va_arg(ap, long int);
} else {
num = va_arg(ap, int);
}
if (num < 0) {
num = - num;
negFlag = 1;
}
length = PrintNum(buf, num, 10, negFlag, width, ladjust, padc, 0);
OUTPUT(arg, buf, length);
break;
case 'o':
case 'O':
if (longFlag) {
num = va_arg(ap, long int);
} else {
num = va_arg(ap, int);
}
length = PrintNum(buf, num, 8, 0, width, ladjust, padc, 0);
OUTPUT(arg, buf, length);
break;
case 'u':
case 'U':
if (longFlag) {
num = va_arg(ap, long int);
} else {
num = va_arg(ap, int);
}
length = PrintNum(buf, num, 10, 0, width, ladjust, padc, 0);
OUTPUT(arg, buf, length);
break;
case 'x':
if (longFlag) {
num = va_arg(ap, long int);
} else {
num = va_arg(ap, int);
}
length = PrintNum(buf, num, 16, 0, width, ladjust, padc, 0);
OUTPUT(arg, buf, length);
break;
case 'X':
if (longFlag) {
num = va_arg(ap, long int);
} else {
num = va_arg(ap, int);
}
length = PrintNum(buf, num, 16, 0, width, ladjust, padc, 1);
OUTPUT(arg, buf, length);
break;
case 'c':
c = (char)va_arg(ap, int);
length = PrintChar(buf, c, width, ladjust);
OUTPUT(arg, buf, length);
break;
case 's':
s = (char*)va_arg(ap, char *);
length = PrintString(buf, s, width, ladjust);
OUTPUT(arg, buf, length);
break;
case '\0':
fmt --;
break;
default:
/* output this char as it is */
OUTPUT(arg, fmt, 1);
break;
} /* switch (*fmt) */
fmt ++;
} /* for(;;) */
/* special termination call */
OUTPUT(arg, "\0", 1);
}
/* --------------- local help functions --------------------- */
int
PrintChar(char * buf, char c, int length, int ladjust)
{
int i;
if (length < 1) length = 1;
if (ladjust) {
*buf = c;
for (i=1; i< length; i++) buf[i] = ' ';
} else {
for (i=0; i< length-1; i++) buf[i] = ' ';
buf[length - 1] = c;
}
return length;
}
int
PrintString(char * buf, char* s, int length, int ladjust)
{
int i;
int len=0;
char* s1 = s;
while (*s1++) len++;
if (length < len) length = len;
if (ladjust) {
for (i=0; i< len; i++) buf[i] = s[i];
for (i=len; i< length; i++) buf[i] = ' ';
} else {
for (i=0; i< length-len; i++) buf[i] = ' ';
for (i=length-len; i < length; i++) buf[i] = s[i-length+len];
}
return length;
}
int
PrintNum(char * buf, unsigned long u, int base, int negFlag,
int length, int ladjust, char padc, int upcase)
{
/* algorithm :
* 1. prints the number from left to right in reverse form.
* 2. fill the remaining spaces with padc if length is longer than
* the actual length
* TRICKY : if left adjusted, no "0" padding.
* if negtive, insert "0" padding between "0" and number.
* 3. if (!ladjust) we reverse the whole string including paddings
* 4. otherwise we only reverse the actual string representing the num.
*/
int actualLength =0;
char *p = buf;
int i;
do {
int tmp = u %base;
if (tmp <= 9) {
*p++ = '0' + tmp;
} else if (upcase) {
*p++ = 'A' + tmp - 10;
} else {
*p++ = 'a' + tmp - 10;
}
u /= base;
} while (u != 0);
if (negFlag) {
*p++ = '-';
}
/* figure out actual length and adjust the maximum length */
actualLength = p - buf;
if (length < actualLength) length = actualLength;
/* add padding */
if (ladjust) {
padc = ' ';
}
if (negFlag && !ladjust && (padc == '0')) {
for (i = actualLength-1; i< length-1; i++) buf[i] = padc;
buf[length -1] = '-';
} else {
for (i = actualLength; i< length; i++) buf[i] = padc;
}
/* prepare to reverse the string */
{
int begin = 0;
int end;
if (ladjust) {
end = actualLength - 1;
} else {
end = length -1;
}
while (end > begin) {
char tmp = buf[begin];
buf[begin] = buf[end];
buf[end] = tmp;
begin ++;
end --;
}
}
/* adjust the string pointer */
return length;
}
在 20221105894-lab目录下 输入
make
gxemul -E testmips -C R3000 -M 64 gxemul/vmlinux
输出这个即可
最后记得提交
git add .
git commit -m '这个谁便写,不一定写我的作业写完了'
git push
最后出现这个就可以
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 等)返回,并且在函数调用前后,编译器会根据函数调用约定进行寄存器值的保存和恢复等操作,以确保函数调用的正确性和程序状态的一致性。