Lab1_实验报告

一、思考题

Thinking 1.1

  • 尝试分别使用实验环境中的原生 x86 工具链和 MIPS 交叉编译工具链,重复其中的编译和解析过程,观察相应的结果

(1)原生 x86 工具链

1.只进行预处理gcc -E hello.c,这 里一阶段并没有 printf 这一函数的定义

1
2
3
4
5
6
7
8
/* 保留部分输出 */
extern int printf (const char *__restrict __format, ...);
# 2 "hello.c"
int main()
{
printf("Hello World!\n");
return 0;
}

2.只编译而不链接 gcc -c hello.c 并进行反汇编 objdump -DS hello.o,printf 的具体实现依然不在我们的程序中

1
2
3
4
5
6
7
8
9
10
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <main+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: 5d pop %rbp
1d: c3 ret

3.正常编译gcc hello.c -o hello 并进行反汇编 objdump -DS hello ,call后填入地址,被标记为 puts@plt的位置

1
2
3
4
5
6
7
8
9
10
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1158: 48 89 c7 mov %rax,%rdi
115b: e8 f0 fe ff ff call 1050 <puts@plt>
1160: b8 00 00 00 00 mov $0x0,%eax
1165: 5d pop %rbp
1166: c3 ret

(2)MIPS 交叉编译工具链

1.mips-linux-gnu-gcc -E hello.c 结果同上

2.mips-linux-gnu-gcc -c hello.cmips-linux-gnu-objdump -DS hello.o

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
00000000 <main>:
0: 27bdffe0 addiu sp,sp,-32
4: afbf001c sw ra,28(sp)
8: afbe0018 sw s8,24(sp)
c: 03a0f025 move s8,sp
10: 3c1c0000 lui gp,0x0
14: 279c0000 addiu gp,gp,0
18: afbc0010 sw gp,16(sp)
1c: 3c020000 lui v0,0x0
20: 24440000 addiu a0,v0,0
24: 8f820000 lw v0,0(gp)
28: 0040c825 move t9,v0
2c: 0320f809 jalr t9
30: 00000000 nop
34: 8fdc0010 lw gp,16(s8)
38: 00001025 move v0,zero
3c: 03c0e825 move sp,s8
40: 8fbf001c lw ra,28(sp)
44: 8fbe0018 lw s8,24(sp)
48: 27bd0020 addiu sp,sp,32
4c: 03e00008 jr ra
50: 00000000 nop
...

3. mips-linux-gnu-gcc hello.c -o hellomips-linux-gnu-objdump -DS hello

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
004006e0 <main>:
4006e0: 27bdffe0 addiu sp,sp,-32
4006e4: afbf001c sw ra,28(sp)
4006e8: afbe0018 sw s8,24(sp)
4006ec: 03a0f025 move s8,sp
4006f0: 3c1c0042 lui gp,0x42
4006f4: 279c9010 addiu gp,gp,-28656
4006f8: afbc0010 sw gp,16(sp)
4006fc: 3c020040 lui v0,0x40
400700: 24440830 addiu a0,v0,2096
400704: 8f828030 lw v0,-32720(gp)
400708: 0040c825 move t9,v0
40070c: 0320f809 jalr t9
400710: 00000000 nop
400714: 8fdc0010 lw gp,16(s8)
400718: 00001025 move v0,zero
40071c: 03c0e825 move sp,s8
400720: 8fbf001c lw ra,28(sp)
400724: 8fbe0018 lw s8,24(sp)
400728: 27bd0020 addiu sp,sp,32
40072c: 03e00008 jr ra
400730: 00000000 nop
...

2和3的主要区别在于gp、v0、t9寄存器的值

在链接阶段,全局数据的布局可能会发生变化。因此,在链接时,gp 寄存器的值可能会被重新计算

此时v0的值改变主要是为了改变t9的值,也就是跳转的地址,在jalr t9步骤完成printf的调用

  • 解释其中向 objdump 传入的参数 的含义
1
2
3
4
-D, --disassemble-all    Display assembler contents of all sections
--disassemble=<sym> Display assembler contents from <sym>
-S, --source Intermix source code with disassembly
--source-comment[=<txt>] Prefix lines of source code with <txt>

所以objdump -DS ...... 中传入参数含义是显示所有section的反汇编,并反汇编出源代码,将源代码与汇编代码混合显示

Thinking 1.2

  • 尝试使用我们编写的 readelf 程序,解析之前在 target 目录下生成的内核 ELF 文 件

    图片

  • 也许你会发现我们编写的 readelf 程序是不能解析 readelf 文件本身的,而我们刚 才介绍的系统工具 readelf 则可以解析,这是为什么呢?(提示:尝试使用 readelf -h,并阅读 tools/readelf 目录下的 Makefile,观察 readelf 与 hello 的不同)

    使用readelf-h来 显示 ELF 文件头,发现hello的类别是ELF32 ,readelf的类别是ELF64

    图片

    图片

阅读 tools/readelf 目录下的Makefile, 使用了 -m32 参数来生成 32 位的hello可执行文件

图片

我们编写的readelf程序使用的数据类型均为32位,只能解析32位的elf文件。

而我们编写的readelf文件是64位,不能解析;而hello是32位,可以解析。

Thinking 1.3

**在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000 (其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但 一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照 内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到? (提示:思考实验中启动过程的两阶段分别由谁执行。) **

本实验使用的QEMU 模拟器支持直接加载 ELF 格式的内核,提供了 bootloader 的引导(启动)功能,故启动流程被简化为加载内核到内存,跳转到内核的入口。

  • QEMU 模拟器通过kernel.lds 控制各节被加载到的位置,使得顶层Makefile生成的内核加载到内存指定位置

  • kernel.lds中通过 ENTRY(_start) 来设置程序入口为 _start 函数(该符号通过EXPORT(_start)init\start.S被导出),链接后的程序从 _start 函数开始执行

    内核自己通过init\start.S初始化sp,跳转到mips_init

    1
    2
    li sp, 0x80400000
    j mips_init

二、难点分析

1.C语言指针使用

  • 结构体指针的使用

    1
    2
    3
    4
    5
    6
    7
    typedef struct {
    Elf32_Off e_shnum;
    ...
    } Elf32_Ehdr;
    Elf32_Ehdr *ehdr = ....
    Elf32_Off sh_entry_count = (*ehdr).e_shnum;//表示方法1
    Elf32_Off sh_entry_count = ehdr->e_shnum;//表示方法2
  • void类型指针强制转换

    (Elf32_Shdr *)p + 1 表示的地址与 (void *)p + sizeof(Elf32_Shdr) 相同

    shdr =(Elf32_Shdr *)(sh_table + i*sh_entry_size)要先在void*下计算偏移再强制转换

2.C语言变长参数使用

  • va_list ,定义变长参数表的变量类型,代码中的 ap 就是 va_list 类型的
  • va_start(va_list ap, lastarg) ,用于初始化变长参数表的宏
  • va_arg(va_list ap, 类型) ,用于取变长参数表下一个参数的宏
  • va_end(va_list ap) ,结束使用变长参数表的宏
1
2
//从可变参数列表中获取一个 int 类型参数的地址,并将其存储在指针变量 ip 中
int* ip = va_arg(ap,int *);

3.Makefile的深入学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lab                     ?= $(shell cat .mos-this-lab 2>/dev/null || echo 6)

lab-ge = $(shell [ "$$(echo $(lab)_ | cut -f1 -d_)" -ge $(1) ] && echo true)

ifeq ($(call lab-ge,3),true)
user_modules += user/bare
endif

ifeq ($(call lab-ge,4),true)
user_modules += user
endif

ifeq ($(call lab-ge,5),true)
user_modules += fs
targets += fs-image
endif

不同Lab不同测试样例

lab的值会影响$(user_modules)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
include include.mk

target_dir := target
mos_elf := $(target_dir)/mos
user_disk := $(target_dir)/fs.img
empty_disk := $(target_dir)/empty.img
qemu_pts := $(shell [ -f .qemu_log ] && grep -Eo '/dev/pts/[0-9]+' .qemu_log)
link_script := kernel.lds

modules := lib init kern
targets := $(mos_elf)

all: $(targets)

$(target_dir):
mkdir -p $@

tools:
CC="$(HOST_CC)" CFLAGS="$(HOST_CFLAGS)" $(MAKE) --directory=$@

$(modules): tools
$(MAKE) --directory=$@

$(mos_elf): $(modules) $(target_dir)
$(LD) $(LDFLAGS) -o $(mos_elf) -N -T $(link_script) $(objects)

objects := $(addsuffix /*.o, $(modules)) $(addsuffix /*.x, $(user_modules))

顶层Makefile,Make之后会产生target/mos内核文件,具体会发生下面的事:

  • $(targets)->$(mos_elf)

  • $(mos_elf)->$(modules)

    进入$(modules)也就是lib init kern三个目录下,make分别将.c文件编译为.o文件

  • $(mos_elf)->$(target_dir)

    生成target_dir

  • $(LD) -o $(mos_elf) -N -T $(link_script) $(objects)

    使用 kernel.lds $(objects) (所以的.o文件)链接, 输出到 $(mos_elf) 位置target/mos

4.printk()

图片

include/stdarg.h

C标准库,用于支持可变参数的接收

include/print.h

1
2
3
#include <stdarg.h>
typedef void (*fmt_callback_t)(void *data, const char *buf, size_t len);
void vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap);

kern/printk.c

1
2
3
#include <print.h>
void outputk(void *data, const char *buf, size_t len);//buf数组打印函数((
void printk(const char *fmt, ...);//printk函数定义,将ap和outputk()函数指针传入vprintfmt

kern/machine.c

1
2
#include <printk.h>
void printcharc(char ch);//向内存特定地址写字符 == 打印字符

lib/print.c

1
2
#include <print.h>
void vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap);//vprintfmt 函数实现了格式化输出的主体逻辑

三、实验体会

Lab1实验的学习让我体验了一把无头苍蝇的感觉,即使有顶层Makefile作为地图,我依然在不同文件的穿梭中狠狠迷路,繁杂的代码如何上手抽丝剥茧,我总结以下三点:

  • 整体把握

​ 如同我难点分析的第四点一样,我们应该对文件之间的依赖关系有一个整体的把握。通过仔细阅读Makefile理解实验代码究竟是如何层层运作的,脑子里模拟一下怎么make run

(lab2更难把握了!!!!!T_T)

  • 刨根问底

​ 这次顶层Makefile简直为我打开了新世界的大门原来还可以这么写,看不懂的地方不能无视跳过得过且过,勤快点粘给chatgpt辅助理解是个不错的选择。对于新知识比如可变参数列表,应该刨根问底追本溯源。因为懒惰我写课下的时候没有看cppreference,在课上extra涉及到它的“灵活使用“差点就寄了T_T

  • 注重细节

​ 我在课下的时候发现自己阴差阳错的通过了printk()测试

1
2
3
4
5
6
7
8
9
10
11
12
if(*fmt == '-'){
ladjust = 1;
padc = ' ';
fmt++;
}else if(*fmt == '0'){
ladjust = 0;
padc = '0';
//fmt++; 之前没有这句
}else{
ladjust = 0;
padc = ' ';
}

然后课上没注意,再次重演悲剧差点就寄

1
2
3
4
5
6
if(ch == '-'){
neg = 0;
//in(data,&ch,1);之前没有这句
}else if(////){
//...
}

由此可见注重细节不要复现习惯性错误的必要性