lab1实验报告

思考题

Thinking 1.1

题面

1
在阅读附录中的编译链接详解以及本章内容后,尝试分别使用实验环境中的原生 x86 工具链(gcc、ld、readelf、objdump 等)和 MIPS 交叉编译工具链(带有mips-linux-gnu- 前缀,如 mips-linux-gnu-gcc、mips-linux-gnu-ld),重复其中的编译和解析过程,观察相应的结果,并解释其中向 objdump 传入的参数的含义。

回答

作为GNU binutils工具集中一个重要成员,objdump命令的语法格式为:

1
objdump [options] <filename>

其中常用的[options]即参数有-d-D-S-C等:

作用 参数
将目标文件中的机器码反汇编成汇编指令,一般用于查看程序的执行逻辑 -d
-d选项类似,但反汇编范围更大,-d只反汇编.txet段,但-D反汇编所有段(只要其看起来像包含可执行代码或可被反汇编的)。 -D
混合显示源码和汇编代码,记得该选项需要在编译时使用-g选项生成调试信息 -S
将Cpp符号名逆向解析 -C

这样一来,指导书上的objdump -DS <filename>就是将filename中所有段进行反汇编,并将反汇编代码和源码交替显示。(使用该选项前,记得编译时要添加-g选项)

现有hello.c文件:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("Hello, World!!!");
return 0;
}

利用gcc -Ehello.c进行预处理,重定向输出至result.txt

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
git@23371355:~/23371355 (lab1)$ gcc -E hello.c > result.txt
git@23371355:~/23371355 (lab1)$ cat result.txt
# 0 "hello.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 28 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4
# 33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 394 "/usr/include/features.h" 3 4
# 1 "/usr/include/features-time64.h" 1 3 4
# 20 "/usr/include/features-time64.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 21 "/usr/include/features-time64.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 1 3 4
# 19 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 20 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 2 3 4
# 22 "/usr/include/features-time64.h" 2 3 4
# 395 "/usr/include/features.h" 2 3 4
# 502 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
# 576 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 577 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4
# 578 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 503 "/usr/include/features.h" 2 3 4
# 526 "/usr/include/features.h" 3 4
...
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1)));
# 959 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 983 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int main() {
printf("Hello, World!!!");
return 0;
}

gcc -c hello.c -o hello.o -g(只编译不链接)+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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
git@23371355:~/23371355 (lab1)$ gcc -c hello.c -o hello.o -g
git@23371355:~/23371355 (lab1)$ ls
codeSet hello.c hello.o include include.mk init kern kernel.lds lib Makefile mk out result.txt target tests tools
git@23371355:~/23371355 (lab1)$ objdump -DS hello.o

hello.o: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
#include <stdio.h>

int main() {
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
printf("Hello, World!!!");
8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>
f: 48 89 c7 mov %rax,%rdi
12: b8 00 00 00 00 mov $0x0,%eax
17: e8 00 00 00 00 call 1c <main+0x1c>
return 0;
1c: b8 00 00 00 00 mov $0x0,%eax
}
21: 5d pop %rbp
22: c3 ret
...
Disassembly of section .eh_frame:

0000000000000000 <.eh_frame>:
0: 14 00 adc $0x0,%al
2: 00 00 add %al,(%rax)
4: 00 00 add %al,(%rax)
6: 00 00 add %al,(%rax)
8: 01 7a 52 add %edi,0x52(%rdx)
b: 00 01 add %al,(%rcx)
d: 78 10 js 1f <.eh_frame+0x1f>
f: 01 1b add %ebx,(%rbx)
11: 0c 07 or $0x7,%al
13: 08 90 01 00 00 1c or %dl,0x1c000001(%rax)
19: 00 00 add %al,(%rax)
1b: 00 1c 00 add %bl,(%rax,%rax,1)
1e: 00 00 add %al,(%rax)
20: 00 00 add %al,(%rax)
22: 00 00 add %al,(%rax)
24: 23 00 and (%rax),%eax
26: 00 00 add %al,(%rax)
28: 00 45 0e add %al,0xe(%rbp)
2b: 10 86 02 43 0d 06 adc %al,0x60d4302(%rsi)
31: 5a pop %rdx
32: 0c 07 or $0x7,%al
34: 08 00 or %al,(%rax)
...

极长的一坨。

gcc hello.o -o hello(在前一个基础上进行链接)+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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
git@23371355:~/23371355 (lab1)$ gcc hello.o -o hello
git@23371355:~/23371355 (lab1)$ ls
codeSet hello hello.c hello.o include include.mk init kern kernel.lds lib Makefile mk out result.txt target tests tools
git@23371355:~/23371355 (lab1)$ objdump -DS hello

hello: 文件格式 elf64-x86-64


Disassembly of section .interp:

0000000000000318 <.interp>:
318: 2f (bad)
319: 6c insb (%dx),%es:(%rdi)
31a: 69 62 36 34 2f 6c 64 imul $0x646c2f34,0x36(%rdx),%esp
321: 2d 6c 69 6e 75 sub $0x756e696c,%eax
326: 78 2d js 355 <__abi_tag-0x37>
328: 78 38 js 362 <__abi_tag-0x2a>
32a: 36 2d 36 34 2e 73 ss sub $0x732e3436,%eax
330: 6f outsl %ds:(%rsi),(%dx)
331: 2e 32 00 cs xor (%rax),%al

Disassembly of section .note.gnu.property:
...
Disassembly of section .debug_line_str:

0000000000000000 <.debug_line_str>:
0: 68 65 6c 6c 6f push $0x6f6c6c65
5: 2e 63 00 cs movsxd (%rax),%eax
8: 2f (bad)
9: 68 6f 6d 65 2f push $0x2f656d6f
e: 67 69 74 2f 32 33 33 imul $0x31373333,0x32(%edi,%ebp,1),%esi
15: 37 31
17: 33 35 35 00 2f 75 xor 0x752f0035(%rip),%esi # 752f0052 <_end+0x752ec03a>
1d: 73 72 jae 91 <__abi_tag-0x2fb>
1f: 2f (bad)
20: 69 6e 63 6c 75 64 65 imul $0x6564756c,0x63(%rsi),%ebp
27: 00 73 74 add %dh,0x74(%rbx)
2a: 64 fs
2b: 69 .byte 0x69
2c: 6f outsl %ds:(%rsi),(%dx)
2d: 2e cs
2e: 68 .byte 0x68
...

对于使用MIPS交叉工具链(如:mips-linux-gnu-gcc)的文件,也可以用相应的反汇编工具mips-linux-gnu-objdump进行相应的反汇编:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
git@23371355:~/23371355 (lab1)$ mips-linux-gnu-gcc -c hello.c -o hello.o -g
git@23371355:~/23371355 (lab1)$ mips-linux-gnu-gcc hello.o -o hello
git@23371355:~/23371355 (lab1)$ mips-linux-gnu-objdump -d hello.o

hello.o: 文件格式 elf32-tradbigmips


Disassembly of section .text:

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
...
git@23371355:~/23371355 (lab1)$ mips-linux-gnu-objdump -S hello

hello: 文件格式 elf32-tradbigmips


Disassembly of section .init:

004004e4 <_init>:
4004e4: 3c1c0002 lui gp,0x2
4004e8: 279c7b2c addiu gp,gp,31532
4004ec: 0399e021 addu gp,gp,t9
4004f0: 27bdffe0 addiu sp,sp,-32
4004f4: afbc0010 sw gp,16(sp)
4004f8: afbf001c sw ra,28(sp)
4004fc: 8f828020 lw v0,-32736(gp)
400500: 10400004 beqz v0,400514 <_init+0x30>
400504: 00000000 nop
400508: 8f998020 lw t9,-32736(gp)
40050c: 0320f809 jalr t9
400510: 00000000 nop
400514: 8fbf001c lw ra,28(sp)
400518: 03e00008 jr ra
40051c: 27bd0020 addiu sp,sp,32
...
Disassembly of section .fini:

004006e0 <_fini>:
4006e0: 3c1c0002 lui gp,0x2
4006e4: 279c7930 addiu gp,gp,31024
4006e8: 0399e021 addu gp,gp,t9
4006ec: 27bdffe0 addiu sp,sp,-32
4006f0: afbc0010 sw gp,16(sp)
4006f4: afbf001c sw ra,28(sp)
4006f8: 8fbf001c lw ra,28(sp)
4006fc: 03e00008 jr ra
400700: 27bd0020 addiu sp,sp,32

Thinking 1.2

题面

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

回答

readelf解析内核ELF结果:

解析内核ELF文件结果

readelf -h readelf

readelf -h readelf

readelf -h hello

readelf -h hello

1
2
3
4
5
6
7
8
9
10
11
12
13
%.o: %.c
$(CC) -c $<

.PHONY: clean

readelf: main.o readelf.o
$(CC) $^ -o $@

hello: hello.c
$(CC) $^ -o $@ -m32 -static -g

clean:
rm -f *.o readelf hello

可以清楚的看出hello目标的编译较readelf目标的编译多了-m32-static-g三个选项,通过咨询GPT,得知其中-m32选项指示GCC编译器生成32位架构的目标代码-static选项指示GCC编译器进行静态链接,将程序依赖的所有库都打包到最终的可执行文件中,其体积会比动态链接大得多-g选项指示GCC编译器在生成的目标代码中包含调试信息,会增加可执行文件的大小

通过控制变量法,也就是分别单独去除这三个选项后再编译,发现其中去除掉-m32选项后,自编写的readelf对生成的hello可执行文件并不能进行作用,观察readelf -h readelfreadelf -h hello的结果发现文件类别前者是ELF64,而后者是ELF32,说明自编写的readelf程序只能对32位可执行文件起作用,而这应该是因为编写的readelf程序的使用的结构体变量在elf.h中定义时都是32位的缘故:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
typedef uint16_t Elf32_Half;

/* Types for signed and unsigned 32-bit quantities. */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;

/* Types for signed and unsigned 64-bit quantities. */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;

/* Type of addresses. */
typedef uint32_t Elf32_Addr;

/* Type of file offsets. */
typedef uint32_t Elf32_Off;

/* Type for section indices, which are 16-bit quantities. */
typedef uint16_t Elf32_Section;

/* Type of symbol indices. */
typedef uint32_t Elf32_Symndx;

typedef struct {
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;

/* Fields in the e_ident array. The EI_* macros are indices into the
array. The macros under each EI_* macro are the values the byte
may have. */

#define EI_MAG0 0 /* File identification byte 0 index */
#define ELFMAG0 0x7f /* Magic number byte 0 */

#define EI_MAG1 1 /* File identification byte 1 index */
#define ELFMAG1 'E' /* Magic number byte 1 */

#define EI_MAG2 2 /* File identification byte 2 index */
#define ELFMAG2 'L' /* Magic number byte 2 */

#define EI_MAG3 3 /* File identification byte 3 index */
#define ELFMAG3 'F' /* Magic number byte 3 */

/* Section segment header. */
typedef struct {
Elf32_Word sh_name; /* Section name */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section addr */
Elf32_Off sh_offset; /* Section offset */
Elf32_Word sh_size; /* Section size */
Elf32_Word sh_link; /* Section link */
Elf32_Word sh_info; /* Section extra info */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Section entry size */
} Elf32_Shdr;

/* Program segment header. */

typedef struct {
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
/* End of Key Code "readelf-struct-def" */

Thinking 1.3

题面

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

回答

实验中启动过程即硬件启动加载内核已经分别由QEMU模拟器和kernel.lds来完成,其中前者已经提供了bootloader的引导(启动)功能,即实验代码运行的第一行代码之前,就已经拥有一个正常的程序运行环境,内存和一些外围设备都可正常使用。而后者则设定好了各个节被加载到的位置,且通过ENTRY(_start)设置程序入口为_start,保证了跳转到正确的内核入口,此后即可通过顶层make得到相应的mos内核,完成启动内核的任务。

lab1课下实验的可能的坑点

Exercise 1.1

题目本身不是很难,主要是需要理解清提示的三个文件elf.hreadelf.cmain.c

1.elf.h中主要需要认真看懂Elf32_EhdrElf32_ShdrElf32_Phdr三个结构体,理解成员性质。

2.再来看main.c,因为其中引用了readelf.c中的需要我们填空的readelf函数:

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
28
29
30
31
32
33
34
35
36
37
/* Lab 1 Key Code "readelf-main" */
FILE *fp = fopen(argv[1], "rb");
if (fp == NULL) {
perror(argv[1]);
return 1;
}

if (fseek(fp, 0, SEEK_END)) {
perror("fseek");
goto err;
}
int fsize = ftell(fp);
if (fsize < 0) {
perror("ftell");
goto err;
}

char *p = malloc(fsize + 1);
if (p == NULL) {
perror("malloc");
goto err;
}
if (fseek(fp, 0, SEEK_SET)) {
perror("fseek");
goto err;
}
if (fread(p, fsize, 1, fp) < 0) {
perror("fread");
goto err;
}
p[fsize] = 0;
/* End of Key Code "readelf-main" */

return readelf(p, fsize);
err:
fclose(fp);
return 1;

其中,(以下均摘自C 标准库 – | 菜鸟教程)

1.fseek(FILE *stream, long int offset, int whence)的功能是设置stream的文件位置为从whence位置开始偏移offset的位置,如果成功返回0;反之返回非零值。且whence有一些常用的常量:SEEK_SET:文件开头;SEEK_CUR:文件指针当前位置;SEEK_END:文件末尾。

2.ftell(FILE *stream)返回的是stream当前位置相对文件首的偏移的字节数。

3.fread(void *ptr, size_t size, size_t nmemb, FILE *stream)是从stream中读取size*nmemb字节的内存块到ptr中,其中size为读取以字节为单位的每个元素大小;nmemb为元素个数,每个元素大小为size字节。读取成功返回成功读取元素总数。否则若与nmemb不同,则可能发生了错误或到达文件末尾

因此根据代码可以判断,fsizefp文件内的总字节数,而p为新开辟的总字节数为fsize+1的空间的首指针并且前fsize字节内容同fp一致,最后一个字节内容为0。

这样一来就可以知道向readelf函数传到到底都是什么参数了,不过好像完成填空并不需要知道参数具体是啥,填空这里只要根据结构体去进行相应的赋值即可,倒是这里的地址输出,一开始我是类似如下书写的:

1
2
3
4
p = (xxx *) table + xxx_size * i;
addr = p->xxx_addr;
printf(...)
#这里只是简化的例子

但这样会得到一个十分奇怪的结果,回顾了强制类型转换以及指针的用法后并在实践后,我大致得知了原因,这里table*先强制类型转换成(xxx *)那么此后对于它的移动都将以xxx的大小进行移动**,也就是以xxx_size移动,因此这里*移动xxx_size*i相当于以字节为单位移动了xxx_size*xxx_size*i**,这显然是不对的。因此改成p = (xxx *)table + i或者p = (xxx *)(table + xxx_size * i),也就是先强制转换然后按照xxx大小移动或者先按字节移动再强制转换,这两者都是行得通的。

这里readelf.c中操作的基本上都是Elf32_xxx,说明它可能只能操作32位文件的,或许对Thinking 1.2有帮助。

Exercise 1.2

1.首先就是观察好include/mmu.h中的内存布局图,尤其注意Kernel Text的位置,之后就是依葫芦画瓢了。

2.这里还有一个我踩的语法小问题,就是给定位计数器.赋值的语句末尾是有;的,其他根据指导书填即可。尤其还要注意Linker Script编码时=左右两边的空格。

Exercise 1.3

1.只需运用好上学期的MPIS知识即可。

关注到赋值地址的低16位都是0,所以lui启动。当然由于已经有定义栈顶的宏KSTACKTOP,使用la指令,或者用支持32位赋值的li指令都是可以实现的。

2.另外如果这里你编译会卡死,请第一时间使用Ctrl+A 后按下 X 来终止,如果make test lab=1_2 && make run后并没有报错只是一味卡死,这并不是你的原因,因为我们还没有实现的Exercise 1.4在实现之前是死循环,运行当然会进入死循环卡死。因此你不必要懊恼,继续前进即可!!!

Exercise 1.4

1.主要是一些C语言的实现逻辑,注意要仔细看指导书上给的printk的格式符%[flags][width][length][specifier]

关注五个变量

1
2
3
4
5
int width;	   // output's least width
int long_flag; // output is long (rather than int)
int neg_flag; // output is negative
int ladjust; // output is left-aligned
char padc; // padding char

根据如下流程图一步步实现即可完成相应的代码编写:

2.一个可能潜在的坑:第八个空,一定要认真看print_num()函数的形参的属性,以及函数功能。因为其中的num传入的其实无符号长整型数,这是因为计算机中所有存储数的方式都是二进制,因此当你将负数传入时,该函数收获的将是它的补码形式,而print_num()函数处理负数的方式其实是先输出**这个数(补码形式)**,然后再根据neg_flag判断是否要加上一个-,所以该怎么输入才能正确输出就留给读者自行思考了

lab1难点分析

ELF——操作系统内核的本质(解析内核文件)

ELF文件结构

1.ELF头,含有程序基本信息,如体系结构OS,同时包含节头表和段头表相对文件的偏移量

2.段头表(程序头表),包含程序各段信息,需在运行时使用

3.节头表,包含程序各节信息,需在编译和链接时使用。

4.段头表中每一个表项,记录该段数据载入内存时目标位置等用于指导应用程序加载的各类信息

5.节头表中每个表项,记录该节数据程序代码段、数据段等各段内容,记录的是该节数据的文件内偏移和地址信息等,主要在链接时使用。

而其中节头表和段头表只是程序数据的两种视图,由节组成段。

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
28
29
typedef struct {
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
...
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
...
} Elf32_Ehdr;

typedef struct {
...
Elf32_Addr sh_addr; /* Section addr */
Elf32_Off sh_offset; /* Section offset */
Elf32_Word sh_size; /* Section size */
...
} Elf32_Shdr;

typedef struct {
...
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
...
} Elf32_Phdr;

ELF文件头实际上存了关于ELF文件信息的结构体,其中存储的魔数是有效ELF的通行证,是用来判断文件类型的唯一依据。此外,其中的节头表入口偏移e-shoff是用来定位节头表第一项的地址的,用ELF的文件头地址加上入口偏移得到的就是其第一项的地址。

还有一种随编译工具链提供的工具readelf用法为readelf [option(s)] <elf-file(s)>,来解析ELF文件信息。可以通过readelf --help来了解其功能。

MIPS内存布局——内核运行的正确位置(加载内核至正确的位置)

MIPS内存布局

分区 地址范围 可用时机 是否需要TLB转换 是否需要经过cache
kuseg 0x0000000-0x7fffffff 用户态与内核态
kseg0 0x80000000-0x9fffffff 内核态
kseg1 0xA0000000-0xBfffffff 内核态
kseg2 0xC0000000-0xffffffff 内核态

由于TLB需要OS来管理,因此载入内核时,不能选需要TLB转换的虚地址空间。则,内核只能放在kseg0kseg1中。而后者不经过cache,一般来说,利用MMIO访问外设时才使用kseg1,因此将内核的.text.data.bss段放在前者,将bootloader放在后者,这样一来载入内存前在kseg1中的bootloader会进行cache初始化工作,使得kseg0可以进行存取。

需要注意的是,kusegkseg0kseg1kseg2位于不同的虚拟地址空间,但都映射到同一个物理地址空间,不同的只在于映射方式和访问权限。

printk函数

printk函数中的va_listva_startva_end是C语言中的处理变长参数的方法。
简而言之,当函数参数列表末尾有省略号(...,是英文的三个.哈,可不是中文的…),该函数就拥有了变长的参数表(其实就是支持了不定数参数传入,确实方便不少)。

这里为了定位变长参数表的起始位置,函数需有至少一个固定参数,且变长参数必须在参数表末尾

stdarg.h头文件中为处理变长参数表定义了一组宏和变量类型

1.va_list,变长参数表的变量类型

2.va_start(va_list ap, lastarg),用于初始化变长参数表的宏

3.va_arg(va_list ap, 类型),用于取变长参数表下一个参数的宏

4.va_end(va_list ap)结束使用变长参数表的宏

在拥有变长参数表的函数中使用变长参数表前,要先声明一个类型为va_list的变量ap,然后用va_start进行一次初始化:

1
2
3
4
5
6
7
va_list ap;
va_start(ap, lastarg)
#这里的lastarg是函数最后一个命名的形参,初始化后,每次可用va_arg宏来获取一个形参,该宏也同时会修改ap使得下次被调用将返回当前获取参数的下一个参数,近似为pop的功能
#例:
int num;
num = va_arg(ap, int);
#所有参数处理完毕后,退出函数前,需调用va_end宏来结束变长参数表的使用

细看printk函数,它本质上就是将fmt存储的内容通过格式化vprintfmt通过回调函数outputk进行相应的输出,(此处在一开始并没有理解很深,导致lab1-extra败北),而回调函数可以看作是一个输出函数的壳子,可以通过定义不同的回调上下文,实现不同的输出形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void outputk(void *data, const char *buf, size_t len) {
for (int i = 0; i < len; i++) {
printcharc(buf[i]);
}
}
#实际上是通过调用printcharc()这个函数来输出一个字符串

void printcharc(char ch) {
if (ch == '\n') {
printcharc('\r');
}
while (!(*((volatile uint8_t *)(KSEG1 + MALTA_SERIAL_LSR)) & MALTA_SERIAL_THR_EMPTY)) {
}
*((volatile uint8_t *)(KSEG1 + MALTA_SERIAL_DATA)) = ch;
}
#实际上就是对某一内存地址写了一个字节

此处是将输出输出到控制台上,因此printk中给定的回调上下文为NULL,当然也可以定义其他的回调上下文达到重定向输出的效果,参考lab1-extra,此处回调函数的第一个参数只是void *的形式,因此传入的回调上下文只要是一个指针即可

lab1课上

exam

在课下的vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap)函数的基础上给其中的参数fmt多考虑一种格式:%[flags][width][length]k,其将格式化输出一个键值对,也就是,读到k时,后面会依次跟着两个参数keyvalue,其中key是一个字符串,value是一个整数(可能是int,也可能是long int,取决于[length]),最终输出<key> => <value>,其中**是key按%[flags][width]s格式化的结果,是value按%[flags][width][length]d格式化的结果注意:=>之间的空格是必须的**。

只有在读到k后,读取一个字符串,再读取一个整数,前者按s格式输出,后者按d格式输出即可,这里要注意**=>**可没有格式化,也就是按s输出字符串=>[width]=0,代码(只放出修改的部分)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//./lib/print.c
case 'k':
case 'K':
s = (char *)va_arg(ap, char *);
print_str(out, data, s, width, ladjust);
char * str = " => ";
print_str(out, data, str, 0, 0);
if (long_flag) {
num = va_arg(ap, long int);
} else {
num = va_arg(ap, int);
}
if (num < 0) {
neg_flag = 1;
num *= -1;
}
print_num(out, data, num, 10, neg_flag, width, ladjust, padc, 0);
break;

extra

题面

实现fmemopen函数,并仿照课下实现的printk函数实现``fmemprintf函数,使得**调用该函数可以将格式化的结果输出到内存流中**。为使内存流功能完整,你还需要额外实现辅助函数fseekfclose`。

已定义FILE结构体表示一个打开的文件流:

1
2
3
4
5
typedef struct {
char *ptr; //内存流的写指针,指向下一个将要写入数据的内存地址。
char *base; //内存流的基指针(头指针),指向内存流的起始地址,即内存流的第一个字节。
char *end; //内存流的尾指针,指向内存流的结束地址,即内存流最后一个字节的下一个位置。
} FILE;

函数声明

fmemopen函数

FILE *fmemopen(FILE *stream, void *buf, const char *mode);
函数功能:打开内存流,并向参数 stream 指向的结构体中初始化内存流的相关信息。

参数:

stream :调用者提供的 FILE 结构体指针,指向一个未初始化的 FILE 结构体。你需要在其中保存内存流的相关信息。

buf :指向内存缓冲区的指针,需要向其中写入fmemprintf格式化得到的结果。

mode :模式字符串(注意是字符串,而不是字符别问我为什么要强调),含义如下:

当参数 mode 为 “w”(注意是双引号吼) 时,以写模式打开内存流。此时内存流内容初始为空,内存流的基指针、写指针、尾指针同时指向 buf。
当参数 mode 为 “a” 时,以追加模式打开内存流。此时内存流内容初始为缓冲区 buf 中原有的字符串内容,内存流的基指针指向 buf,写指针、尾指针指向 buf 中第一个 ‘\0’ 的位置。
返回值 :

调用成功时,返回传入的第一个参数 stream。如果参数 mode 不是以上定义的两种,则返回 NULL。

fmemprintf函数

int fmemprintf(FILE *stream, const char *fmt, ...);
函数功能:向内存流 stream 中写入格式化的结果。格式化的结果会写入内存流 stream 的写指针指向的地址开始的内存区域。同时写指针会向前移动移动的字节数等于写入的字节数。在内存流内部写入数据会覆盖原有内容。如果写指针 ptr 移动到内存流尾指针 end 外,则内存流会自动扩展至足够容纳新数据,即内存流的尾指针 end 会向前移动至写入数据后的写指针 ptr 处

参数:

stream :要写入的内存流。
fmt :格式字符串,与 printk 函数中的定义相同。
返回值 :

返回写入的字符总数。

fseek函数

int fseek(FILE *stream, long offset, int fromwhere);
函数功能:将内存流 stream 的写指针移至以fromwhere为基准,偏移offset个字节的位置。如果偏移之后,写指针位于基指针与尾指针之间(闭区间 [base, end] 内),则函数调用成功,返回 0;否则函数调用失败,不改变写指针位置,并返回 -1。参数fromwhere的取值和含义如下:

取值 含义
SEEK_SET 以内存流起始位置 base 为基准,偏移 offset 个字节
SEEK_CUR 以写指针当前位置 ptr 为基准,偏移 offset 个字节
SEEK_END 以内存流结束位置 end 为基准,偏移 offset 个字节

fclose函数

int fclose(FILE *stream);
函数功能:向内存流结束位置 end 处写入结束符 **’\0’**,以保证内存流中的内容可以使用 C 字符串的格式正确读取,返回值固定为 0。

使用限制

保证向fmemopen传入的缓冲区 buf 的有效大小足够容纳后续需要写入的格式化结果。
保证以追加模式打开内存流时,缓冲区 buf 中一定存在 ‘\0’ 字节。
保证每个用fmemopen正确打开的内存流调用且仅调用一次fclose函数,且调用fclose函数后,不再对内存流进行fmemprintffseek操作。
保证仅在fclose之后检查缓冲区 buf 内容的正确性。

题目要求

include/stream.h文件已在初始化分支时向仓库中添加,具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef _STREAM_H_
#define _STREAM_H_

#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2

typedef struct {
char *ptr;
char *base;
char *end;
} FILE;

FILE *fmemopen(FILE *stream, void *buf, const char *mode);
int fmemprintf(FILE *stream, const char *fmt, ...);
int fseek(FILE *stream, long offset, int fromwhere);
int fclose(FILE *stream);

#endif /* _STREAM_H_ */

此外,你需要按照下面的步骤修改lib/string.c

在文件头部引入相关的头文件:

1
2
#include <print.h>
#include <stream.h>

在文件尾部实现 fmemopenfmemprintffseekfclose 函数。

Hints

  • 可调用vprintfmt完成格式化字符串的解析
  • void vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap)中,outdata 分别叫做回调函数、回调上下文。printk 函数直接输出到控制台,因此可以不使用 data;在实现 fmemprintf 函数时,你可以**利用参数 data 传递 FILE 结构体(没认真看到这个,看了博客后先入为主了)**,并仿照 outputk 函数实现自定义的回调函数。

解答

先看课下实现的printk()函数和outputk()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Lab 1 Key Code "outputk" */
void outputk(void *data, const char *buf, size_t len) {
for (int i = 0; i < len; i++) {
printcharc(buf[i]);
}
}
/* End of Key Code "outputk" */

/* Lab 1 Key Code "printk" */
void printk(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vprintfmt(outputk, NULL, fmt, ap);
va_end(ap);
}
/* End of Key Code "printk" */

其中,printk()函数只是通过将回调函数、回调上下文、格式化字符串和可变参数列表传给vprintfmt()函数,由vprintfmt()函数通过回调函数将格式化后的结果输出到指定的区域内,因此,仿照此并认真阅读提示后,可以得到如下的fmemprintf()函数和newOutputk()函数:

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
void newOutputk(void * data, const char *buf, size_t len) {
FILE * stream = (FILE *)data; //直接传入FILE *,这样一来可以直接修改相应内容,一定要认真看题目!!!
char * ptr = stream->ptr;
for (int j = 0; j < len; j++){
*ptr = buf[j];
ptr++;
}
stream->ptr = ptr;
return;
}

int fmemprintf(FILE *stream, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
char * oriPtr = stream->ptr;
vprintfmt(newOutputk, stream, fmt, ap); //此处回调上下文直接传入FILE *指针,认真看题目!!!
//关于此处为什么不能直接传入stream->ptr,然后在newoutputk函数中进行相应的输出,那是因为,如果这里传入的将只会是stream->ptr的拷贝,也就是另一个指向stream->ptr指向地方的指针,如果在拷贝的上进行修改并不会去修改原有的stream->ptr的指向,导致调用vprintfmt函数后这里用newPtr获取的指针仍是指向原有的地址,这显然是不对的。
//但是如果传入FILE *,同样传入的也是其拷贝,但是在newOutputk中并没有对拷贝后的FILE*指针进行加减改变,也就是对于拷贝后的FILE *指针的改变就是对于原有指针的改变,妙啊()QAQ!!!
char * newPtr = stream->ptr;
char * base = stream->base;
char * end = stream->end;
if ((end - base) < (newPtr - base)) {
stream->end = newPtr;
}
va_end(ap);
return (newPtr - oriPtr);
}

fmemopen()函数如下:

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
FILE *fmemopen(FILE *stream, void *buf, const char *mode) {
if (strlen(mode) != 1) {
return NULL;
} //注意这里要先判断mode是不是一个字符组成的字符串,还是审题不清的问题
switch(*mode) {
case ('w') :
stream->ptr = (char *) buf;
stream->base = (char *) buf;
stream->end = (char *) buf;
return stream;
break;
case ('a') :
stream->base = (char *) buf;
char * temp = (char *) buf;
while (*temp != '\0') {
temp++;
}
stream->ptr = temp;
stream->end = temp;
return stream;
break;
default :
return NULL;
}
}

fseek()函数和fclose()函数:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int fseek(FILE *stream, long offset, int fromwhere) {
char * base = stream->base;
char * cur = stream->ptr;
char * end = stream->end;
long length1 = (end - base);
long length2 = (cur - base);
switch (fromwhere) {
case (SEEK_SET) :
if (offset > length1) {
return -1;
}
else if (offset == length1) {
stream->ptr = end;
return 0;
}
else {
if (offset < 0) {
return -1;
}
while (offset--) {
base++;
}
stream->ptr = base;
return 0;
}
break;
case (SEEK_CUR) :
if (offset > length2) {
return -1;
}
else if (offset == length2) {
stream->ptr = end;
return 0;
}
else {
if (offset < 0) {
return -1;
}
while (offset--) {
cur++;
}
stream->ptr = cur;
return 0;
}
break;
case (SEEK_END) :
if (offset != 0) {
return -1;
}
else {
stream->ptr = end;
return 0;
}
break;
default :
return -1;

}
}
int fclose(FILE *stream) {
char * end = stream->end;
*end = '\0';
return 0;
}

经验教训

  • 一定要认真审题,这次由于审题不清导致了全面败退,一定要警钟长鸣
  • 写代码过于慢(机房电脑键盘实在差劲),导致后面发现错误也没时间debug了,要提高编写时间,考场上不用去想什么很好的实现,对于这种没性能要求的考试完成即可
  • 不要先入为主,往年博客仅作为参考,切不可奉为圭臬,这次我就是把往年博客的相关内容拷贝到跳板机本地,然后想照着写,结果并行不通,犯了先入为主的大忌了。
  • C语言基础过于薄弱,太久没碰C了,生疏太多,一定要偶尔温习,不要被Java迷了心窍

心得体会

  • 虽然一下子突然出现的大量的内核代码一开始很唬人,但是在仔细阅读指导书和讲解视频后,还是能够较好的理解部分代码,并且完成主要是C语言考察的任务
  • 不过,很多概念的掌握还是有待提高,bootloader的具体启动过程由于在qemu环境下被拦腰折断了一大半,导致除了一些概念的理解,我对于启动的具体过程还是一知半解,而ELF的认识也是十分机械的,停留在由什么组成等机械概念,此外还有回调函数的概念(直到lab1-extra)也是不清不楚。但是,总归还是有收获的,不过仍需继续努力!!!