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 -E
对hello.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
结果:
readelf -h readelf
:
readelf -h hello
:
1 2 3 4 5 6 7 8 9 10 11 12 13 %.o: %.c $(CC) -c $< .PHONY : cleanreadelf: main.o readelf.o $(CC) $^ -o $@ hello: hello.c $(CC) $^ -o $@ -m32 -static -gclean: rm -f *.o readelf hello
可以清楚的看出hello
目标的编译较readelf
目标的编译多了-m32
、-static
、-g
三个选项,通过咨询GPT,得知其中-m32
选项指示GCC编译器生成32位架构的目标代码 ;-static
选项指示GCC编译器进行静态链接,将程序依赖的所有库都打包到最终的可执行文件中,其体积会比动态链接大得多 ;-g
选项指示GCC编译器在生成的目标代码中包含调试信息,会增加可执行文件的大小 。
通过控制变量法,也就是分别单独去除这三个选项后再编译 ,发现其中去除掉-m32
选项后,自编写的readelf
对生成的hello
可执行文件并不能进行作用,观察readelf -h readelf
和readelf -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;typedef uint32_t Elf32_Word;typedef int32_t Elf32_Sword;typedef uint64_t Elf32_Xword;typedef int64_t Elf32_Sxword;typedef uint32_t Elf32_Addr;typedef uint32_t Elf32_Off;typedef uint16_t Elf32_Section;typedef uint32_t Elf32_Symndx;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; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; } Elf32_Ehdr;#define EI_MAG0 0 #define ELFMAG0 0x7f #define EI_MAG1 1 #define ELFMAG1 'E' #define EI_MAG2 2 #define ELFMAG2 'L' #define EI_MAG3 3 #define ELFMAG3 'F' 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;
Thinking 1.3 题面 1 在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000 (其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到?(提示:思考实验中启动过程的两阶段分别由谁执行。)
回答 实验中启动过程即硬件启动 和加载内核 已经分别由QEMU
模拟器和kernel.lds
来完成,其中前者已经提供了bootloader
的引导(启动)功能,即实验代码运行的第一行代码之前,就已经拥有一个正常的程序运行环境,内存和一些外围设备都可正常使用 。而后者则设定好了各个节被加载到的位置,且通过ENTRY(_start)
设置程序入口为_start
,保证了跳转到正确的内核入口,此后即可通过顶层make
得到相应的mos
内核,完成启动内核的任务。
lab1课下实验的可能的坑点 Exercise 1.1 题目本身不是很难,主要是需要理解清提示的三个文件elf.h
、readelf.c
和main.c
。
1.elf.h
中主要需要认真看懂Elf32_Ehdr
、Elf32_Shdr
和Elf32_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 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 ; 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
不同,则可能发生了错误或到达文件末尾 。
因此根据代码可以判断,fsize
是fp
文件内的总字节数,而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; int long_flag; int neg_flag; int ladjust; char padc;
根据如下流程图一步步实现即可完成相应的代码编写:
2.一个可能潜在的坑:第八个空,一定要认真看print_num()
函数的形参的属性,以及函数功能。因为其中的num
传入的其实无符号长整型数 ,这是因为计算机中所有存储数的方式都是二进制,因此当你将负数传入时,该函数收获的将是它的补码形式,而print_num()
函数处理负数的方式其实是先输出**这个数(补码形式)**,然后再根据neg_flag
判断是否要加上一个-
,所以该怎么输入才能正确输出就留给读者自行思考了。
lab1难点分析 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]; ... Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; ... } Elf32_Ehdr;typedef struct { ... Elf32_Addr sh_addr; Elf32_Off sh_offset; Elf32_Word sh_size; ... } Elf32_Shdr;typedef struct { ... Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; ... } Elf32_Phdr;
ELF
文件头实际上存了关于ELF
文件信息的结构体,其中存储的魔数 是有效ELF
的通行证,是用来判断文件类型的唯一依据 。此外,其中的节头表入口偏移e-shoff
是用来定位节头表第一项的地址 的,用ELF
的文件头地址加上入口偏移得到的就是其第一项的地址。
还有一种随编译工具链提供的工具readelf
用法为readelf [option(s)] <elf-file(s)>
,来解析ELF
文件信息。可以通过readelf --help
来了解其功能。
MIPS内存布局——内核运行的正确位置(加载内核至正确的位置)
分区
地址范围
可用时机
是否需要TLB转换
是否需要经过cache
kuseg
0x0000000-0x7fffffff
用户态与内核态
是
是
kseg0
0x80000000-0x9fffffff
内核态
否
是
kseg1
0xA0000000-0xBfffffff
内核态
否
否
kseg2
0xC0000000-0xffffffff
内核态
是
是
由于TLB 需要OS来管理,因此载入内核时,不能选需要TLB 转换的虚地址空间。则,内核只能放在kseg0
或kseg1
中。而后者不经过cache
,一般来说,利用MMIO 访问外设时才使用kseg1
,因此将内核的.text
、.data
、.bss
段放在前者,将bootloader
放在后者,这样一来载入内存前在kseg1
中的bootloader
会进行cache
初始化工作,使得kseg0
可以进行存取。
需要注意的是,kuseg
、kseg0
、kseg1
、kseg2
位于不同的虚拟地址空间,但都映射到同一个物理地址空间,不同的只在于映射方式和访问权限。
printk函数 printk
函数中的va_list
、va_start
、va_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
时,后面会依次跟着两个参数key
和value
,其中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 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 ;
题面 实现fmemopen
函数,并仿照课下实现的printk
函数实现``fmemprintf函数,使得**调用该函数可以将格式化的结果输出到内存流中**。为使内存流功能完整,你还需要额外实现辅助函数
fseek和
fclose`。
已定义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
函数后,不再对内存流进行fmemprintf
、fseek
操作。 保证仅在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
此外,你需要按照下面的步骤修改lib/string.c
:
在文件头部引入相关的头文件:
1 2 #include <print.h> #include <stream.h>
在文件尾部实现 fmemopen
、fmemprintf
、fseek
、fclose
函数。
Hints
可调用vprintfmt
完成格式化字符串的解析
在void vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap)
中,out
、data
分别叫做回调函数、回调上下文 。printk 函数直接输出到控制台,因此可以不使用 data;在实现 fmemprintf 函数时,你可以**利用参数 data 传递 FILE 结构体(没认真看到这个,看了博客后先入为主了)**,并仿照 outputk 函数实现自定义的回调函数。
解答 先看课下实现的printk()
函数和outputk()
函数:
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]); } }void printk (const char *fmt, ...) { va_list ap; va_start(ap, fmt); vprintfmt(outputk, NULL , fmt, ap); va_end(ap); }
其中,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; 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); 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 ; } 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)也是不清不楚。但是,总归还是有收获的,不过仍需继续努力!!!