文章目录:
  1. OS Lab1 内核、启动和PRINTF
    1. OS,启动!
    2. 内核代码结构
      1. (1) Makefile
      2. (2) ELF
        1. 「1」编译处理过程
        2. 「2」ELF是什么
        3. 「3」ELF结构
        4. 「4」 ELF内容解析
      3. (3)MIPS内存布局
      4. (4)Linker Script
    3. _start函数
    4. printk函数
      1. 代码结构
  2. 附录
    1. tools/readelf/elf.h
    2. mmu.h部分
    3. MIPS指令

OS Lab1 内核、启动和PRINTF

OS,启动!

QEMU是一个计算机模拟器,可以模拟CPU等硬件环境,用来运行实验中生成的操作系统可执行文件。

启动流程:加载ELF格式内核到内存,跳转到内核的入口。

内核代码结构

.
├── include              # 系统头文件
│   └── mmu.h            # 里面有内存布局图  
├── init                 # 内核初始化目录
│   └── start.S          # _start 初始化CPU和栈指针 
│   └── init.c           # 各模块的初始化函数  
├── kern                 # 内核主体代码目录,存放内核核心功能模块实现
├── lib                  # 通用库函数目录,提供字符串处理、内存操作、格式化输出等基础函数
├── mk                   
├── out                  # 编译输出目录
├── target               # 目标平台相关目录,存放特定平台/目标系统的文件或产物
│   └── mos              # 当前目标系统相关内容
├── tests                # 测试用例目录,存放本地测试和功能验证代码
├── tools                # 工具目录,不属于MOS,在linux上运行
├── .gitignore           # Git 忽略规则文件,指定无需纳入版本控制的文件
├── .mos-this-lab        # 实验标识文件,用于辅助识别当前实验内容
├── include.mk           # 供 Makefile 引用
├── kernel.lds           # 内核链接脚本,用于指定内核各段的链接布局和加载地址
└── Makefile             # Makefule
(1) Makefile
include include.mk                                 # 部分变量定义在此

# 文件输出定义
target_dir   := target                           # MOS 构建目标所在目录
mos_elf      := $(target_dir)/mos                # 最终需要生成的 ELF 可执行文件
user_disk    := $(target_dir)/fs.img             # MOS 文件系统使用的磁盘镜像文件
link_script  := kernel.lds

# 子模块定义
modules      := lib init kern                    # 需要生成的子模块
objects      := $(addsuffix /*.o, $(modules))    # 遍历需要生成的目标文件

QEMU_FLAGS   := -cpu 4Kc -m 64 -nographic -M malta \
                $(shell [ -f '$(user_disk)' ] && \
                echo '-drive id=ide,file=$(user_disk),if=ide,format=raw') \
                -no-reboot                       # QEMU 运行参数

.PHONY: all $(modules) clean

all: $(mos_elf)                                  # 我们的“最终目标”

$(mos_elf): $(modules)                           # 调用链接器 $(LD) 链接所有目标文件
    $(LD) $(LDFLAGS) -o $(mos_elf) -N -T $(link_script) $(objects)

$(modules):                                      # 进入各个子目录进行 make
    $(MAKE) --directory=$@

clean:
    for d in $(modules); do \
        $(MAKE) --directory=$$d clean; \
    done; \
    rm -rf *.o *~ $(mos_elf)

run:
    $(QEMU) $(QEMU_FLAGS) -kernel $(mos_elf)

先要make子模块(lib init kern…),然后使用链接器链接子模块,大概展开后是这样:

mips-linux-gnu-ld $(LDFLAGS) -o target/mos -N -T kernel.lds lib/*.o init/*.o kern/*.o

可以看到把上一步子模块里面生成的.o文件链接到了一起,使用的链接脚本为上面定义的kernel.lds文件(后面也单独讲了这个文件),最后生成了target/mos这个最终的可执行文件。使用all目标作为make的最终执行结果。

清理:make clean,运行:make run,均为.PHONY目标(即不依赖文件生成时间,每次都会执行)。

这里使用mips-linux-gnu-前缀为交叉编译器,意思是在本地环境(可能x86下)编译,要拿到MIPS模拟器里面运行,即目标代码运行的平台和编译器运行的平台不同。

(2) ELF
「1」编译处理过程

对于含有多个.c文件的工程来说,编译器会首先将所有的.c文件以文件为单位,编译成.o 文件。最后再将所有的.o 文件以及函数库链接在一起,形成最终的可执行文件。

预处理: 仍是C语言文本,把#include头文件复制粘贴,#define定义的宏替换为代码,以及条件编译指令#if等,去掉注释,生成.i文件。

$ gcc -E hello.c -o hello.i

编译: C语言翻译为汇编代码,生成.s文件。

$ gcc -S hello.i -o hello.s

汇编: 将汇编代码转换为机器代码,生成.o目标文件

$ gcc -c hello.s -o hello.o

链接: 和系统库等文件编译到一起,得到可执行文件

「2」ELF是什么

一般有三种类型:

「3」ELF结构

分为这几个部分:

image-20260326154738943

分为几个部分,看图很好理解:

  1. ELF头,包括程序的基本信息,比如体系结构和操作系统,同时也包含了节头表和段头表相对文件的偏移量(offset)。
  2. 段头表(或程序头表),主要包含程序中各个段(segment)的信息, 段的信息需要在运行时刻使用。
  3. 节头表,主要包含程序中各个节(section)的信息,节的信息需要在程序编译和链接的时候使用。
  4. 段头表中的每一个表项,记录了该段数据载入内存时的目标位置等,记录了用于指导应用程序加载的各类信息。
  5. 节头表中的每一个表项,记录了该节程序的代码段、数据段等各个段的内容,主要是链接器在链接的过程中需要使用。

即通过ELF头,我们可以得知段头表、节头表位置,通过这两个表可以查出其中的表项在ELF文件里面的位置,如.text等。

其中节头表主要用于链接阶段,段头表主要用于运行

Exercise 1.1 (只保留补全部分)

/* Exercise 1.1: Your code here. (1/2) */
sh_table = (const char *)binary + ehdr->e_shoff;
sh_entry_count = ehdr->e_shnum;
sh_entry_size = ehdr->e_shentsize;

/* Exercise 1.1: Your code here. (2/2) */
shdr = (const Elf32_Shdr *)((const char *)sh_table + i * sh_entry_size);
addr = shdr->sh_addr;

解析:看elf.h里面各个结构体的定义即可(中文注释见附录)。同时所有的偏移地址按照字节寻址,最终的节头表需要保存为对应结构体指针格式。

「4」 ELF内容解析

使用readelf -l查看一个可执行文件的ELF内容:

image-20260326165434262

各列含义如下:

这块内容为了说明什么?>> 模拟器加载内核的时候,也需要将内核代码、数据加载到对应的位置上,才能保证内核正常运行 >> 所以我们需要知道内核的正确加载位置是哪里。

(3)MIPS内存布局

程序中一般使用的是虚拟地址,通过MMU转为物理地址发往总线访存。

本次实验使用的MIPS虚拟地址空间为4GB,划分为四个大区域:

image-20260326172212211

区域地址范围大小访问权限是否经 TLB是否缓存
kuseg0x00000000 - 0x7FFFFFFF2GB用户态 / 内核态都可用取决于页属性,通常可缓存
kseg00x80000000 - 0x9FFFFFFF512MB仅内核态否,直接映射
kseg10xA0000000 - 0xBFFFFFFF512MB仅内核态否,直接映射
kseg2/30xC0000000 - 0xFFFFFFFF1GB仅内核态取决于页属性,通常可缓存

因为TLB依赖于操作系统,所以OS装在只能使用Kseg0/1,其中Kseg0因为缓存了,速度快一些,所以一般使用kseg0。

实验中需要使用部分地址(见附录mmu.h),.h头文件已经定义好对应的变量了:

#define PAGE_SIZE   4096            // 4KB
#define PTMAP       PAGE_SIZE       // 4KB
#define PDMAP       (4 * 1024 * 1024) // 4MB

#define KERNBASE    0x80020000      // 内核代码起始地址
#define ULIM        0x80000000      // 用户空间上限 / 内核空间起点

#define KSTACKTOP   (ULIM + PDMAP)  // 0x80400000,内核栈顶
#define UVPT        (ULIM - PDMAP)  // 0x7FC00000,用户页表映射区
#define UPAGES      (UVPT - PDMAP)  // 0x7F800000,物理页信息区
#define UENVS       (UPAGES - PDMAP)// 0x7F400000,进程/环境信息区

#define UTOP        UENVS           // 0x7F400000,用户空间顶部
#define UXSTACKTOP  UTOP            // 0x7F400000,用户异常栈顶
#define USTACKTOP   (UTOP - 2 * PTMAP) // 0x7F3FE000,普通用户栈顶

#define UTEXT       PDMAP           // 0x00400000,用户程序入口
#define UCOW        (UTEXT - PTMAP) // 0x003FF000,写时复制保留页
#define UTEMP       (UCOW - PTMAP)  // 0x003FE000,临时映射页
(4)Linker Script

在不同的设备上使用链接器ld时,由于不同设备加载地址不同,所以需要Linker Script记录各个节应该如何映射到段,以及各个段应该被加载到的位置。

其基本格式:

SECTIONS
{
    . = 0x10000;
    .text : { *(.text) }
    . = 0x8000000;
    .data : { *(.data) }
    .bss : { *(.bss) }
}

其中,.代表当前位置,即接下来的内容从什么位置开始放起。

而第二行:定义一个输出段.text,把所有输入文件里的 .text 段都放进来。

后面的一样。如果没写.,则满足对齐要求后,接着往后排。

应该只用会这两个语法。

Exercise 1.2

/* Step 1: Set the loading address of the text section to the location counter ".". */
/* Exercise 1.2: Your code here. (1/4) */
. = 0x80020000;
/* Step 2: Define the text section. */
/* Exercise 1.2: Your code here. (2/4) */
.text : { *(.text) }
/* Step 3: Define the data section. */
/* Exercise 1.2: Your code here. (3/4) */
.data : { *(.data) }
bss_start = .;
/* Step 4: Define the bss section. */
/* Exercise 1.2: Your code here. (4/4) */
.bss : { *(.bss) }
bss_end = .;

第一个去找内核启动地址(mmu.h),应该可以替换为KERNBASE。后面的那不是和前面例子一模一样吗()

另外,

  • bss_start.bss 段起始地址
  • bss_end.bss 段结束地址

gpt说是用于方便给.bss段清零。

_start函数

Exercise 1.3
#include <asm/asm.h>
#include <mmu.h>

.text
EXPORT(_start)
.set at
.set reorder
/* Lab 1 Key Code "enter-kernel" */
    /* clear .bss segment */
    la      v0, bss_start
    la      v1, bss_end
clear_bss_loop:
    beq     v0, v1, clear_bss_done
    sb      zero, 0(v0)
    addiu   v0, v0, 1
    j       clear_bss_loop
/* End of Key Code "enter-kernel" */

clear_bss_done:
    /* disable interrupts */
    mtc0    zero, CP0_STATUS

    /* hint: you can refer to the memory layout in include/mmu.h */
    /* set up the kernel stack */
    /* Exercise 1.3: Your code here. (1/2) */
    li        sp, 0x80400000
    /* jump to mips_init */
    /* Exercise 1.3: Your code here. (2/2) */
    jal     mips_init

这个函数内主要做了这么几件事:首先是_start函数的声明,随后2、3行的.set允许汇编器使用at寄存器,也允许对接下来的代码进行重排序。接下来直到clear_bss_done标签的代码对.bss段进行了清零操作。 而最后一行代码则禁用了外部中断。

要首先将sp寄存器设置到内核栈空间的位置上,随后跳转到mips_init函数。

栈指针的位置在mmu.h中可以看到,直接赋值即可。然后jal跳转到mips_init地址即可。

printk函数

代码结构

附录

tools/readelf/elf.h

/* 本文件定义了标准 ELF 类型、结构体和宏。 */

#ifndef _ELF_H
#define _ELF_H

#include <stdint.h>

/* 来自 GNU C Library 的 ELF 定义文件。我们为了实验对该文件做了简化,
 * 移除了 ELF64、以及我们不关心的结构体和枚举定义。
 */

/* 16 位数据类型。 */
typedef uint16_t Elf32_Half;

/* 有符号和无符号 32 位数据类型。 */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;

/* 有符号和无符号 64 位数据类型。 */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;

/* 地址类型。 */
typedef uint32_t Elf32_Addr;

/* 文件偏移类型。 */
typedef uint32_t Elf32_Off;

/* 节索引类型,为 16 位数据。 */
typedef uint16_t Elf32_Section;

/* 符号索引类型。 */
typedef uint32_t Elf32_Symndx;

/* 实验 1 关键代码 "readelf-struct-def" */

/* ELF 文件头。它出现在每个 ELF 文件的开头。 */

#define EI_NIDENT (16)

typedef struct {
    unsigned char e_ident[EI_NIDENT]; /* 魔数及其他信息 */
    Elf32_Half e_type;          /* 目标文件类型 */
    Elf32_Half e_machine;          /* 体系结构 */
    Elf32_Word e_version;          /* 目标文件版本 */
    Elf32_Addr e_entry;          /* 入口点虚拟地址 */
    Elf32_Off e_phoff;          /* 程序头表在文件中的偏移 */
    Elf32_Off e_shoff;          /* 节头表在文件中的偏移 */
    Elf32_Word e_flags;          /* 处理器特定标志 */
    Elf32_Half e_ehsize;          /* ELF 头大小(字节) */
    Elf32_Half e_phentsize;          /* 程序头表项大小 */
    Elf32_Half e_phnum;          /* 程序头表项数量 */
    Elf32_Half e_shentsize;          /* 节头表项大小 */
    Elf32_Half e_shnum;          /* 节头表项数量 */
    Elf32_Half e_shstrndx;          /* 节头字符串表索引 */
} Elf32_Ehdr;

/* e_ident 数组中的各字段。EI_* 宏是该数组中的索引。
   每个 EI_* 宏下面定义的宏,是该字节可能取的值。 */

#define EI_MAG0 0    /* 文件标识第 0 字节索引 */
#define ELFMAG0 0x7f /* 魔数字节 0 */

#define EI_MAG1 1   /* 文件标识第 1 字节索引 */
#define ELFMAG1 'E' /* 魔数字节 1 */

#define EI_MAG2 2   /* 文件标识第 2 字节索引 */
#define ELFMAG2 'L' /* 魔数字节 2 */

#define EI_MAG3 3   /* 文件标识第 3 字节索引 */
#define ELFMAG3 'F' /* 魔数字节 3 */

/* 节头。 */
typedef struct {
    Elf32_Word sh_name;     /* 节名称 */
    Elf32_Word sh_type;     /* 节类型 */
    Elf32_Word sh_flags;     /* 节标志 */
    Elf32_Addr sh_addr;     /* 节地址 */
    Elf32_Off sh_offset;     /* 节偏移 */
    Elf32_Word sh_size;     /* 节大小 */
    Elf32_Word sh_link;     /* 节链接 */
    Elf32_Word sh_info;     /* 节额外信息 */
    Elf32_Word sh_addralign; /* 节对齐 */
    Elf32_Word sh_entsize;     /* 节表项大小 */
} Elf32_Shdr;

/* 程序段头。 */

typedef struct {
    Elf32_Word p_type;   /* 段类型 */
    Elf32_Off p_offset;  /* 段在文件中的偏移 */
    Elf32_Addr p_vaddr;  /* 段虚拟地址 */
    Elf32_Addr p_paddr;  /* 段物理地址 */
    Elf32_Word p_filesz; /* 段在文件中的大小 */
    Elf32_Word p_memsz;  /* 段在内存中的大小 */
    Elf32_Word p_flags;  /* 段标志 */
    Elf32_Word p_align;  /* 段对齐 */
} Elf32_Phdr;
/* 关键代码 "readelf-struct-def" 结束 */

/* p_type(段类型)的合法取值。 */

#define PT_NULL 0         /* 未使用的程序头表项 */
#define PT_LOAD 1         /* 可装载程序段 */
#define PT_DYNAMIC 2         /* 动态链接信息 */
#define PT_INTERP 3         /* 程序解释器 */
#define PT_NOTE 4         /* 辅助信息 */
#define PT_SHLIB 5         /* 保留 */
#define PT_PHDR 6         /* 头表自身的表项 */
#define PT_NUM 7         /* 已定义类型的数量。 */
#define PT_LOOS 0x60000000   /* 操作系统特定范围起始 */
#define PT_HIOS 0x6fffffff   /* 操作系统特定范围结束 */
#define PT_LOPROC 0x70000000 /* 处理器特定范围起始 */
#define PT_HIPROC 0x7fffffff /* 处理器特定范围结束 */

/* p_flags(段标志)的合法取值。 */

#define PF_X (1 << 0)           /* 段可执行 */
#define PF_W (1 << 1)           /* 段可写 */
#define PF_R (1 << 2)           /* 段可读 */
#define PF_MASKPROC 0xf0000000 /* 处理器特定 */

#endif /* _ELF_H */

mmu.h部分

 o     4G ----------->  +----------------------------+------------0x100000000
 o                      |       ...                  |  kseg2
 o      KSEG2    -----> +----------------------------+------------0xc000 0000
 o                      |          Devices           |  kseg1
 o      KSEG1    -----> +----------------------------+------------0xa000 0000
 o                      |      Invalid Memory        |   /|\
 o                      +----------------------------+----|-------Physical Memory Max
 o                      |       ...                  |  kseg0
 o      KSTACKTOP-----> +----------------------------+----|-------0x8040 0000-------end
 o                      |       Kernel Stack         |    | KSTKSIZE            /|\
 o                      +----------------------------+----|------                |
 o                      |       Kernel Text          |    |                    PDMAP
 o      KERNBASE -----> +----------------------------+----|-------0x8002 0000    |
 o                      |      Exception Entry       |   \|/                    \|/
 o      ULIM     -----> +----------------------------+------------0x8000 0000-------
 o                      |         User VPT           |     PDMAP                /|\
 o      UVPT     -----> +----------------------------+------------0x7fc0 0000    |
 o                      |           pages            |     PDMAP                 |
 o      UPAGES   -----> +----------------------------+------------0x7f80 0000    |
 o                      |           envs             |     PDMAP                 |
 o  UTOP,UENVS   -----> +----------------------------+------------0x7f40 0000    |
 o  UXSTACKTOP -/       |     user exception stack   |     PTMAP                 |
 o                      +----------------------------+------------0x7f3f f000    |
 o                      |                            |     PTMAP                 |
 o      USTACKTOP ----> +----------------------------+------------0x7f3f e000    |
 o                      |     normal user stack      |     PTMAP                 |
 o                      +----------------------------+------------0x7f3f d000    |
 a                      |                            |                           |
 a                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                           |
 a                      .                            .                           |
 a                      .                            .                         kuseg
 a                      .                            .                           |
 a                      |~~~~~~~~~~~~~~~~~~~~~~~~~~~~|                           |
 a                      |                            |                           |
 o       UTEXT   -----> +----------------------------+------------0x0040 0000    |
 o                      |      reserved for COW      |     PTMAP                 |
 o       UCOW    -----> +----------------------------+------------0x003f f000    |
 o                      |   reversed for temporary   |     PTMAP                 |
 o       UTEMP   -----> +----------------------------+------------0x003f e000    |
 o                      |       invalid memory       |                          \|/
 a     0 ------------>  +----------------------------+ ----------------------------

MIPS指令

类别指令格式作用备注
算术addadd rd, rs, rtrd = rs + rt有符号加法,检查溢出
算术adduaddu rd, rs, rtrd = rs + rt不检查溢出
算术addiaddi rt, rs, immrt = rs + imm立即数加法
算术addiuaddiu rt, rs, immrt = rs + imm不检查溢出
算术subsub rd, rs, rtrd = rs - rt有符号减法
算术subusubu rd, rs, rtrd = rs - rt不检查溢出
算术mulmul rd, rs, rt乘法结果低位放 rd有些环境支持
算术multmult rs, rt乘积放入 HI/LO高位在 HI,低位在 LO
算术multumultu rs, rt无符号乘法结果放 HI/LO
算术divdiv rs, rt商到 LO,余数到 HI有符号除法
算术divudivu rs, rt商到 LO,余数到 HI无符号除法
特殊寄存器mflomflo rdrd = LO取乘除法结果的商/低位
特殊寄存器mfhimfhi rdrd = HI取乘除法余数/高位
特殊寄存器mtlomtlo rsLO = rsLO
特殊寄存器mthimthi rsHI = rsHI
逻辑andand rd, rs, rt按位与
逻辑andiandi rt, rs, imm按位与立即数常用于掩码
逻辑oror rd, rs, rt按位或
逻辑oriori rt, rs, imm按位或立即数常配合 lui
逻辑xorxor rd, rs, rt按位异或
逻辑xorixori rt, rs, imm按位异或立即数
逻辑nornor rd, rs, rt按位或后取反常用于实现 not
移位sllsll rd, rt, shamt逻辑左移低位补 0
移位srlsrl rd, rt, shamt逻辑右移高位补 0
移位srasra rd, rt, shamt算术右移高位补符号位
移位sllvsllv rd, rt, rs可变左移位数在 rs
移位srlvsrlv rd, rt, rs可变逻辑右移
移位sravsrav rd, rt, rs可变算术右移
比较sltslt rd, rs, rtrs < rt,则 rd=1有符号比较
比较sltusltu rd, rs, rtrs < rt,则 rd=1无符号比较
比较sltislti rt, rs, immrs < immrt=1有符号比较
比较sltiusltiu rt, rs, immrs < immrt=1无符号比较
访存lwlw rt, offset(base)读一个字到寄存器4 字节
访存swsw rt, offset(base)把一个字写入内存4 字节
访存lblb rt, offset(base)读一个字节符号扩展
访存lbulbu rt, offset(base)读一个字节零扩展
访存sbsb rt, offset(base)写一个字节
访存lhlh rt, offset(base)读半字符号扩展,2 字节
访存lhulhu rt, offset(base)读半字零扩展
访存shsh rt, offset(base)写半字2 字节
访存llll rt, offset(base)原子读配合 sc
访存scsc rt, offset(base)原子写配合 ll
跳转/分支beqbeq rs, rt, label若相等则跳转常用于 if
跳转/分支bnebne rs, rt, label若不等则跳转
跳转/分支bgezbgez rs, labelrs >= 0 则跳转和 0 比较
跳转/分支bgtzbgtz rs, labelrs > 0 则跳转
跳转/分支blezblez rs, labelrs <= 0 则跳转
跳转/分支bltzbltz rs, labelrs < 0 则跳转
跳转/分支jj label无条件跳转
跳转/分支jaljal label跳转并保存返回地址返回地址进 $ra
跳转/分支jrjr rs跳到寄存器中的地址常写 jr $ra
跳转/分支jalrjalr rd, rs间接调用常用于函数指针
数据装入luilui rt, imm立即数放高 16 位低 16 位补 0
伪指令lili rt, imm装立即数伪指令
伪指令lala rt, label装地址伪指令
伪指令movemove rd, rsrd = rs常等价于 addu rd, rs, $zero
伪指令negneg rd, rsrd = -rs伪指令
伪指令notnot rd, rs按位取反常等价于 nor rd, rs, $zero
伪指令nopnop空操作常等价于 sll $zero, $zero, 0
系统调用syscallsyscall执行系统调用服务号放 $v0

image-20260326195253005