![](https://i-blog.csdnimg.cn/blog_migrate/309e7c3bddcafacce8901bb82ac5f0d6.png)
Info: 普典衡 21020007074
Date: 2023-12-02
一、实验要求(
10%
)
1.
从操作系统角度理解
MIPS
体系结构
2.
掌握操作系统启动的基本流程
3.
掌握
ELF
文件的结构和功能
二、实验内容及步骤(
80%
)
![](https://i-blog.csdnimg.cn/blog_migrate/439007b8d42d5475dbdf05de2278ba4f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1bdf801c39933128510884cb1c95f12e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c931ab0eb32e4256e989f0d5adfb7bb7.png)
把include.mk中的
/opt/eldk/usr/bin/mips-4kc-
改成
/OSLAB/compiler/usr/bin/mips_4KC-
![](https://i-blog.csdnimg.cn/blog_migrate/ef466e24db6b9ce9670a2b1d41564c74.png)
查看
gxemul
文件夹,该目录下生成了 vmlinux 内核文件
![](https://i-blog.csdnimg.cn/blog_migrate/6f26f80e7f6ae2b09a43b52e60d5f60a.png)
1. ELF
定义:
-ELF(
Executable and Linkable Format
)是一种文件格式,主要用于
Linux
平台。它用于存储各种类 型的文件,包括可执行文件、可重定位文件(通常是 `.o`
文件)、共享对象文件(如
`.so`
文件)和核 心转储文件。在 Windows
平台中,类似的格式是
PE/COFF
。
2. ELF
文件头:
-ELF 文件的起始位置是文件头,包含关于文件的静态信息。这些信息可以通过
`readelf`
命令查看。 文件头的结构和参数在 `/usr/include/elf.h`
中定义,分为
32
位版本(
Elf32_Ehdr
)和
64
位版本 (Elf64_Ehdr
),这两个版本在大多数内容上是相似的,结构如下所示:
typedef struct {
Elf32_Half e_type;//Elf 文件类型
Elf32_Half e_machine;//ELF 文件的 CPU 平台属性
Elf32_Word e_version;//ELF 版本信息
Elf32_Addr e_entry;//入口地址
Elf32_Off e_phoff;//程序头表的文件偏移(以字节为单位)。如果文件没有程序头表,则此成员值为零。
Elf32_Off e_shoff;//节头表的文件偏移(以字节为单位)。如果文件没有节头表,则此成员值为零。
Elf32_Word e_flags;//与文件关联的特定于处理器的标志。标志名称采用 EF_machine_flag 形式。
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;
2
补全全
readelf.c
中代码
![](https://i-blog.csdnimg.cn/blog_migrate/b780bfd8f6ee8f91e368fc6bdf270c59.png)
// 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++;
}
编译运行结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/e5816038711e8dc944284bdae9d1652c.png)
之前生成的 vmlinux 内核文件为大端存储
big endian
,而
testELF
为小端存储
little endian
。
readelf
程序只 能解析后者而不能解析前者,因为 readelf
程序使用
c
语言不能简单的读取大端存储的数据。
![](https://i-blog.csdnimg.cn/blog_migrate/937be92b8d3a9e99170be84dddbb74fd.png)
补全
scse_03.lds
:
![](https://i-blog.csdnimg.cn/blog_migrate/8b82bf10b75a2bebe53ed4535d6e889a.png)
OUTPUT_ARCH(mips)
/*
Set the architecture to mips.
*/
ENTRY(_start)
/*
Set the ENTRY point of the program to _start.
*/
SECTIONS
{
. = 0x80010000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
/* To do:
fill in the correct address of the key sections
such as text, data, bss...
*/
end = . ;
}
生成
vmlinux
后查看各
section
地址:
![](https://i-blog.csdnimg.cn/blog_migrate/ec2938ad7b5fcebe57063bb6119697c6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8fc5f8846249d3241aee8c4bc3daed66.png)
在本次实验中,内核入口处设定的起始地址是 0x80000000
,而
main
函数的位置在
0x80010000
。入口函 数(本例中为 start.S
)内部利用
`jal`
跳转指令来跳转至特定的函数地址。实现跨文件的函数调用时,这 种跳转通常先要求将数据存入栈中,随后再执行跳转。
![](https://i-blog.csdnimg.cn/blog_migrate/5a7b77f94673900dc9dd45ee0bfcba53.png)
补全
boot/start.S
:
![](https://i-blog.csdnimg.cn/blog_migrate/f7072645e25f429f52abd355c374781c.png)
/*To do:
set up stack
you can reference the memory layout in the include/mmu.h
*/
li sp, 0x80400000
jal main
nop
重新
make
,执行
gxemul -E testmips -C R3000 -M64 gxemul/vmlinux
命令:
![](https://i-blog.csdnimg.cn/blog_migrate/4596a00ee1161e7c95886916b5ea6fc1.png)
![](https://i-blog.csdnimg.cn/blog_migrate/ee6dc73aaea7c3e147296de91fc8a11d.png)
补全
lib/print.c
中的
Ip_Print()
函数中缺失部分:
![](https://i-blog.csdnimg.cn/blog_migrate/b2d428b1371b7c10633c03d13fde9bbe.png)
![](https://i-blog.csdnimg.cn/blog_migrate/49fdf09bcf98ddcedbf5a8da47b5c73e.png)
for (;;) {
{
/* scan for the next '%' */
while ((*fmt) != '%' && (*fmt) != '\0') {
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 == '-') {
ladjust = 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 == 'l') {
longFlag = 1;
fmt++;
}
longFlag = 0;
width = 0;
ladjust = 0;
prec = 0;
padc = ' ';
negFlag = 0;
再次输入 gxemul -E testmips -C R3000 -M64 gxemul/vmlinux
![](https://i-blog.csdnimg.cn/blog_migrate/288c3e6d0496fe6897f6457022f00202.png)
Push lab1 分支:
![](https://i-blog.csdnimg.cn/blog_migrate/52ffb382feb0f1a7618c07f88d0d062f.png)
通过
![](https://i-blog.csdnimg.cn/blog_migrate/44279405f1c7239400688faf5c840ddd.png)
三、心得总结(写出自己在完成实验过程中遇到的问题、解决方法,以及体会、收获等)
(10%)
遇到的问题与解决方法:
1.
交叉编译器路径的配置:
开始时遇到交叉编译器路径不正确的问题。通过修改
include.mk
文件中的
CROSS_COMPILE
变量为正确的路径,解决了这个问题。
2.
理解并操作
ELF
文件:
ELF
文件的复杂结构最初让我感到困惑。通过研究
readelf
工具和
ELF
文件的头部结构,我逐渐
理解了它们的组成部分和功能。
3.
内核文件的大端和小端问题:
尝试解析不同格式的
ELF
文件时,发现
readelf
程序不能解析大端存储的
vmlinux
文件。我了解
到这是由于
C
语言处理字节顺序的方式造成的,进而理解了大端和小端存储格式的差异。
4.
编写和调试汇编代码:
在补全
boot/start.S
时,学会了如何编写
MIPS
汇编代码,并理解了汇编层面的函数跳转。
5.
编译和调试:
整个实验过程中,我反复进行编译、测试和调试,以确保每一部分都能正确工作。
体会和收获:
1.
深入理解
MIPS
架构:
通过这次实验,我对
MIPS
架构有了更深入的理解,包括其指令集和内存管理方式。
2.
操作系统启动过程的认识:
学习了操作系统启动的基本流程,特别是
bootloader
的作用以及如何将控制权从
bootloader 转移
到操作系统内核。
3. ELF
文件格式的深入了解:
对
ELF
文件格式的研究让我更好地理解了
Linux
下的可执行文件是如何被组织和管理的。
4.
实践编程技能:
实验过程中的编码和调试工作提升了我的编程能力,特别是在处理底层系统层面的编程时。
5.
解决问题的能力:
面对各种挑战和问题,我学会了如何系统地分析问题、查找资料并应用知识来找到解决方案。更加自信地面对未来可能遇到的技术挑战。